diff --git a/cli/cmd/register_test.go b/cli/cmd/register_test.go index 0ed709f1e..e04b5daa9 100644 --- a/cli/cmd/register_test.go +++ b/cli/cmd/register_test.go @@ -72,7 +72,7 @@ func setup(t *testing.T) (context.Context, *ethbackend.Backend, Contracts, EOAS) ctx := context.Background() - ethCl, _, stop, err := anvil.Start(ctx, tutil.TempDir(t), chainID) + ethCl, stop, err := anvil.Start(ctx, tutil.TempDir(t), chainID) require.NoError(t, err) t.Cleanup(stop) diff --git a/e2e/app/anvil.go b/e2e/app/anvil.go index fee5578af..0a8590950 100644 --- a/e2e/app/anvil.go +++ b/e2e/app/anvil.go @@ -17,11 +17,16 @@ func fundAnvil(ctx context.Context, def Definition) error { return nil } - toFund := eoa.MustAddresses(netconf.Devnet, eoa.AllRoles()...) + toFund := eoa.MustAddresses(netconf.Devnet, eoa.AllRolesWithSolver()...) amt := math.NewInt(1000000).MulRaw(1e18).BigInt() // 1M ETH for _, chain := range def.Testnet.AnvilChains { - if err := anvil.FundAccounts(ctx, chain.ExternalRPC, amt, toFund...); err != nil { + backend, err := def.Backends().Backend(chain.Chain.ChainID) + if err != nil { + return errors.Wrap(err, "get backend") + } + + if err := anvil.FundAccounts(ctx, backend.Client, amt, toFund...); err != nil { return errors.Wrap(err, "fund anvil account") } } diff --git a/e2e/app/eoa/eoa.go b/e2e/app/eoa/eoa.go index d1e47b6c1..d4ea126db 100644 --- a/e2e/app/eoa/eoa.go +++ b/e2e/app/eoa/eoa.go @@ -43,7 +43,7 @@ const ( RoleTester Role = "tester" // RoleXCaller is used for testing xcalls on a network. RoleXCaller Role = "xcaller" - // RolveSolver is the allowed solver for solve inbox / outboxes. + // RoleSolver is solver EOA on all networks that interacts with solve inbox / outboxes. RoleSolver Role = "solver" ) @@ -63,6 +63,11 @@ func AllRoles() []Role { } } +// TODO(corver): Remove this once solver added above. +func AllRolesWithSolver() []Role { + return append(AllRoles(), RoleSolver) +} + func (r Role) Verify() error { for _, role := range AllRoles() { if r == role { diff --git a/e2e/app/run.go b/e2e/app/run.go index d979ce05f..9c73d71e7 100644 --- a/e2e/app/run.go +++ b/e2e/app/run.go @@ -82,6 +82,7 @@ func Deploy(ctx context.Context, def Definition, cfg DeployConfig) (*pingpong.XD if err := waitForEVMs(ctx, def.Testnet.EVMChains(), def.Backends()); err != nil { return nil, err } + logRPCs(ctx, def) contracts.UseStagingOmniRPC(def.Testnet.BroadcastOmniEVM().ExternalRPC) @@ -98,7 +99,12 @@ func Deploy(ctx context.Context, def Definition, cfg DeployConfig) (*pingpong.XD return nil, errors.Wrap(err, "deploy portals") } - logRPCs(ctx, def) + if def.Manifest.DeploySolve { + // Deploy solver before initPortalRegistry, so solver detects boxes after netconf.Await + if err := solve.DeployContracts(ctx, NetworkFromDef(def), def.Backends()); err != nil { + return nil, errors.Wrap(err, "deploy solve") + } + } // Deploy other contracts (and other on-chain setup) var eg2 errgroup.Group @@ -108,9 +114,6 @@ func Deploy(ctx context.Context, def Definition, cfg DeployConfig) (*pingpong.XD eg2.Go(func() error { return DeployBridge(ctx, def) }) eg2.Go(func() error { return maybeSubmitNetworkUpgrade(ctx, def) }) eg2.Go(func() error { return FundValidatorsForTesting(ctx, def) }) - if def.Manifest.DeploySolve { - eg2.Go(func() error { return solve.DeployContracts(ctx, NetworkFromDef(def), def.Backends()) }) - } if err := eg2.Wait(); err != nil { return nil, errors.Wrap(err, "deploy other contracts") } diff --git a/e2e/manifests/devnet1.toml b/e2e/manifests/devnet1.toml index 96037542e..48a2c6d5a 100644 --- a/e2e/manifests/devnet1.toml +++ b/e2e/manifests/devnet1.toml @@ -4,6 +4,7 @@ anvil_chains = ["mock_l1", "mock_l2"] multi_omni_evms = true prometheus = true +deploy_solve = true [node.validator01] [node.validator02] diff --git a/e2e/solve/deploy.go b/e2e/solve/deploy.go index e075a3ce5..12944011c 100644 --- a/e2e/solve/deploy.go +++ b/e2e/solve/deploy.go @@ -10,6 +10,8 @@ import ( "github.com/omni-network/omni/lib/ethclient/ethbackend" "github.com/omni-network/omni/lib/log" "github.com/omni-network/omni/lib/netconf" + + "golang.org/x/sync/errgroup" ) // DeployContracts deploys solve inbox / outbox contracts, and devnet app (if devnet). @@ -21,12 +23,17 @@ func DeployContracts(ctx context.Context, network netconf.Network, backends ethb log.Info(ctx, "Deploying solve contracts") - if err := deployBoxes(ctx, network, backends); err != nil { - return errors.Wrap(err, "deploy boxes") - } + var eg errgroup.Group + + eg.Go(func() error { + return deployBoxes(ctx, network, backends) + }) + eg.Go(func() error { + return devapp.Deploy(ctx, network, backends) + }) - if err := devapp.Deploy(ctx, network, backends); err != nil { - return errors.Wrap(err, "deploy devapp") + if err := eg.Wait(); err != nil { + return errors.Wrap(err, "deploy solver contracts") } return nil @@ -34,25 +41,34 @@ func DeployContracts(ctx context.Context, network netconf.Network, backends ethb // DeployContracts deploys solve inbox / outbox contracts. func deployBoxes(ctx context.Context, network netconf.Network, backends ethbackend.Backends) error { + var eg errgroup.Group for _, chain := range network.EVMChains() { - backend, err := backends.Backend(chain.ID) - if err != nil { - return errors.Wrap(err, "get backend", "chain", chain.Name) - } + eg.Go(func() error { + backend, err := backends.Backend(chain.ID) + if err != nil { + return errors.Wrap(err, "get backend", "chain", chain.Name) + } - addr, _, err := solveinbox.DeployIfNeeded(ctx, network.ID, backend) - if err != nil { - return errors.Wrap(err, "deploy solve inbox") - } + addr, _, err := solveinbox.DeployIfNeeded(ctx, network.ID, backend) + if err != nil { + return errors.Wrap(err, "deploy solve inbox") + } - log.Info(ctx, "SolveInbox deployed", "addr", addr.Hex(), "chain", chain.Name) + log.Debug(ctx, "SolveInbox deployed", "addr", addr.Hex(), "chain", chain.Name) - addr, _, err = solveoutbox.DeployIfNeeded(ctx, network.ID, backend) - if err != nil { - return errors.Wrap(err, "deploy solve outbox") - } + addr, _, err = solveoutbox.DeployIfNeeded(ctx, network.ID, backend) + if err != nil { + return errors.Wrap(err, "deploy solve outbox") + } + + log.Debug(ctx, "SolveOutbox deployed", "addr", addr.Hex(), "chain", chain.Name) + + return nil + }) + } - log.Info(ctx, "SolveOutbox deployed", "addr", addr.Hex(), "chain", chain.Name) + if err := eg.Wait(); err != nil { + return errors.Wrap(err, "deploy solver boxes") } return nil diff --git a/e2e/solve/devapp/deposit.go b/e2e/solve/devapp/deposit.go index 76ea39ccc..2c1bb0450 100644 --- a/e2e/solve/devapp/deposit.go +++ b/e2e/solve/devapp/deposit.go @@ -10,7 +10,6 @@ import ( "github.com/omni-network/omni/lib/errors" "github.com/omni-network/omni/lib/ethclient/ethbackend" "github.com/omni-network/omni/lib/netconf" - "github.com/omni-network/omni/lib/xchain" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -24,7 +23,7 @@ type DepositReq struct { Deposit DepositArgs // deposit args } -func RequestDeposits(ctx context.Context, endpoints xchain.RPCEndpoints, backends ethbackend.Backends) ([]DepositReq, error) { +func RequestDeposits(ctx context.Context, backends ethbackend.Backends) ([]DepositReq, error) { app := GetApp() backend, err := backends.Backend(app.L2.ChainID) @@ -32,20 +31,15 @@ func RequestDeposits(ctx context.Context, endpoints xchain.RPCEndpoints, backend return nil, err } - rpc, err := endpoints.ByNameOrID(app.L2.Name, app.L2.ChainID) - if err != nil { - return nil, err - } + const numDeposits = 3 - const numDeposits = 10 - - depositors, err := makeDepositors(numDeposits, backend) + depositors, err := addRandomDepositors(numDeposits, backend) if err != nil { return nil, errors.Wrap(err, "make depositors") } // fund for gas - if err := anvil.FundAccounts(ctx, rpc, big.NewInt(1e18), depositors...); err != nil { + if err := anvil.FundAccounts(ctx, backend, big.NewInt(1e18), depositors...); err != nil { return nil, errors.Wrap(err, "fund accounts") } @@ -62,43 +56,36 @@ func RequestDeposits(ctx context.Context, endpoints xchain.RPCEndpoints, backend return reqs, nil } -func CheckDeposits(ctx context.Context, backends ethbackend.Backends, reqs []DepositReq) error { +func IsDeposited(ctx context.Context, backends ethbackend.Backends, req DepositReq) (bool, error) { app := GetApp() backend, err := backends.Backend(app.L1.ChainID) if err != nil { - return errors.Wrap(err, "backend") + return false, errors.Wrap(err, "backend") } vault, err := bindings.NewMockVault(app.L1Vault, backend) if err != nil { - return errors.Wrap(err, "new mock vault") + return false, errors.Wrap(err, "new mock vault") } callOpts := &bind.CallOpts{Context: ctx} - for _, req := range reqs { - balance, err := vault.Balances(callOpts, req.Deposit.OnBehalfOf) - if err != nil { - return errors.Wrap(err, "get balance") - } - - // assumes balance(onBehalfOf) was zero before deposit request - // assumes one deposit per test case onBehalfOf addr - if balance.Cmp(req.Deposit.Amount) != 0 { - return errors.New("missing deposit", - "requester", req.Deposit.OnBehalfOf, - "expected", req.Deposit.Amount, - "actual", balance) - } + balance, err := vault.Balances(callOpts, req.Deposit.OnBehalfOf) + if err != nil { + return false, errors.Wrap(err, "get balance") } - return nil + // assumes balance(onBehalfOf) was zero before deposit request + // assumes one deposit per test case onBehalfOf addr + return balance.Cmp(req.Deposit.Amount) != 0, nil } -func makeDepositors(n int, backend *ethbackend.Backend) ([]common.Address, error) { - depositors := make([]common.Address, n) - for i := 0; i < n; i++ { +// addRandomDepositors adds n random depositors privkeys to the backend. +// It returns the addresses of the added depositors. +func addRandomDepositors(n int, backend *ethbackend.Backend) ([]common.Address, error) { + var depositors []common.Address + for range n { pk, err := crypto.GenerateKey() if err != nil { return nil, errors.Wrap(err, "generate key") @@ -109,17 +96,15 @@ func makeDepositors(n int, backend *ethbackend.Backend) ([]common.Address, error return nil, errors.Wrap(err, "add account") } - depositors[i] = depositor + depositors = append(depositors, depositor) } return depositors, nil } func requestDeposits(ctx context.Context, backend *ethbackend.Backend, inbox common.Address, depositors []common.Address) ([]DepositReq, error) { - reqs := make([]DepositReq, 0, len(depositors)) - for i := 0; i < len(depositors); i++ { - depositor := depositors[i] - + var reqs []DepositReq + for _, depositor := range depositors { deposit := DepositArgs{ OnBehalfOf: depositor, Amount: big.NewInt(1e18), @@ -162,6 +147,7 @@ func requestAtInbox(ctx context.Context, backend *ethbackend.Backend, addr commo bindings.SolveCall{ DestChainId: app.L1.ChainID, Target: app.L1Vault, + Value: new(big.Int), // 0 native Data: data, }, []bindings.SolveTokenDeposit{{ @@ -238,9 +224,9 @@ func parseReqID(inbox bindings.SolveInboxFilterer, logs []*types.Log) ([32]byte, } func packDeposit(args DepositArgs) ([]byte, error) { - data, err := vaultDeposit.Inputs.Pack(args) + data, err := vaultDeposit.Inputs.Pack(args.OnBehalfOf, args.Amount) if err != nil { - return nil, errors.Wrap(err, "unpack data") + return nil, errors.Wrap(err, "pack data") } return data, nil diff --git a/e2e/solve/devapp/target.go b/e2e/solve/devapp/target.go index a07b90719..8ef8f830a 100644 --- a/e2e/solve/devapp/target.go +++ b/e2e/solve/devapp/target.go @@ -22,29 +22,11 @@ func (App) ChainID() uint64 { return evmchain.IDMockL1 } -func (t App) Address() common.Address { - return t.L1Vault +func (a App) Address() common.Address { + return a.L1Vault } -func (t App) IsAllowedCall(call bindings.SolveCall) bool { - if call.DestChainId != t.ChainID() { - return false - } - - if call.Target != t.Address() { - return false - } - - _, err := unpackDeposit(call.Data) - - return err == nil -} - -func (t App) TokenPrereqs(call bindings.SolveCall) ([]bindings.SolveTokenPrereq, error) { - if !t.IsAllowedCall(call) { - return nil, errors.New("call not allowed") - } - +func (a App) TokenPrereqs(call bindings.SolveCall) ([]bindings.SolveTokenPrereq, error) { args, err := unpackDeposit(call.Data) if err != nil { return nil, errors.Wrap(err, "unpack deposit") @@ -52,31 +34,31 @@ func (t App) TokenPrereqs(call bindings.SolveCall) ([]bindings.SolveTokenPrereq, return []bindings.SolveTokenPrereq{ { - Token: t.L1Token, - Amount: args.Amount, + Token: a.L1Token, + Spender: a.L1Vault, + Amount: args.Amount, }, }, nil } -func (t App) Verify(srcChainID uint64, call bindings.SolveCall, deposits []bindings.SolveDeposit) error { +func (a App) Verify(srcChainID uint64, call bindings.SolveCall, deposits []bindings.SolveDeposit) error { // we only accept deposits from mock L2 if srcChainID != evmchain.IDMockL2 { return errors.New("source chain not supported", "src", srcChainID) } - if !t.IsAllowedCall(call) { - return errors.New("call not allowed") - } - args, err := unpackDeposit(call.Data) if err != nil { return errors.Wrap(err, "invalid deposit") } - var l2token *bindings.SolveDeposit + if _, err := a.TokenPrereqs(call); err != nil { + return errors.Wrap(err, "token prereqs") + } + var l2token *bindings.SolveDeposit for _, deposit := range deposits { - if deposit.Token == t.L2Token { + if deposit.Token == a.L2Token { l2token = &deposit } } diff --git a/e2e/test/e2e_test.go b/e2e/test/e2e_test.go index e34241420..2839bede9 100644 --- a/e2e/test/e2e_test.go +++ b/e2e/test/e2e_test.go @@ -55,6 +55,7 @@ type testFunc struct { TestPortal func(*testing.T, netconf.Network, Portal, []Portal) TestOmniEVM func(*testing.T, ethclient.Client) TestNetwork func(*testing.T, netconf.Network, xchain.RPCEndpoints) + skipFunc func(types.Manifest) bool } func testNode(t *testing.T, fn func(*testing.T, netconf.Network, *e2e.Node, []Portal)) { @@ -77,6 +78,15 @@ func testNetwork(t *testing.T, fn func(*testing.T, netconf.Network, xchain.RPCEn test(t, testFunc{TestNetwork: fn}) } +func maybeTestNetwork( + t *testing.T, + skipFunc func(types.Manifest) bool, + fn func(*testing.T, netconf.Network, xchain.RPCEndpoints), +) { + t.Helper() + test(t, testFunc{TestNetwork: fn, skipFunc: skipFunc}) +} + // test runs tests for testnet nodes. The callback functions are respectively given a // single node to test, and a single portal to test, running as a subtest in parallel with other subtests. // @@ -90,6 +100,11 @@ func test(t *testing.T, testFunc testFunc) { testnet, network, _, endpoints := loadEnv(t) nodes := testnet.Nodes + if testFunc.skipFunc != nil && testFunc.skipFunc(testnet.Manifest) { + t.Skip("Skipping test") + return + } + if name := os.Getenv(app.EnvE2ENode); name != "" { node := testnet.LookupNode(name) require.NotNil(t, node, "node %q not found in testnet %q", name, testnet.Name) diff --git a/e2e/test/solve_test.go b/e2e/test/solve_test.go new file mode 100644 index 000000000..81b20df00 --- /dev/null +++ b/e2e/test/solve_test.go @@ -0,0 +1,68 @@ +package e2e_test + +import ( + "context" + "testing" + "time" + + "github.com/omni-network/omni/e2e/solve/devapp" + "github.com/omni-network/omni/e2e/types" + "github.com/omni-network/omni/lib/ethclient/ethbackend" + "github.com/omni-network/omni/lib/log" + "github.com/omni-network/omni/lib/netconf" + "github.com/omni-network/omni/lib/xchain" + + "github.com/stretchr/testify/require" +) + +// TestSolver submits deposits to the solve inbox and waits for them to be processed. +func TestSolver(t *testing.T) { + t.Parallel() + skipFunc := func(manifest types.Manifest) bool { + return !manifest.DeploySolve + } + maybeTestNetwork(t, skipFunc, func(t *testing.T, network netconf.Network, endpoints xchain.RPCEndpoints) { + t.Helper() + ctx := context.Background() + + backends, err := ethbackend.BackendsFromNetwork(network, endpoints) + require.NoError(t, err) + + deposits, err := devapp.RequestDeposits(ctx, backends) + require.NoError(t, err) + + timeout, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + toCheck := toSet(deposits) + for { + if timeout.Err() != nil { + require.Fail(t, "timeout waiting for deposits") + } + + for deposit := range toCheck { + ok, err := devapp.IsDeposited(ctx, backends, deposit) + require.NoError(t, err) + if ok { + log.Info(ctx, "Deposit complete", "remaining", len(toCheck)-1) + delete(toCheck, deposit) + } + } + + if len(toCheck) == 0 { + return + } + + time.Sleep(time.Second) + } + }) +} + +func toSet[T comparable](slice []T) map[T]bool { + set := make(map[T]bool) + for _, v := range slice { + set[v] = true + } + + return set +} diff --git a/lib/anvil/anvil.go b/lib/anvil/anvil.go index 215f650c7..9a2d39e93 100644 --- a/lib/anvil/anvil.go +++ b/lib/anvil/anvil.go @@ -17,6 +17,8 @@ import ( "github.com/omni-network/omni/lib/log" "github.com/omni-network/omni/scripts" + "github.com/ethereum/go-ethereum/params" + "cosmossdk.io/math" _ "embed" @@ -26,39 +28,39 @@ import ( // The dir parameter is the location of the docker compose. // If useLogProxy is true, all requests are routed via a reserve proxy that logs all requests, which will be printed // at stop. -func Start(ctx context.Context, dir string, chainID uint64) (ethclient.Client, string, func(), error) { +func Start(ctx context.Context, dir string, chainID uint64) (ethclient.Client, func(), error) { ctx, cancel := context.WithTimeout(ctx, time.Minute) // Allow 1 minute for edge case of pulling images. defer cancel() if !composeDown(ctx, dir) { - return nil, "", nil, errors.New("failure to clean up previous anvil instance") + return nil, nil, errors.New("failure to clean up previous anvil instance") } // Ensure ports are available port, err := getAvailablePort() if err != nil { - return nil, "", nil, errors.Wrap(err, "get available port") + return nil, nil, errors.Wrap(err, "get available port") } if err := writeComposeFile(dir, chainID, port, scripts.FoundryVersion()); err != nil { - return nil, "", nil, errors.Wrap(err, "write compose file") + return nil, nil, errors.Wrap(err, "write compose file") } if err := writeAnvilState(dir); err != nil { - return nil, "", nil, errors.Wrap(err, "write anvil state") + return nil, nil, errors.Wrap(err, "write anvil state") } log.Info(ctx, "Starting anvil") out, err := execCmd(ctx, dir, "docker", "compose", "up", "-d", "--remove-orphans") if err != nil { - return nil, "", nil, errors.Wrap(err, "docker compose up: "+out) + return nil, nil, errors.Wrap(err, "docker compose up: "+out) } endpoint := "http://localhost:" + port ethCl, err := ethclient.Dial("anvil", endpoint) if err != nil { - return nil, "", nil, errors.Wrap(err, "new eth client") + return nil, nil, errors.Wrap(err, "new eth client") } stop := func() { //nolint:contextcheck // Fresh context required for stopping. @@ -73,12 +75,12 @@ func Start(ctx context.Context, dir string, chainID uint64) (ethclient.Client, s for i := 0; i < retry; i++ { if i == retry-1 { stop() - return nil, "", nil, errors.New("wait for RPC timed out") + return nil, nil, errors.New("wait for RPC timed out") } select { case <-ctx.Done(): - return nil, "", nil, errors.Wrap(ctx.Err(), "timeout") + return nil, nil, errors.Wrap(ctx.Err(), "timeout") case <-time.After(time.Millisecond * 500): } @@ -91,15 +93,15 @@ func Start(ctx context.Context, dir string, chainID uint64) (ethclient.Client, s } // always fund dev accounts - eth1m := math.NewInt(1000000).MulRaw(1e18).BigInt() // 1M ETH - if err := FundAccounts(ctx, endpoint, eth1m, eoa.DevAccounts()...); err != nil { + eth1m := math.NewInt(1_000_000).MulRaw(params.Ether).BigInt() // 1M ETH + if err := FundAccounts(ctx, ethCl, eth1m, eoa.DevAccounts()...); err != nil { stop() - return nil, "", nil, errors.Wrap(err, "fund accounts") + return nil, nil, errors.Wrap(err, "fund accounts") } log.Info(ctx, "Anvil: RPC is available", "addr", endpoint) - return ethCl, endpoint, stop, nil + return ethCl, stop, nil } // composeDown runs docker-compose down in the provided directory. diff --git a/lib/anvil/utils.go b/lib/anvil/utils.go index a6863fc4e..44cf5e74f 100644 --- a/lib/anvil/utils.go +++ b/lib/anvil/utils.go @@ -5,23 +5,17 @@ import ( "math/big" "github.com/omni-network/omni/lib/errors" + "github.com/omni-network/omni/lib/ethclient" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/ethclient" ) // FundAccounts funds the anvil account via the anvil_setBalance RPC method. -func FundAccounts(ctx context.Context, rpc string, amount *big.Int, accounts ...common.Address) error { - client, err := ethclient.Dial(rpc) - if err != nil { - return err - } - defer client.Close() - +func FundAccounts(ctx context.Context, ethCl ethclient.Client, amount *big.Int, accounts ...common.Address) error { for _, account := range accounts { result := make(map[string]any) - err = client.Client().CallContext(ctx, &result, "anvil_setBalance", account, hexutil.EncodeBig(amount)) + err := ethCl.CallContext(ctx, &result, "anvil_setBalance", account, hexutil.EncodeBig(amount)) if err != nil { return errors.Wrap(err, "set balance", "account", account) } diff --git a/lib/anvil/utils_test.go b/lib/anvil/utils_test.go index 8e4a968cd..4447dd71d 100644 --- a/lib/anvil/utils_test.go +++ b/lib/anvil/utils_test.go @@ -2,7 +2,6 @@ package anvil_test import ( "context" - "math/big" "testing" "time" @@ -10,7 +9,9 @@ import ( "github.com/omni-network/omni/lib/tutil" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "cosmossdk.io/math" "github.com/stretchr/testify/require" ) @@ -25,7 +26,7 @@ func TestFundAccounts(t *testing.T) { ctx := context.Background() - ethCl, endpoint, stop, err := anvil.Start(ctx, tutil.TempDir(t), chainID) + ethCl, stop, err := anvil.Start(ctx, tutil.TempDir(t), chainID) require.NoError(t, err) defer stop() @@ -35,8 +36,8 @@ func TestFundAccounts(t *testing.T) { common.HexToAddress("0x333"), } - amt := big.NewInt(1).Mul(big.NewInt(1e18), big.NewInt(100)) - err = anvil.FundAccounts(ctx, endpoint, amt, accounts...) + amt := math.NewInt(100).MulRaw(params.Ether).BigInt() // 100 ETH + err = anvil.FundAccounts(ctx, ethCl, amt, accounts...) require.NoError(t, err) for _, account := range accounts { diff --git a/lib/contracts/portal/deploy_test.go b/lib/contracts/portal/deploy_test.go index 098d82524..071c76a05 100644 --- a/lib/contracts/portal/deploy_test.go +++ b/lib/contracts/portal/deploy_test.go @@ -31,7 +31,7 @@ func TestDeployDevnet(t *testing.T) { network := netconf.Devnet ctx := context.Background() - client, _, stop, err := anvil.Start(ctx, tutil.TempDir(t), chainID) + client, stop, err := anvil.Start(ctx, tutil.TempDir(t), chainID) require.NoError(t, err) t.Cleanup(stop) diff --git a/lib/ethclient/ethclient.go b/lib/ethclient/ethclient.go index 4e837b7d4..f7028182e 100644 --- a/lib/ethclient/ethclient.go +++ b/lib/ethclient/ethclient.go @@ -205,3 +205,17 @@ func (w Wrapper) TxReceipt(ctx context.Context, txHash common.Hash) (*Receipt, e return r, err } + +//nolint:revive // interface{} required by upstream. +func (w Wrapper) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + const endpoint = "raw_call" + defer latency(w.chain, endpoint)() + + err := w.cl.Client().CallContext(ctx, result, method, args...) + if err != nil { + incError(w.chain, endpoint) + err = errors.Wrap(err, "json-rpc", "endpoint", endpoint) + } + + return err +} diff --git a/lib/ethclient/ethclient_gen.go b/lib/ethclient/ethclient_gen.go index 798f193df..19d4a83f5 100644 --- a/lib/ethclient/ethclient_gen.go +++ b/lib/ethclient/ethclient_gen.go @@ -33,6 +33,7 @@ type Client interface { PeerCount(ctx context.Context) (uint64, error) SetHead(ctx context.Context, height uint64) error ProgressIfSyncing(ctx context.Context) (*ethereum.SyncProgress, bool, error) + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error Address() string Close() } diff --git a/lib/ethclient/genwrap/genwrap.go b/lib/ethclient/genwrap/genwrap.go index cde90d09a..42ecc6122 100644 --- a/lib/ethclient/genwrap/genwrap.go +++ b/lib/ethclient/genwrap/genwrap.go @@ -50,6 +50,7 @@ type Client interface { PeerCount(ctx context.Context) (uint64, error) SetHead(ctx context.Context, height uint64) error ProgressIfSyncing(ctx context.Context) (*ethereum.SyncProgress, bool, error) + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error Address() string Close() } diff --git a/lib/ethclient/mock/mock_interfaces.go b/lib/ethclient/mock/mock_interfaces.go index b52bd360f..1d6b2f73a 100644 --- a/lib/ethclient/mock/mock_interfaces.go +++ b/lib/ethclient/mock/mock_interfaces.go @@ -119,6 +119,25 @@ func (mr *MockClientMockRecorder) BlockNumber(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockNumber", reflect.TypeOf((*MockClient)(nil).BlockNumber), ctx) } +// CallContext mocks base method. +func (m *MockClient) CallContext(ctx context.Context, result any, method string, args ...any) error { + m.ctrl.T.Helper() + varargs := []any{ctx, result, method} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CallContext", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// CallContext indicates an expected call of CallContext. +func (mr *MockClientMockRecorder) CallContext(ctx, result, method any, args ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, result, method}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallContext", reflect.TypeOf((*MockClient)(nil).CallContext), varargs...) +} + // CallContract mocks base method. func (m *MockClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { m.ctrl.T.Helper() diff --git a/solver/app/app.go b/solver/app/app.go index 334d938b0..285c769f4 100644 --- a/solver/app/app.go +++ b/solver/app/app.go @@ -226,10 +226,10 @@ func startEventStreams( deps := procDeps{ ParseID: newIDParser(inboxContracts), GetRequest: newRequestGetter(inboxContracts), - ShouldReject: newRequestValidator(), - Accept: newAcceptor(network.ID, inboxContracts, backends, solverAddr), + ShouldReject: newShouldRejector(network.ID), + Accept: newAcceptor(inboxContracts, backends, solverAddr), Reject: newRejector(inboxContracts, backends, solverAddr), - Fulfill: newFulfiller(outboxContracts, backends, solverAddr), + Fulfill: newFulfiller(network.ID, outboxContracts, backends, solverAddr), Claim: newClaimer(inboxContracts, backends, solverAddr), SetCursor: cursorSetter, } diff --git a/solver/app/definition.go b/solver/app/definition.go deleted file mode 100644 index 555b33295..000000000 --- a/solver/app/definition.go +++ /dev/null @@ -1,14 +0,0 @@ -package app - -import ( - "context" - - "github.com/omni-network/omni/contracts/bindings" - "github.com/omni-network/omni/lib/errors" -) - -func newRequestValidator() func(ctx context.Context, chainID uint64, req bindings.SolveRequest) (uint8, bool, error) { - return func(context.Context, uint64, bindings.SolveRequest) (uint8, bool, error) { - return 0, false, errors.New("not implemented") - } -} diff --git a/solver/app/deps.go b/solver/app/deps.go index e2ab0aaf6..ceae0a018 100644 --- a/solver/app/deps.go +++ b/solver/app/deps.go @@ -18,11 +18,11 @@ import ( type procDeps struct { ParseID func(chainID uint64, log types.Log) ([32]byte, error) GetRequest func(ctx context.Context, chainID uint64, id [32]byte) (bindings.SolveRequest, bool, error) - ShouldReject func(ctx context.Context, chainID uint64, req bindings.SolveRequest) (uint8, bool, error) + ShouldReject func(ctx context.Context, chainID uint64, req bindings.SolveRequest) (rejectReason, bool, error) SetCursor func(ctx context.Context, chainID uint64, height uint64) error Accept func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error - Reject func(ctx context.Context, chainID uint64, req bindings.SolveRequest, reason uint8) error + Reject func(ctx context.Context, chainID uint64, req bindings.SolveRequest, reason rejectReason) error Fulfill func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error Claim func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error } @@ -62,6 +62,7 @@ func newClaimer( } func newFulfiller( + network netconf.ID, outboxContracts map[uint64]*bindings.SolveOutbox, backends ethbackend.Backends, solverAddr common.Address, @@ -89,8 +90,15 @@ func newFulfiller( return nil } - // TODO(corver): Convert req.Deposits into TokenPreReqs - var prereqs []bindings.SolveTokenPrereq + target, err := getTarget(network, req.Call) + if err != nil { + return errors.Wrap(err, "get target [BUG]") + } + + prereqs, err := target.TokenPrereqs(req.Call) + if err != nil { + return errors.Wrap(err, "get token prereqs") + } tx, err := outbox.Fulfill(txOpts, req.Id, chainID, req.Call, prereqs) if err != nil { @@ -107,8 +115,8 @@ func newRejector( inboxContracts map[uint64]*bindings.SolveInbox, backends ethbackend.Backends, solverAddr common.Address, -) func(ctx context.Context, chainID uint64, req bindings.SolveRequest, reason uint8) error { - return func(ctx context.Context, chainID uint64, req bindings.SolveRequest, reason uint8) error { +) func(ctx context.Context, chainID uint64, req bindings.SolveRequest, reason rejectReason) error { + return func(ctx context.Context, chainID uint64, req bindings.SolveRequest, reason rejectReason) error { inbox, ok := inboxContracts[chainID] if !ok { return errors.New("unknown chain") @@ -124,7 +132,7 @@ func newRejector( return err } - tx, err := inbox.Reject(txOpts, req.Id, reason) + tx, err := inbox.Reject(txOpts, req.Id, uint8(reason)) if err != nil { return errors.Wrap(err, "reject request") } else if _, err := backend.WaitMined(ctx, tx); err != nil { @@ -136,23 +144,11 @@ func newRejector( } func newAcceptor( - network netconf.ID, inboxContracts map[uint64]*bindings.SolveInbox, backends ethbackend.Backends, solverAddr common.Address, ) func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error { return func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error { - target, err := targetFor(network, req.Call) - if err != nil { - log.Debug(ctx, "No target found for call", "call", req.Call) - return errors.Wrap(err, "get target") - } - - if err := target.Verify(chainID, req.Call, req.Deposits); err != nil { - log.Debug(ctx, "Call rejected by target", "call", req.Call, "err", err) - return errors.Wrap(err, "verify target") - } - inbox, ok := inboxContracts[chainID] if !ok { return errors.New("unknown chain") diff --git a/solver/app/helpers.go b/solver/app/helpers.go index a80be16dc..e27d04dfb 100644 --- a/solver/app/helpers.go +++ b/solver/app/helpers.go @@ -2,6 +2,7 @@ package app import ( "context" + "encoding/hex" "github.com/omni-network/omni/lib/errors" "github.com/omni-network/omni/lib/ethclient/ethbackend" @@ -31,3 +32,9 @@ func detectContractChains(ctx context.Context, network netconf.Network, backends return resp, nil } + +// fmtReqID returns the least-significant 7 hex chars of the provided request ID. +// ReqIDs are monotonically incrementing numbers, not hashes. +func fmtReqID(reqID [32]byte) string { + return hex.EncodeToString(reqID[:])[:7] +} diff --git a/solver/app/processor.go b/solver/app/processor.go index d4412e54a..72a37ab0f 100644 --- a/solver/app/processor.go +++ b/solver/app/processor.go @@ -25,14 +25,16 @@ func newEventProcessor(deps procDeps, chainID uint64) xchain.EventLogsCallback { return errors.Wrap(err, "parse id") } - ctx := log.WithCtx(ctx, log.Hex7("req_id", reqID[:])) + ctx := log.WithCtx(ctx, "status", statusString(event.Status), "req_id", fmtReqID(reqID)) + + log.Debug(ctx, "Processing event") req, _, err := deps.GetRequest(ctx, chainID, reqID) if err != nil { return errors.Wrap(err, "current status") } else if event.Status != req.Status { // TODO(corver): Detect unexpected on-chain status. - log.Info(ctx, "Ignoring mismatching old event", "actual", statusString(req.Status), "event", statusString(event.Status)) + log.Info(ctx, "Ignoring mismatching old event", "actual", statusString(req.Status)) continue } diff --git a/solver/app/processor_internal_test.go b/solver/app/processor_internal_test.go index d678448c3..d167e00e8 100644 --- a/solver/app/processor_internal_test.go +++ b/solver/app/processor_internal_test.go @@ -28,7 +28,7 @@ func TestEventProcessor(t *testing.T) { name string event common.Hash getStatus uint8 - rejectReason uint8 + rejectReason rejectReason expect string }{ { @@ -107,7 +107,7 @@ func TestEventProcessor(t *testing.T) { Status: test.getStatus, }, true, nil }, - ShouldReject: func(ctx context.Context, _ uint64, req bindings.SolveRequest) (uint8, bool, error) { + ShouldReject: func(ctx context.Context, _ uint64, req bindings.SolveRequest) (rejectReason, bool, error) { return test.rejectReason, test.rejectReason != 0, nil }, Accept: func(ctx context.Context, _ uint64, req bindings.SolveRequest) error { @@ -117,7 +117,7 @@ func TestEventProcessor(t *testing.T) { return nil }, - Reject: func(ctx context.Context, _ uint64, req bindings.SolveRequest, reason uint8) error { + Reject: func(ctx context.Context, _ uint64, req bindings.SolveRequest, reason rejectReason) error { actual = reject require.Equal(t, test.getStatus, req.Status) require.Equal(t, test.rejectReason, reason) diff --git a/solver/app/reject.go b/solver/app/reject.go new file mode 100644 index 000000000..4f462adb0 --- /dev/null +++ b/solver/app/reject.go @@ -0,0 +1,41 @@ +package app + +import ( + "context" + + "github.com/omni-network/omni/contracts/bindings" + "github.com/omni-network/omni/lib/log" + "github.com/omni-network/omni/lib/netconf" +) + +//go:generate stringer -type=rejectReason -trimprefix=reject +type rejectReason uint8 + +const ( + rejectNone rejectReason = 0 + rejectDestCallReverts rejectReason = 1 + rejectInsufficientFee rejectReason = 2 + rejectInsufficientInventory rejectReason = 3 + rejectNoTarget rejectReason = 4 +) + +// newShouldRejector returns as ShouldReject function for the given network. +func newShouldRejector(network netconf.ID) func(ctx context.Context, chainID uint64, req bindings.SolveRequest) (rejectReason, bool, error) { + return func(ctx context.Context, srcChainID uint64, req bindings.SolveRequest) (rejectReason, bool, error) { + reject := func(reason rejectReason, err error) (rejectReason, bool, error) { + log.Warn(ctx, "Rejecting request", err, "reason", reason, "call", req.Call) + return reason, true, err + } + + target, err := getTarget(network, req.Call) + if err != nil { + return reject(rejectNoTarget, err) + } + + if err := target.Verify(srcChainID, req.Call, req.Deposits); err != nil { + return reject(rejectInsufficientInventory, err) // TODO(corver): Fix reason + } + + return rejectNone, false, nil + } +} diff --git a/solver/app/rejectreason_string.go b/solver/app/rejectreason_string.go new file mode 100644 index 000000000..41884a8c6 --- /dev/null +++ b/solver/app/rejectreason_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type=rejectReason -trimprefix=reject"; DO NOT EDIT. + +package app + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[rejectNone-0] + _ = x[rejectDestCallReverts-1] + _ = x[rejectInsufficientFee-2] + _ = x[rejectInsufficientInventory-3] + _ = x[rejectNoTarget-4] +} + +const _rejectReason_name = "NoneDestCallRevertsInsufficientFeeInsufficientInventoryNoTarget" + +var _rejectReason_index = [...]uint8{0, 4, 19, 34, 55, 63} + +func (i rejectReason) String() string { + if i >= rejectReason(len(_rejectReason_index)-1) { + return "rejectReason(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _rejectReason_name[_rejectReason_index[i]:_rejectReason_index[i+1]] +} diff --git a/solver/app/targets.go b/solver/app/targets.go index 8b3f6e804..6afa9a6f3 100644 --- a/solver/app/targets.go +++ b/solver/app/targets.go @@ -8,17 +8,30 @@ import ( "github.com/omni-network/omni/solver/types" ) -var ( - targets map[netconf.ID]types.Targets = map[netconf.ID]types.Targets{ - netconf.Devnet: {devapp.GetApp()}, - } -) +var targetsByNetwork = map[netconf.ID][]types.Target{ + netconf.Devnet: {devapp.GetApp()}, +} -func targetFor(network netconf.ID, call bindings.SolveCall) (types.Target, error) { - t, ok := targets[network] +// getTarget returns the target for the given network and call. +func getTarget(network netconf.ID, call bindings.SolveCall) (types.Target, error) { + targets, ok := targetsByNetwork[network] if !ok { return nil, errors.New("no targets for network", "network", network) } - return t.ForCall(call) + var resp *types.Target + for _, target := range targets { + if target.ChainID() == call.DestChainId && target.Address() == call.Target { + if resp != nil { + return nil, errors.New("multiple targets found") + } + resp = &target + } + } + + if resp == nil { + return nil, errors.New("no target found") + } + + return *resp, nil } diff --git a/solver/types/target.go b/solver/types/target.go index f3db66bef..86e78b137 100644 --- a/solver/types/target.go +++ b/solver/types/target.go @@ -2,7 +2,6 @@ package types import ( "github.com/omni-network/omni/contracts/bindings" - "github.com/omni-network/omni/lib/errors" "github.com/ethereum/go-ethereum/common" ) @@ -15,36 +14,10 @@ type Target interface { // Address returns the address of the target contract. Address() common.Address - // IsAllowedCall returns true if the call is allowed. - IsAllowedCall(call bindings.SolveCall) bool - // TokenPrereqs returns the token prerequisites required for the call. TokenPrereqs(call bindings.SolveCall) ([]bindings.SolveTokenPrereq, error) // Verify returns an error if the call should not be fulfilled. + // TODO(corver): Return reject reason. Verify(srcChainID uint64, call bindings.SolveCall, deposits []bindings.SolveDeposit) error } - -type Targets []Target - -func (t Targets) ForCall(call bindings.SolveCall) (Target, error) { - var match Target - var matched bool - - for _, target := range t { - if target.IsAllowedCall(call) { - if matched { - return nil, errors.New("multiple targets found") - } - - match = target - matched = true - } - } - - if !matched { - return nil, errors.New("no target found") - } - - return match, nil -}