Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(monitor): add xcall loadgen to the monitor app #2723

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions e2e/app/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,8 @@ func writeMonitorConfig(ctx context.Context, def Definition, logCfg log.Config,
cfg.RPCEndpoints = endpoints
cfg.XFeeMngr.RPCEndpoints = xfeemngrEndpoints
cfg.XFeeMngr.CoinGeckoAPIKey = def.Cfg.CoinGeckoAPIKey
cfg.XCaller.Enabled = def.Manifest.XCallerEnabled
cfg.XCaller.ChainIDs = def.Manifest.XCallerChainIDPairs

if err := monapp.WriteConfigTOML(cfg, logCfg, filepath.Join(confRoot, configFile)); err != nil {
return errors.Wrap(err, "write monitor config")
Expand Down
5 changes: 5 additions & 0 deletions e2e/manifests/devnet1.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ anvil_chains = ["mock_l1", "mock_l2"]
multi_omni_evms = true
prometheus = true
deploy_solve = true
xcaller_enabled = true

[node.validator01]
[node.validator02]

[node.fullnode01]
mode="archive"

[xcaller_chainid_pairs]
mock_l1 = "mock_l2"
mock_l2 = "mock_l1"
Comment on lines +8 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts on config.

  • let's make it clear these are part of loadgen config
  • we use chain names, not chain ids.
  • do we have a reason for specifying which xcalls to to make xcalls from / to? if not, let's just do all of them. until we have a reason for more granular control
[loadgen.xcalls]
enabled = true

if we do have a need to specify, I think pairs are limitting. for ex, we could not loadtest xcalls on all streams by configuring pairs (at least, not as written). What about just specifying source and destination chains with a regex

[loadgen.xcall]
from = "mock_l1|mock_l2"
to = "*"

6 changes: 6 additions & 0 deletions e2e/types/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ type Manifest struct {

// FeatureFlags defines the feature flags to enable.
FeatureFlags feature.Flags `toml:"feature_flags"`

// XCallerEnabled defines whether to enable the xcaller load generator for the monitor app.
XCallerEnabled bool `toml:"xcaller_enabled"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets follow the existing loadgen pattern, "Validator self-delegation" is simply enabled by presence of the key. Also, avoid complex configuration options in v1, simply hardcode the chain pairs in monitor. lessismore.


// XCallerChainIDPairs maps chan ID pairs to make xcalls from -> to for the monitor xcall loadgen.
XCallerChainIDPairs map[string]string `toml:"xcaller_chainid_pairs"`
}

// Seeds returns a map of seed nodes by name.
Expand Down
4 changes: 2 additions & 2 deletions lib/netconf/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (s Static) ExecutionRPC() string {
if s.Network == Devnet {
// First omni_evm in devnet docker-compose.
// Note that it might not be running.
return "http://127.0.0.1:8000"
return "http://validator01_evm:8545"
}

return fmt.Sprintf("https://%s.omni.network", s.Network)
Expand All @@ -131,7 +131,7 @@ func (s Static) ConsensusRPC() string {
if s.Network == Devnet {
// First halo in devnet docker-compose.
// Note that it might not be running.
return "http://localhost:5701"
return "http://validator01:26657"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lib/netconf/static.go should remain external RPCs. which for devnet, are local host

with have internalEndpoints() in e2e to get endpoints from within a network (docker or gcp)

}

return fmt.Sprintf("https://consensus.%s.omni.network", s.Network)
Expand Down
18 changes: 18 additions & 0 deletions lib/xchain/connect/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
rpcclient "github.com/cometbft/cometbft/rpc/client"
rpchttp "github.com/cometbft/cometbft/rpc/client/http"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)

Expand All @@ -34,6 +35,7 @@ type Connector struct {
CProvider cchain.Provider
EthClients map[uint64]ethclient.Client
CmtCl rpcclient.Client
network netconf.Network
}

// Backend returns an ethbackend for the given chainID.
Expand All @@ -51,6 +53,21 @@ func (c Connector) Backend(chainID uint64) (*ethbackend.Backend, error) {
return ethbackend.NewBackend(chain.Name, chainID, chain.BlockPeriod, cl)
}

// GetPortal returns an OmniPortal binding for the portal at the provided address.
func (c Connector) GetPortal(chainID uint64, backend bind.ContractBackend) (*bindings.OmniPortal, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xconnector isn't used in monitor, it is only used in scripts, lets keep it that way. please remove this.

chain, ok := c.network.Chain(chainID)
if !ok {
return nil, errors.New("failed to get chain from network", "chain", chainID)
}

portal, err := bindings.NewOmniPortal(chain.PortalAddress, backend)
if err != nil {
return nil, errors.Wrap(err, "failed to get portal")
}

return portal, nil
}

type option func(xchain.RPCEndpoints) error

// WithPublicRPCs returns an option using well known public free RPCs for all xchains.
Expand Down Expand Up @@ -188,6 +205,7 @@ func New(ctx context.Context, netID netconf.ID, opts ...option) (Connector, erro
CProvider: cprov,
EthClients: ethClients,
CmtCl: cometCl,
network: network,
}, nil
}

Expand Down
6 changes: 3 additions & 3 deletions monitor/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func Run(ctx context.Context, cfg Config) error {
return errors.Wrap(err, "monitor contracts")
}

if err := startLoadGen(ctx, cfg, network, ethClients); err != nil {
if err := startLoadGen(ctx, cfg, network, ethClients, cfg.RPCEndpoints); err != nil {
return errors.Wrap(err, "start load generator")
}

Expand Down Expand Up @@ -165,8 +165,8 @@ func serveMonitoring(address string) <-chan error {
return errChan
}

func startLoadGen(ctx context.Context, cfg Config, network netconf.Network, ethClients map[uint64]ethclient.Client) error {
if err := loadgen.Start(ctx, network, ethClients, cfg.LoadGen); err != nil {
func startLoadGen(ctx context.Context, cfg Config, network netconf.Network, ethClients map[uint64]ethclient.Client, endpoints xchain.RPCEndpoints) error {
if err := loadgen.Start(ctx, network, ethClients, cfg.LoadGen, cfg.XCaller, endpoints); err != nil {
return errors.Wrap(err, "start load generator")
}

Expand Down
1 change: 1 addition & 0 deletions monitor/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Config struct {
HaloURL string
LoadGen loadgen.Config
XFeeMngr xfeemngr.Config
XCaller loadgen.XCallerConfig
DBDir string
}

Expand Down
13 changes: 13 additions & 0 deletions monitor/app/config.toml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,16 @@ color = "{{ .Log.Color }}"
[loadgen]
# Validator keys glob defines the validator keys to use for self-delegation.
validator-keys-glob = "{{ .LoadGen.ValidatorKeysGlob }}"

[xcaller]
enabled = "{{ .XCaller.Enabled }}"

# Chain ID pairs, where each pair specifies a 'from' and 'to' chain for sending xCalls.
[xcaller.chainid-pairs]
{{- if not .XCaller.ChainIDs }}
# arbitrum_one = "optimism"
# base = "arbitrum_one"
{{ end -}}
{{- range $key, $value := .XCaller.ChainIDs }}
{{ $key }} = "{{ $value }}"
{{ end }}
9 changes: 9 additions & 0 deletions monitor/app/testdata/default_monitor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,12 @@ color = "auto"
[loadgen]
# Validator keys glob defines the validator keys to use for self-delegation.
validator-keys-glob = "path/*/1"

[xcaller]
enabled = "false"

# Chain ID pairs, where each pair specifies a 'from' and 'to' chain for sending xCalls.
[xcaller.chainid-pairs]
# arbitrum_one = "optimism"
# base = "arbitrum_one"

Comment on lines 72 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xcaller is part of loadgen, so let's make it part of loadgen config

2 changes: 1 addition & 1 deletion monitor/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func New() *cobra.Command {
cfg := monitor.DefaultConfig()
bindRunFlags(cmd.Flags(), &cfg)
bindLoadGenFlags(cmd.Flags(), &cfg.LoadGen)
bindXCallerFlags(cmd.Flags(), &cfg.XCaller)
bindXFeeMngrFlags(cmd.Flags(), &cfg.XFeeMngr)

logCfg := log.DefaultConfig()
log.BindFlags(cmd.Flags(), &logCfg)

Expand Down
5 changes: 5 additions & 0 deletions monitor/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ func bindXFeeMngrFlags(flags *pflag.FlagSet, cfg *xfeemngr.Config) {
flags.StringToStringVar((*map[string]string)(&cfg.RPCEndpoints), "xfeemngr-rpc-endpoints", cfg.RPCEndpoints, "Cross-chain EVM RPC endpoints. e.g. \"ethereum=http://geth:8545,optimism=https://optimism.io\"")
flags.StringVar(&cfg.CoinGeckoAPIKey, "xfeemngr-coingecko-apikey", cfg.CoinGeckoAPIKey, "The CoinGecko API key to use for fetching token prices")
}

func bindXCallerFlags(flags *pflag.FlagSet, cfg *loadgen.XCallerConfig) {
flags.StringToStringVar(&cfg.ChainIDs, "xcaller-chainid-pairs", cfg.ChainIDs, "Chan ID pairs to make xcalls from -> to. e.g. \"arbitrum_one=optimism,optimism=base,base=arbitrum_one\"")
flags.BoolVar(&cfg.Enabled, "xcaller-enabled", false, "Enable or disable xcall loadgen from the monitor app")
}
29 changes: 27 additions & 2 deletions monitor/loadgen/loadgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@ import (
"time"

"github.com/omni-network/omni/contracts/bindings"
"github.com/omni-network/omni/e2e/app/eoa"
"github.com/omni-network/omni/halo/genutil/evm/predeploys"
"github.com/omni-network/omni/lib/errors"
"github.com/omni-network/omni/lib/ethclient"
"github.com/omni-network/omni/lib/ethclient/ethbackend"
"github.com/omni-network/omni/lib/netconf"
"github.com/omni-network/omni/lib/xchain"
"github.com/omni-network/omni/lib/xchain/connect"

"github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
)

// Config is the configuration for the load generator.
type Config struct {
// ValidatorKeysGlob defines the paths to the validator keys used for self-delegation.
ValidatorKeysGlob string
}

// Start starts the validator self delegation load generator.
// It does:
// - Validator self-delegation on periodic basis.
func Start(ctx context.Context, network netconf.Network, ethClients map[uint64]ethclient.Client, cfg Config) error {
func Start(ctx context.Context, network netconf.Network, ethClients map[uint64]ethclient.Client, cfg Config, xCallerCfg XCallerConfig, endpoints xchain.RPCEndpoints) error {
// Only generate load in ephemeral networks, devnet and staging.
if !network.ID.IsEphemeral() {
return nil
Expand Down Expand Up @@ -79,5 +81,28 @@ func Start(ctx context.Context, network netconf.Network, ethClients map[uint64]e
go selfDelegateForever(ctx, contract, backend, val, period)
}

if xCallerCfg.Enabled {
if len(xCallerCfg.ChainIDs) == 0 {
return errors.New("xcaller enabled but no chain id pairs specified")
}

xCallerPK, err := eoa.PrivateKey(ctx, network.ID, eoa.RoleXCaller)
if err != nil {
return errors.Wrap(err, "failed to get RoleXCaller priv key exiting xcall loadgen")
}

backends, err := ethbackend.BackendsFromNetwork(network, endpoints, xCallerPK)
if err != nil {
return err
}

connector, err := connect.New(ctx, network.ID)
if err != nil {
return err
}
xCallerAddr := eoa.MustAddress(network.ID, eoa.RoleXCaller)
go xCallForever(ctx, xCallerAddr, period, xCallerCfg.ChainIDs, backends, connector)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Start function has a lot of stuff in it now. First half about loadgening validator updates. Second part about loadgening xcalls. Maybe put both in their own function

return nil
}
4 changes: 2 additions & 2 deletions monitor/loadgen/stake.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import (
"github.com/ethereum/go-ethereum/params"
)

const selfDelegateJitter = 0.2 // 20% jitter
const loadgenJitter = 0.2 // 20% jitter

func selfDelegateForever(ctx context.Context, contract *bindings.Staking, backend *ethbackend.Backend, validator common.Address, period time.Duration) {
log.Info(ctx, "Starting periodic self-delegation", "validator", validator.Hex(), "period", period)

nextPeriod := func() time.Duration {
jitter := time.Duration(float64(period) * rand.Float64() * selfDelegateJitter) //nolint:gosec // Weak random ok for load tests.
jitter := time.Duration(float64(period) * rand.Float64() * loadgenJitter) //nolint:gosec // Weak random ok for load tests.
return period + jitter
}

Expand Down
109 changes: 109 additions & 0 deletions monitor/loadgen/xcall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package loadgen

import (
"context"
"fmt"
"math/rand/v2"
"time"

"github.com/omni-network/omni/lib/errors"
"github.com/omni-network/omni/lib/ethclient/ethbackend"
"github.com/omni-network/omni/lib/evmchain"
"github.com/omni-network/omni/lib/log"
"github.com/omni-network/omni/lib/xchain"
"github.com/omni-network/omni/lib/xchain/connect"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)

const (
dead = "0x000000000000000000000000000000000000dead"
)

type XCallerConfig struct {
Enabled bool
ChainIDs map[string]string
}

func xCallForever(ctx context.Context, xCallerAddr common.Address, period time.Duration, chainIDs map[string]string, backends ethbackend.Backends, connector connect.Connector) {
log.Info(ctx, "Starting periodic xcalls", "period", period)

nextPeriod := func() time.Duration {
jitter := time.Duration(float64(period) * rand.Float64() * loadgenJitter) //nolint:gosec // Weak random ok for load tests.
return period + jitter
}

timer := time.NewTimer(nextPeriod())
defer timer.Stop()

// tick immediately
if err := xCallOnce(ctx, xCallerAddr, chainIDs, backends, connector); err != nil {
log.Warn(ctx, "Failed to xcall (will retry)", err)
}

for {
select {
case <-ctx.Done():
return
case <-timer.C:
if err := xCallOnce(ctx, xCallerAddr, chainIDs, backends, connector); err != nil {
log.Warn(ctx, "Failed to xcall (will retry)", err)
}
timer.Reset(nextPeriod())
}
}
}

func xCallOnce(ctx context.Context, xCallerAddr common.Address, chainIDs map[string]string, backends ethbackend.Backends, connector connect.Connector) error {
for from, to := range chainIDs {
fromChain, ok := evmchain.MetadataByName(from)
if !ok {
return errors.New("unknown source chain name", from)
}

dstChain, ok := evmchain.MetadataByName(to)
if !ok {
return errors.New("unknown destination chain name", from)
}

backend, err := backends.Backend(fromChain.ChainID)
if err != nil {
return err
}

fromPortal, err := connector.GetPortal(fromChain.ChainID, backend)
if err != nil {
return err
}

var data []byte
to := common.HexToAddress(dead)
gasLimit := uint64(100_000)
fee, err := fromPortal.FeeFor(&bind.CallOpts{}, dstChain.ChainID, data, gasLimit)
if err != nil {
return errors.Wrap(err, "feeFor",
"src_chain", fromChain.ChainID,
"dst_chain_id", dstChain.ChainID,
)
}

txOpts, err := backend.BindOpts(ctx, xCallerAddr)
if err != nil {
return errors.Wrap(err, "bindOpts")
}

txOpts.Value = fee
tx, err := fromPortal.Xcall(txOpts, dstChain.ChainID, uint8(xchain.ConfFinalized), to, data, gasLimit)
if err != nil {
return errors.Wrap(err, "xcall",
"src_chain", fromChain.ChainID,
"dst_chain_id", dstChain.ChainID,
)
}

log.Debug(ctx, fmt.Sprintf("xcall made %d -> %d %s", fromChain.ChainID, dstChain.ChainID, tx.To()))
}

return nil
}
Loading