diff --git a/core/rawdb/accessors_indexes_test.go b/core/rawdb/accessors_indexes_test.go index a812fefeaa..926c92ba8f 100644 --- a/core/rawdb/accessors_indexes_test.go +++ b/core/rawdb/accessors_indexes_test.go @@ -297,3 +297,61 @@ func TestExtractReceiptFields(t *testing.T) { } } } + +func TestSenderNonceIndex(t *testing.T) { + db := NewMemoryDatabase() + + addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111") + addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222") + + txHash1 := common.HexToHash("0xaaaa") // Sender: addr1, Nonce: 1 + txHash2 := common.HexToHash("0xbbbb") // Sender: addr1, Nonce: 2 + txHash3 := common.HexToHash("0xcccc") // Sender: addr2, Nonce: 1 + + // Initially index is empty + if hash := ReadTxSenderNonceEntry(db, addr1, 1); hash != nil { + t.Fatalf("index should be empty, got %x", hash) + } + + WriteTxSenderNonceEntry(db, addr1, 1, txHash1) + WriteTxSenderNonceEntry(db, addr1, 2, txHash2) + WriteTxSenderNonceEntry(db, addr2, 1, txHash3) + + // Verify indices + tests := []struct { + name string + sender common.Address + nonce uint64 + expect *common.Hash + }{ + {"addr1-nonce1", addr1, 1, &txHash1}, + {"addr1-nonce2", addr1, 2, &txHash2}, + {"addr2-nonce1", addr2, 1, &txHash3}, + {"addr2-nonce2 (missing)", addr2, 2, nil}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := ReadTxSenderNonceEntry(db, tc.sender, tc.nonce) + if tc.expect == nil { + if got != nil { + t.Errorf("expected nil, got %x", got) + } + } else { + if got == nil { + t.Errorf("expected %x, got nil", *tc.expect) + } else if *got != *tc.expect { + t.Errorf("expected %x, got %x", *tc.expect, *got) + } + } + }) + } + + // Verify Overwrite + newHash := common.HexToHash("0xdddd") + WriteTxSenderNonceEntry(db, addr1, 1, newHash) + + if got := ReadTxSenderNonceEntry(db, addr1, 1); *got != newHash { + t.Fatalf("failed to overwrite index: expected %x, got %x", newHash, got) + } +} diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 6d55e2c44b..61ec8e3c91 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -4073,3 +4073,40 @@ func TestSendRawTransactionSync_Timeout(t *testing.T) { t.Fatalf("expected ErrorData=%s, got %v", want, got) } } + +func TestGetTransactionBySenderAndNonce(t *testing.T) { + t.Parallel() + + key, _ := crypto.HexToECDSA("b71c73a37e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr := crypto.PubkeyToAddress(key.PublicKey) + nonce := uint64(0) + tx := types.NewTransaction(nonce, common.Address{0xaa}, big.NewInt(1000), 21000, big.NewInt(1000000000), nil) + signedTx, _ := types.SignTx(tx, types.HomesteadSigner{}, key) + + genesis := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{ + addr: {Balance: big.NewInt(1000000000000000000)}, + }, + } + + backend := newTestBackend(t, 1, genesis, ethash.NewFaker(), func(i int, b *core.BlockGen) { + if i == 0 { + b.AddTx(signedTx) + } + }) + + api := NewTransactionAPI(backend, new(AddrLocker)) + ctx := context.Background() + + result, err := api.GetTransactionBySenderAndNonce(ctx, addr, nonce) + if err != nil { + t.Fatalf("GetTransactionBySenderAndNonce failed: %v", err) + } + if result == nil { + t.Fatalf("Expected transaction, got nil") + } + if *result != signedTx.Hash() { + t.Errorf("Hash mismatch: have %x, want %x", *result, signedTx.Hash()) + } +} diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 754a771ab3..76d9080a7b 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -66,7 +66,6 @@ type Backend interface { BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) - GetTransactionBySenderAndNonce(ctx context.Context, sender common.Address, nonce uint64) (*common.Hash, error) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) Pending() (*types.Block, types.Receipts, *state.StateDB) @@ -87,6 +86,7 @@ type Backend interface { TxPoolContent() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) TxPoolContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction) SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription + GetTransactionBySenderAndNonce(ctx context.Context, sender common.Address, nonce uint64) (*common.Hash, error) ChainConfig() *params.ChainConfig Engine() consensus.Engine diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 30791f32b5..6182f866b9 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -389,6 +389,11 @@ func (b *backendMock) GetCanonicalTransaction(txHash common.Hash) (bool, *types. func (b *backendMock) TxIndexDone() bool { return true } func (b *backendMock) GetPoolTransactions() (types.Transactions, error) { return nil, nil } func (b *backendMock) GetPoolTransaction(txHash common.Hash) *types.Transaction { return nil } + +// GetTransactionHashBySenderAndNonce implements the Backend interface for testing. +func (b *backendMock) GetTransactionBySenderAndNonce(ctx context.Context, sender common.Address, nonce uint64) (*common.Hash, error) { + return nil, nil +} func (b *backendMock) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { return 0, nil }