diff --git a/vms/saevm/sae/BUILD.bazel b/vms/saevm/sae/BUILD.bazel index 64455c105d0b..4c6f86958ae0 100644 --- a/vms/saevm/sae/BUILD.bazel +++ b/vms/saevm/sae/BUILD.bazel @@ -124,6 +124,7 @@ go_test( "@com_github_ava_labs_libevm//crypto", "@com_github_ava_labs_libevm//eth/tracers/logger", "@com_github_ava_labs_libevm//ethclient", + "@com_github_ava_labs_libevm//ethclient/gethclient", "@com_github_ava_labs_libevm//ethdb", "@com_github_ava_labs_libevm//libevm", "@com_github_ava_labs_libevm//libevm/ethapi", @@ -209,6 +210,7 @@ go_test( "@com_github_ava_labs_libevm//crypto", "@com_github_ava_labs_libevm//eth/tracers/logger", "@com_github_ava_labs_libevm//ethclient", + "@com_github_ava_labs_libevm//ethclient/gethclient", "@com_github_ava_labs_libevm//ethdb", "@com_github_ava_labs_libevm//libevm", "@com_github_ava_labs_libevm//libevm/ethapi", diff --git a/vms/saevm/sae/rpc/server.go b/vms/saevm/sae/rpc/server.go index cfdcbdbe6297..5040f7742ff8 100644 --- a/vms/saevm/sae/rpc/server.go +++ b/vms/saevm/sae/rpc/server.go @@ -57,18 +57,27 @@ func (b *backend) server(filter *filters.FilterAPI) (*rpc.Server, error) { {"eth", ethapi.NewEthereumAPI(b)}, // Standard Ethereum node APIs: // - eth_blockNumber + // - eth_call // - eth_chainId + // - eth_estimateGas + // - eth_getBalance // - eth_getBlockByHash // - eth_getBlockByNumber - // - eth_getBlockReceipts + // - eth_getCode + // - eth_getProof + // - eth_getStorageAt // - eth_getUncleByBlockHashAndIndex // - eth_getUncleByBlockNumberAndIndex // - eth_getUncleCountByBlockHash // - eth_getUncleCountByBlockNumber // - // geth-specific APIs: + // Geth-specific APIs: + // - eth_createAccessList // - eth_getHeaderByHash // - eth_getHeaderByNumber + // + // Undocumented APIs: + // - eth_getBlockReceipts {"eth", &blockChainAPI{ethapi.NewBlockChainAPI(b), b}}, // Standard Ethereum node APIs: // - eth_getBlockTransactionCountByHash diff --git a/vms/saevm/sae/rpc_custom_test.go b/vms/saevm/sae/rpc_custom_test.go index 9592dff3f2dc..31160b15266d 100644 --- a/vms/saevm/sae/rpc_custom_test.go +++ b/vms/saevm/sae/rpc_custom_test.go @@ -136,30 +136,7 @@ func TestCallDetailed(t *testing.T) { } })) - deploy := &types.LegacyTx{ - Gas: 1e6, - GasPrice: big.NewInt(1), - Data: escrow.CreationCode(), - } - - escrowAddr := crypto.CreateAddress(sut.wallet.Addresses()[0], 0) - recv := common.Address{'r', 'e', 'c', 'v'} - const depositVal = 42 - deposit := &types.LegacyTx{ - To: &escrowAddr, - Gas: 1e6, - GasPrice: big.NewInt(1), - Data: escrow.CallDataToDeposit(recv), - Value: big.NewInt(depositVal), - } - - sign := sut.wallet.SetNonceAndSign - b := sut.runConsensusLoop(t, sign(t, 0, deploy), sign(t, 0, deposit)) - require.Len(t, b.Transactions(), 2, "tx count") - require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) - for _, r := range b.Receipts() { - require.Equalf(t, types.ReceiptStatusSuccessful, r.Status, "%T.Status", r) - } + _, escrowAddr, recv, _ := sut.deployEscrow(ctx, t, big.NewInt(escrowDepositVal)) const revertWith = 12345 revertAsPanic := slices.Concat( @@ -186,7 +163,7 @@ func TestCallDetailed(t *testing.T) { }, want: saerpc.DetailedExecutionResult{ UsedGas: 23675, - ReturnData: uint256.NewInt(depositVal).PaddedBytes(32), + ReturnData: uint256.NewInt(escrowDepositVal).PaddedBytes(32), }, }, { diff --git a/vms/saevm/sae/rpc_stateful_test.go b/vms/saevm/sae/rpc_stateful_test.go index f1ccfb30220c..edd7506fc78e 100644 --- a/vms/saevm/sae/rpc_stateful_test.go +++ b/vms/saevm/sae/rpc_stateful_test.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/eth/tracers/logger" + "github.com/ava-labs/libevm/ethclient/gethclient" "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/rpc" "github.com/google/go-cmp/cmp" @@ -28,7 +29,6 @@ import ( "github.com/ava-labs/avalanchego/vms/saevm/saetest/escrow" saeparams "github.com/ava-labs/avalanchego/vms/saevm/params" - ethereum "github.com/ava-labs/libevm" ) // TestStateQueryOnNonCanonicalBlock verifies that state-dependent RPC calls @@ -92,30 +92,15 @@ func TestStateQueryBlocksUntilExecuted(t *testing.T) { func TestDebugTrace(t *testing.T) { ctx, sut := newSUT(t, 1) - escrowAddr := crypto.CreateAddress(sut.wallet.Addresses()[0], 0) - recv := common.Address{'r', 'e', 'c', 'v'} - const depositVal = 42 - - sign := sut.wallet.SetNonceAndSign - deployTx := sign(t, 0, &types.LegacyTx{ - Gas: 1e6, - GasPrice: big.NewInt(1), - Data: escrow.CreationCode(), - }) - depositTx := sign(t, 0, &types.LegacyTx{ - To: &escrowAddr, - Gas: 1e6, - GasPrice: big.NewInt(1), - Data: escrow.CallDataToDeposit(recv), - Value: big.NewInt(depositVal), - }) - - b := sut.runConsensusLoop(t, deployTx, depositTx) - require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) - require.Lenf(t, b.Receipts(), 2, "%T.Receipts()", b) - for _, r := range b.Receipts() { - require.Equalf(t, types.ReceiptStatusSuccessful, r.Status, "%T.Status", r) - } + b, _, recv, _ := sut.deployEscrow(ctx, t, big.NewInt(escrowDepositVal)) + // deployEscrow includes the deploy tx at index 0 and the deposit tx at + // index 1. + const ( + deployTxIdx = 0 + depositTxIdx = 1 + ) + deployTxHash := b.Transactions()[deployTxIdx].Hash() + depositTxHash := b.Transactions()[depositTxIdx].Hash() // Specifying the entire trace would be excessive and uninformative so we // select a precise location of an event associated with the deposit() @@ -137,23 +122,23 @@ func TestDebugTrace(t *testing.T) { Error string `json:"error"` }{ { - TxHash: deployTx.Hash(), + TxHash: deployTxHash, Result: &logger.ExecutionResult{ - Gas: b.Receipts()[0].GasUsed, + Gas: b.Receipts()[deployTxIdx].GasUsed, ReturnValue: common.Bytes2Hex(escrow.ByteCode()), StructLogs: []logger.StructLogRes{}, }, }, { - TxHash: depositTx.Hash(), + TxHash: depositTxHash, Result: &logger.ExecutionResult{ - Gas: b.Receipts()[1].GasUsed, + Gas: b.Receipts()[depositTxIdx].GasUsed, StructLogs: []logger.StructLogRes{{ Pc: logPC, Op: vm.LOG1.String(), Depth: 1, Stack: utils.PointerTo([]string{ - escrow.DepositEvent(recv, uint256.NewInt(depositVal)).Topics[0].String(), + escrow.DepositEvent(recv, uint256.NewInt(escrowDepositVal)).Topics[0].String(), "0x40", "0x80", // arbitrary memory locations selected by Solidity }), }}, @@ -199,34 +184,11 @@ func TestDebugTrace(t *testing.T) { sut.testRPC(ctx, t, tests...) } -func TestEthCall(t *testing.T) { +func TestStatefulRPCs(t *testing.T) { opt, vmTime := withVMTime(t, time.Unix(saeparams.TauSeconds, 0)) ctx, sut := newSUT(t, 1, opt) - deploy := &types.LegacyTx{ - Gas: 1e6, - GasPrice: big.NewInt(1), - Data: escrow.CreationCode(), - } - - escrowAddr := crypto.CreateAddress(sut.wallet.Addresses()[0], 0) - recv := common.Address{'r', 'e', 'c', 'v'} - const val = 42 - deposit := &types.LegacyTx{ - To: &escrowAddr, - Gas: 1e6, - GasPrice: big.NewInt(1), - Data: escrow.CallDataToDeposit(recv), - Value: big.NewInt(val), - } - - sign := sut.wallet.SetNonceAndSign - b := sut.runConsensusLoop(t, sign(t, 0, deploy), sign(t, 0, deposit)) - require.Len(t, b.Transactions(), 2, "tx count") - require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) - for _, r := range b.Receipts() { - require.Equalf(t, types.ReceiptStatusSuccessful, r.Status, "%T.Status", r) - } + b, escrowAddr, recv, callMsg := sut.deployEscrow(ctx, t, big.NewInt(escrowDepositVal)) vmTime.advanceToSettle(ctx, t, b) for range 2 { @@ -236,6 +198,21 @@ func TestEthCall(t *testing.T) { _, ok := sut.rawVM.consensusCritical.Load(b.Hash()) require.Falsef(t, ok, "%T[%#x] still in VM memory", b, b.Hash()) + // Storage key for balances[recv] at mapping slot 0: + // keccak256(abi.encode(address, uint256(0))) + storageKey := crypto.Keccak256Hash( + common.LeftPadBytes(recv.Bytes(), 32), + common.Hash{}.Bytes(), + ) + storageKeyHex := storageKey.Hex() + + gc := gethclient.New(sut.rpcClient) + + wantBalance := big.NewInt(escrowDepositVal) + wantStorageValue := big.NewInt(escrowDepositVal) + wantStorageBytes := uint256.NewInt(escrowDepositVal).PaddedBytes(32) + wantCode := escrow.ByteCode() + tests := []struct { name string num rpc.BlockNumber @@ -251,15 +228,68 @@ func TestEthCall(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - msg := ethereum.CallMsg{ - To: &escrowAddr, - Data: escrow.CallDataForBalance(recv), - } - - got, err := sut.CallContract(ctx, msg, big.NewInt(int64(tt.num))) - t.Logf("%T.CallContract(%+v, %d)", sut.Client, msg, tt.num) // avoids having to repeat in failure messages - require.NoError(t, err) - assert.Equal(t, uint256.NewInt(val).PaddedBytes(32), got) + blockNum := big.NewInt(int64(tt.num)) + + t.Run("eth_call", func(t *testing.T) { + got, err := sut.CallContract(ctx, callMsg, blockNum) + require.NoError(t, err, "CallContract()") + assert.Equal(t, wantStorageBytes, got, "CallContract() result") + }) + + t.Run("eth_getBalance", func(t *testing.T) { + got, err := sut.BalanceAt(ctx, escrowAddr, blockNum) + require.NoError(t, err, "BalanceAt()") + require.Zero(t, wantBalance.Cmp(got), "BalanceAt(): want %d, got %s", escrowDepositVal, got) + }) + + t.Run("eth_getCode", func(t *testing.T) { + got, err := sut.CodeAt(ctx, escrowAddr, blockNum) + require.NoError(t, err, "CodeAt()") + assert.Equal(t, wantCode, got, "CodeAt() result") + }) + + t.Run("eth_getStorageAt", func(t *testing.T) { + got, err := sut.StorageAt(ctx, escrowAddr, storageKey, blockNum) + require.NoError(t, err, "StorageAt()") + assert.Equal(t, wantStorageBytes, got, "StorageAt() result") + }) + + t.Run("eth_getProof", func(t *testing.T) { + got, err := gc.GetProof(ctx, escrowAddr, []string{storageKeyHex}, blockNum) + require.NoError(t, err, "GetProof()") + require.NotNil(t, got, "GetProof() result") + + assert.NotEmpty(t, got.AccountProof, "GetProof() accountProof") + require.Zero(t, wantBalance.Cmp(got.Balance), "GetProof() balance: want %d, got %s", escrowDepositVal, got.Balance) + + require.Len(t, got.StorageProof, 1, "GetProof() storageProof length") + assert.NotEmpty(t, got.StorageProof[0].Proof, "GetProof() storageProof[0].Proof") + require.Zero(t, wantStorageValue.Cmp(got.StorageProof[0].Value), "GetProof() storageProof[0].Value: want %d, got %s", escrowDepositVal, got.StorageProof[0].Value) + }) }) } } + +// TestStatefulRPCsLatestOnly tests stateful RPC methods that don't accept a +// block number parameter via ethclient/gethclient and so always run against +// the latest block: eth_estimateGas and eth_createAccessList. +func TestStatefulRPCsLatestOnly(t *testing.T) { + ctx, sut := newSUT(t, 1) + + _, _, _, callMsg := sut.deployEscrow(ctx, t, nil) + + t.Run("eth_estimateGas", func(t *testing.T) { + got, err := sut.EstimateGas(ctx, callMsg) + require.NoError(t, err, "EstimateGas()") + assert.Positive(t, got, "EstimateGas() result") + }) + + t.Run("eth_createAccessList", func(t *testing.T) { + gc := gethclient.New(sut.rpcClient) + al, gas, errMsg, err := gc.CreateAccessList(ctx, callMsg) + require.NoError(t, err, "CreateAccessList()") + assert.Empty(t, errMsg, "CreateAccessList() error message") + assert.NotEmpty(t, al, "CreateAccessList() access list") + assert.Positive(t, gas, "CreateAccessList() gasUsed") + }) +} diff --git a/vms/saevm/sae/vm_test.go b/vms/saevm/sae/vm_test.go index 69603820a64f..a005f9d79676 100644 --- a/vms/saevm/sae/vm_test.go +++ b/vms/saevm/sae/vm_test.go @@ -22,6 +22,7 @@ import ( "github.com/ava-labs/libevm/core/txpool/legacypool" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/ethclient" "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/libevm" @@ -54,11 +55,13 @@ import ( "github.com/ava-labs/avalanchego/vms/saevm/hook" "github.com/ava-labs/avalanchego/vms/saevm/hook/hookstest" "github.com/ava-labs/avalanchego/vms/saevm/saetest" + "github.com/ava-labs/avalanchego/vms/saevm/saetest/escrow" "github.com/ava-labs/avalanchego/vms/saevm/txgossip/txgossiptest" snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" saeparams "github.com/ava-labs/avalanchego/vms/saevm/params" saetypes "github.com/ava-labs/avalanchego/vms/saevm/types" + ethereum "github.com/ava-labs/libevm" libevmhookstest "github.com/ava-labs/libevm/libevm/hookstest" ) @@ -444,6 +447,52 @@ func (s *SUT) runConsensusLoop(tb testing.TB, txs ...*types.Transaction) *blocks return s.runConsensusLoopOnPreference(tb, s.lastAcceptedBlock(tb), txs...) } +// escrowDepositVal is deposit amount used by [SUT.deployEscrow] when the caller +// requests a deposit. +const escrowDepositVal = 42 + +// deployEscrow signs and runs a deploy tx for the escrow contract from +// s.wallet[0]. If depositVal is non-nil, a tx depositing that value to +// balances[recv] is included in the same consensus block. All included txs +// are asserted to execute successfully. +func (s *SUT) deployEscrow(ctx context.Context, tb testing.TB, depositVal *big.Int) ( + b *blocks.Block, escrowAddr, recv common.Address, callMsg ethereum.CallMsg, +) { + tb.Helper() + escrowAddr = crypto.CreateAddress(s.wallet.Addresses()[0], 0) + recv = common.Address{'r', 'e', 'c', 'v'} + + sign := s.wallet.SetNonceAndSign + txs := []*types.Transaction{sign(tb, 0, &types.LegacyTx{ + Gas: 1e6, + GasPrice: big.NewInt(1), + Data: escrow.CreationCode(), + })} + if depositVal != nil { + txs = append(txs, sign(tb, 0, &types.LegacyTx{ + To: &escrowAddr, + Gas: 1e6, + GasPrice: big.NewInt(1), + Data: escrow.CallDataToDeposit(recv), + Value: depositVal, + })) + } + + b = s.runConsensusLoop(tb, txs...) + require.Lenf(tb, b.Transactions(), len(txs), "%T.Transactions()", b) + require.NoErrorf(tb, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) + for _, r := range b.Receipts() { + require.Equalf(tb, types.ReceiptStatusSuccessful, r.Status, "%T.Status", r) + } + + callMsg = ethereum.CallMsg{ + From: s.wallet.Addresses()[0], + To: &escrowAddr, + Data: escrow.CallDataForBalance(recv), + } + return b, escrowAddr, recv, callMsg +} + func (s *SUT) stateAt(tb testing.TB, root common.Hash) *state.StateDB { tb.Helper() sdb, err := s.rawVM.exec.StateDB(root)