diff --git a/vms/saevm/sae/BUILD.bazel b/vms/saevm/sae/BUILD.bazel index 2e3d97e05cf5..64455c105d0b 100644 --- a/vms/saevm/sae/BUILD.bazel +++ b/vms/saevm/sae/BUILD.bazel @@ -131,6 +131,7 @@ go_test( "@com_github_ava_labs_libevm//libevm/options", "@com_github_ava_labs_libevm//log", "@com_github_ava_labs_libevm//params", + "@com_github_ava_labs_libevm//rlp", "@com_github_ava_labs_libevm//rpc", "@com_github_ava_labs_libevm//trie", "@com_github_google_go_cmp//cmp", diff --git a/vms/saevm/sae/rpc/mempool.go b/vms/saevm/sae/rpc/mempool.go index 90a2330aa471..1ae05f046b1c 100644 --- a/vms/saevm/sae/rpc/mempool.go +++ b/vms/saevm/sae/rpc/mempool.go @@ -27,6 +27,7 @@ func (b *backend) TxPoolContentFrom(addr common.Address) ([]*types.Transaction, return b.Set.Pool.ContentFrom(addr) } +// GetPoolTransactions returns only pending transactions from the mempool. func (b *backend) GetPoolTransactions() (types.Transactions, error) { pending := b.Pool.Pending(txpool.PendingFilter{}) diff --git a/vms/saevm/sae/rpc/server.go b/vms/saevm/sae/rpc/server.go index 865e1433f9e9..cfdcbdbe6297 100644 --- a/vms/saevm/sae/rpc/server.go +++ b/vms/saevm/sae/rpc/server.go @@ -124,14 +124,12 @@ func (b *backend) server(filter *filters.FilterAPI) (*rpc.Server, error) { // - debug_dbAncient // - debug_dbAncients // - debug_dbGet - // - debug_getRawTransaction - // - debug_printBlock - // - debug_setHead (no-op, logs info) - // - // TODO: implement once BlockByNumberOrHash and GetReceipts exist: // - debug_getRawBlock // - debug_getRawHeader // - debug_getRawReceipts + // - debug_getRawTransaction + // - debug_printBlock + // - debug_setHead (no-op, logs info) "debug", ethapi.NewDebugAPI(b), }) } diff --git a/vms/saevm/sae/rpc/transactions.go b/vms/saevm/sae/rpc/transactions.go index a7177252d9ee..32febdeb1210 100644 --- a/vms/saevm/sae/rpc/transactions.go +++ b/vms/saevm/sae/rpc/transactions.go @@ -19,6 +19,8 @@ func (b *backend) GetTransaction(ctx context.Context, txHash common.Hash) (exist return true, tx, blockHash, blockNumber, index, nil } +// GetPoolTransaction returns a transaction from the mempool regardless of its +// status (pending or queued). func (b *backend) GetPoolTransaction(txHash common.Hash) *types.Transaction { return b.Set.Pool.Get(txHash) } diff --git a/vms/saevm/sae/rpc_test.go b/vms/saevm/sae/rpc_test.go index 7e2c87ac0369..54873b8a7165 100644 --- a/vms/saevm/sae/rpc_test.go +++ b/vms/saevm/sae/rpc_test.go @@ -26,6 +26,7 @@ import ( "github.com/ava-labs/libevm/libevm/hookstest" "github.com/ava-labs/libevm/libevm/options" "github.com/ava-labs/libevm/params" + "github.com/ava-labs/libevm/rlp" "github.com/ava-labs/libevm/rpc" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -533,7 +534,7 @@ func TestEthGetters(t *testing.T) { timeOpt, vmTime := withVMTime(t, time.Unix(saeparams.TauSeconds, 0)) blockingPrecompile := common.Address{'b', 'l', 'o', 'c', 'k'} precompileOpt, unblock := withBlockingPrecompile(blockingPrecompile) - ctx, sut := newSUT(t, 1, timeOpt, precompileOpt) + ctx, sut := newSUT(t, 1, timeOpt, precompileOpt, withDebugAPI()) t.Cleanup(unblock) t.Run("unknown_hashes", func(t *testing.T) { @@ -608,6 +609,65 @@ func TestEthGetters(t *testing.T) { }) } +func TestMempoolTxGetters(t *testing.T) { + ctx, sut := newSUT(t, 1, withDebugAPI()) + + // These RPC methods use GetPoolTransaction, which returns any transaction + // accepted into the pool regardless of pending/queued status. A + // transaction only needs to have been accepted by the pool (i.e. + // mustSendTx succeeds) to be returned. + pendingTx := sut.wallet.SetNonceAndSign(t, 0, &types.DynamicFeeTx{ + To: &zeroAddr, + Gas: params.TxGas, + GasFeeCap: big.NewInt(1), + }) + // Skip nonce 1 to create a gap so queuedTx (nonce 2) is accepted into + // the pool but not marked as pending. + sut.wallet.SetNonceAndSign(t, 0, &types.DynamicFeeTx{ + To: &zeroAddr, + Gas: params.TxGas, + GasFeeCap: big.NewInt(1), + }) + queuedTx := sut.wallet.SetNonceAndSign(t, 0, &types.DynamicFeeTx{ + To: &zeroAddr, + Gas: params.TxGas, + GasFeeCap: big.NewInt(1), + }) + sut.mustSendTx(t, pendingTx, queuedTx) + sut.waitUntilTxsPending(t, pendingTx) + + for _, tt := range []struct { + name string + tx *types.Transaction + }{ + {"pending", pendingTx}, + {"queued", queuedTx}, + } { + t.Run(tt.name, func(t *testing.T) { + marshaled, err := tt.tx.MarshalBinary() + require.NoErrorf(t, err, "%T.MarshalBinary()", tt.tx) + + sut.testRPC(ctx, t, []rpcTest{ + { + method: "eth_getTransactionByHash", + args: []any{tt.tx.Hash()}, + want: tt.tx, + }, + { + method: "eth_getRawTransactionByHash", + args: []any{tt.tx.Hash()}, + want: hexutil.Bytes(marshaled), + }, + { + method: "debug_getRawTransaction", + args: []any{tt.tx.Hash()}, + want: hexutil.Bytes(marshaled), + }, + }...) + }) + } +} + func TestGetLogs(t *testing.T) { // We shorten section size to reduce number of required blocks in the test. const bloomSectionSize = 8 @@ -789,7 +849,7 @@ func TestGetReceipts(t *testing.T) { timeOpt, vmTime := withVMTime(t, time.Unix(saeparams.TauSeconds, 0)) precompileOpt, unblock := withBlockingPrecompile(blockingPrecompile) - ctx, sut := newSUT(t, 2, timeOpt, precompileOpt) + ctx, sut := newSUT(t, 2, timeOpt, precompileOpt, withDebugAPI()) t.Cleanup(unblock) var ( @@ -860,33 +920,60 @@ func TestGetReceipts(t *testing.T) { GasPrice: big.NewInt(1), })) + marshalReceipts := func(rs []*types.Receipt) []hexutil.Bytes { + raw := make([]hexutil.Bytes, len(rs)) + for i, r := range rs { + buf, err := r.MarshalBinary() + require.NoErrorf(t, err, "receipts[%d].MarshalBinary()", i) + raw[i] = buf + } + return raw + } + var tests []rpcTest for _, tc := range []struct { - id rpc.BlockNumberOrHash + ids []rpc.BlockNumberOrHash want []*types.Receipt }{ { - id: rpc.BlockNumberOrHashWithHash(onDisk.Hash(), true), + ids: []rpc.BlockNumberOrHash{ + rpc.BlockNumberOrHashWithHash(onDisk.Hash(), true), + rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(onDisk.Height())), //#nosec G115 -- Won't overflow + }, want: wantOnDisk, }, { - id: rpc.BlockNumberOrHashWithHash(settled.Hash(), true), + ids: []rpc.BlockNumberOrHash{ + rpc.BlockNumberOrHashWithHash(settled.Hash(), true), + rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(settled.Height())), //#nosec G115 -- Won't overflow + rpc.BlockNumberOrHashWithNumber(rpc.SafeBlockNumber), + rpc.BlockNumberOrHashWithNumber(rpc.FinalizedBlockNumber), + }, want: wantSettled, }, { - id: rpc.BlockNumberOrHashWithHash(unsettled.Hash(), true), - want: wantUnsettled, - }, - { - id: rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), + ids: []rpc.BlockNumberOrHash{ + rpc.BlockNumberOrHashWithHash(unsettled.Hash(), true), + rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(unsettled.Height())), //#nosec G115 -- Won't overflow + rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), + }, want: wantUnsettled, }, } { - tests = append(tests, rpcTest{ - method: "eth_getBlockReceipts", - args: []any{tc.id.String()}, - want: tc.want, - }) + for _, id := range tc.ids { + tests = append(tests, []rpcTest{ + { + method: "eth_getBlockReceipts", + args: []any{id.String()}, + want: tc.want, + }, + { + method: "debug_getRawReceipts", + args: []any{id.String()}, + want: marshalReceipts(tc.want), + }, + }...) + } } for i, tx := range txs { @@ -911,21 +998,41 @@ func TestGetReceipts(t *testing.T) { args: []any{common.Hash{}}, want: ([]*types.Receipt)(nil), }, + { + method: "debug_getRawReceipts", + args: []any{common.Hash{}}, + want: []hexutil.Bytes{}, + }, { method: "eth_getBlockReceipts", args: []any{genesis.Hash()}, want: []*types.Receipt{}, }, + { + method: "debug_getRawReceipts", + args: []any{genesis.Hash()}, + want: []hexutil.Bytes{}, + }, { method: "eth_getBlockReceipts", args: []any{pending.Hash()}, want: ([]*types.Receipt)(nil), }, + { + method: "debug_getRawReceipts", + args: []any{pending.Hash()}, + want: []hexutil.Bytes{}, + }, { method: "eth_getBlockReceipts", args: []any{hexutil.Uint64(pending.Height())}, want: ([]*types.Receipt)(nil), }, + { + method: "debug_getRawReceipts", + args: []any{hexutil.Uint64(pending.Height())}, + want: []hexutil.Bytes{}, + }, }...) sut.testRPC(ctx, t, tests...) @@ -1175,6 +1282,11 @@ func TestDebugRPCs(t *testing.T) { method: "debug_dbAncients", wantErr: testerr.Contains("not supported"), }, + { + method: "debug_printBlock", + args: []any{uint64(1)}, // SUT only has genesis, so block 1 doesn't exist. + wantErr: testerr.Contains("not found"), + }, }...) // The profiling debug namespace is handled entirely by upstream code @@ -1205,57 +1317,16 @@ func TestDebugRPCs(t *testing.T) { }) } -func TestDebugGetRawTransaction(t *testing.T) { - ctx, sut := newSUT(t, 1, withDebugAPI()) - - tx := sut.wallet.SetNonceAndSign(t, 0, &types.DynamicFeeTx{ - To: &common.Address{}, - Gas: params.TxGas, - GasFeeCap: big.NewInt(1), - }) - b := sut.runConsensusLoop(t, tx) - require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) - - marshaled, err := tx.MarshalBinary() - require.NoErrorf(t, err, "%T.MarshalBinary()", tx) - - // Mempool tx: send without building a block, then query. - mempoolTx := sut.wallet.SetNonceAndSign(t, 0, &types.DynamicFeeTx{ - To: &common.Address{}, - Gas: params.TxGas, - GasFeeCap: big.NewInt(1), - }) - sut.sendTxsAndWaitUntilPending(t, mempoolTx) - - mempoolMarshaled, err := mempoolTx.MarshalBinary() - require.NoErrorf(t, err, "%T.MarshalBinary()", mempoolTx) - - t.Logf("Tx in block: %#x", tx.Hash()) - t.Logf("Tx in mempool: %#x", mempoolTx.Hash()) - - sut.testRPC(ctx, t, []rpcTest{ - { - method: "debug_getRawTransaction", - args: []any{tx.Hash()}, - want: hexutil.Bytes(marshaled), - }, - { - method: "debug_getRawTransaction", - args: []any{common.Hash{}}, - want: hexutil.Bytes(nil), - }, - { - method: "debug_getRawTransaction", - args: []any{mempoolTx.Hash()}, - want: hexutil.Bytes(mempoolMarshaled), - }, - }...) -} - func (s *SUT) testGetByHash(ctx context.Context, t *testing.T, want *types.Block) { t.Helper() testRPCGetter(ctx, t, "eth_getBlockByHash", s.BlockByHash, want.Hash(), want) + + wantBlockRLP, err := rlp.EncodeToBytes(want) + require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", want) + wantHeaderRLP, err := rlp.EncodeToBytes(want.Header()) + require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", want.Header()) + s.testRPC(ctx, t, []rpcTest{ { method: "eth_getBlockByHash", @@ -1277,6 +1348,16 @@ func (s *SUT) testGetByHash(ctx context.Context, t *testing.T, want *types.Block args: []any{want.Hash()}, want: hexutil.Uint(0), // SAE never has uncles (no reorgs) }, + { + method: "debug_getRawBlock", + args: []any{want.Hash()}, + want: hexutil.Bytes(wantBlockRLP), + }, + { + method: "debug_getRawHeader", + args: []any{want.Hash()}, + want: hexutil.Bytes(wantHeaderRLP), + }, }...) for i, wantTx := range want.Transactions() { @@ -1305,6 +1386,11 @@ func (s *SUT) testGetByHash(ctx context.Context, t *testing.T, want *types.Block args: []any{wantTx.Hash()}, want: hexutil.Bytes(marshaled), }, + { + method: "debug_getRawTransaction", + args: []any{wantTx.Hash()}, + want: hexutil.Bytes(marshaled), + }, }...) } @@ -1362,6 +1448,21 @@ func (s *SUT) testGetByUnknownHash(ctx context.Context, t *testing.T) { args: []any{common.Hash{}}, want: hexutil.Bytes(nil), }, + { + method: "debug_getRawTransaction", + args: []any{common.Hash{}}, + want: hexutil.Bytes(nil), + }, + { + method: "debug_getRawBlock", + args: []any{common.Hash{}}, + wantErr: testerr.Contains("not found"), + }, + { + method: "debug_getRawHeader", + args: []any{common.Hash{}}, + wantErr: testerr.Contains("not found"), + }, }...) } @@ -1372,6 +1473,11 @@ func (s *SUT) testGetByNumber(ctx context.Context, t *testing.T, want *types.Blo t.Helper() testRPCGetter(ctx, t, "eth_getBlockByNumber", s.BlockByNumber, big.NewInt(n.Int64()), want) + wantBlockRLP, err := rlp.EncodeToBytes(want) + require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", want) + wantHeaderRLP, err := rlp.EncodeToBytes(want.Header()) + require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", want.Header()) + s.testRPC(ctx, t, []rpcTest{ { method: "eth_getBlockByNumber", @@ -1393,6 +1499,16 @@ func (s *SUT) testGetByNumber(ctx context.Context, t *testing.T, want *types.Blo args: []any{n}, want: hexutil.Uint(0), // SAE never has uncles (no reorgs) }, + { + method: "debug_getRawBlock", + args: []any{n}, + want: hexutil.Bytes(wantBlockRLP), + }, + { + method: "debug_getRawHeader", + args: []any{n}, + want: hexutil.Bytes(wantHeaderRLP), + }, }...) for i, wantTx := range want.Transactions() { @@ -1459,6 +1575,16 @@ func (s *SUT) testGetByUnknownNumber(ctx context.Context, t *testing.T) { args: []any{n, hexutil.Uint(0)}, want: hexutil.Bytes(nil), }, + { + method: "debug_getRawBlock", + args: []any{n}, + want: hexutil.Bytes(nil), + }, + { + method: "debug_getRawHeader", + args: []any{n}, + want: hexutil.Bytes(nil), + }, }...) }