internal/ethapi: fix tx.from in eth_simulateV1 (#31480)

Issue statement: when user requests eth_simulateV1 to return full
transaction objects, these objects always had an empty `from` field. The
reason is we lose the sender when translation the message into a
types.Transaction which is then later on serialized.

I did think of an alternative but opted to keep with this approach as it
keeps complexity at the edge. The alternative would be to pass down a
signer object to RPCMarshal* methods and define a custom signer which
keeps the senders in its state and doesn't attempt the signature
recovery.
This commit is contained in:
Sina M 2025-05-05 12:42:19 +02:00 committed by GitHub
parent fc2ba1fb2e
commit bca0646ede
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 102 additions and 12 deletions

View file

@ -2488,6 +2488,77 @@ func TestSimulateV1ChainLinkage(t *testing.T) {
require.Equal(t, block2.Hash().Bytes(), []byte(results[2].Calls[1].ReturnValue), "returned blockhash for block2 does not match")
}
func TestSimulateV1TxSender(t *testing.T) {
var (
sender = common.Address{0xaa, 0xaa}
sender2 = common.Address{0xaa, 0xab}
sender3 = common.Address{0xaa, 0xac}
recipient = common.Address{0xbb, 0xbb}
gspec = &core.Genesis{
Config: params.MergedTestChainConfig,
Alloc: types.GenesisAlloc{
sender: {Balance: big.NewInt(params.Ether)},
sender2: {Balance: big.NewInt(params.Ether)},
sender3: {Balance: big.NewInt(params.Ether)},
},
}
ctx = context.Background()
)
backend := newTestBackend(t, 0, gspec, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {})
stateDB, baseHeader, err := backend.StateAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err != nil {
t.Fatalf("failed to get state and header: %v", err)
}
sim := &simulator{
b: backend,
state: stateDB,
base: baseHeader,
chainConfig: backend.ChainConfig(),
gp: new(core.GasPool).AddGas(math.MaxUint64),
traceTransfers: false,
validate: false,
fullTx: true,
}
results, err := sim.execute(ctx, []simBlock{
{Calls: []TransactionArgs{
{From: &sender, To: &recipient, Value: (*hexutil.Big)(big.NewInt(1000))},
{From: &sender2, To: &recipient, Value: (*hexutil.Big)(big.NewInt(2000))},
{From: &sender3, To: &recipient, Value: (*hexutil.Big)(big.NewInt(3000))},
}},
{Calls: []TransactionArgs{
{From: &sender2, To: &recipient, Value: (*hexutil.Big)(big.NewInt(4000))},
}},
})
if err != nil {
t.Fatalf("simulation execution failed: %v", err)
}
require.Len(t, results, 2, "expected 2 simulated blocks")
require.Len(t, results[0].Block.Transactions(), 3, "expected 3 transaction in simulated block")
require.Len(t, results[1].Block.Transactions(), 1, "expected 1 transaction in 2nd simulated block")
enc, err := json.Marshal(results)
if err != nil {
t.Fatalf("failed to marshal results: %v", err)
}
type resultType struct {
Transactions []struct {
From common.Address `json:"from"`
}
}
var summary []resultType
if err := json.Unmarshal(enc, &summary); err != nil {
t.Fatalf("failed to unmarshal results: %v", err)
}
require.Len(t, summary, 2, "expected 2 simulated blocks")
require.Len(t, summary[0].Transactions, 3, "expected 3 transaction in simulated block")
require.Equal(t, sender, summary[0].Transactions[0].From, "sender address mismatch")
require.Equal(t, sender2, summary[0].Transactions[1].From, "sender address mismatch")
require.Equal(t, sender3, summary[0].Transactions[2].From, "sender address mismatch")
require.Len(t, summary[1].Transactions, 1, "expected 1 transaction in simulated block")
require.Equal(t, sender2, summary[1].Transactions[0].From, "sender address mismatch")
}
func TestSignTransaction(t *testing.T) {
t.Parallel()
// Initialize test accounts

View file

@ -78,11 +78,25 @@ type simBlockResult struct {
chainConfig *params.ChainConfig
Block *types.Block
Calls []simCallResult
// senders is a map of transaction hashes to their senders.
senders map[common.Hash]common.Address
}
func (r *simBlockResult) MarshalJSON() ([]byte, error) {
blockData := RPCMarshalBlock(r.Block, true, r.fullTx, r.chainConfig)
blockData["calls"] = r.Calls
// Set tx sender if user requested full tx objects.
if r.fullTx {
if raw, ok := blockData["transactions"].([]any); ok {
for _, tx := range raw {
if tx, ok := tx.(*RPCTransaction); ok {
tx.From = r.senders[tx.Hash]
} else {
return nil, errors.New("simulated transaction result has invalid type")
}
}
}
}
return json.Marshal(blockData)
}
@ -181,18 +195,18 @@ func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]*simBlo
parent = sim.base
)
for bi, block := range blocks {
result, callResults, err := sim.processBlock(ctx, &block, headers[bi], parent, headers[:bi], timeout)
result, callResults, senders, err := sim.processBlock(ctx, &block, headers[bi], parent, headers[:bi], timeout)
if err != nil {
return nil, err
}
headers[bi] = result.Header()
results[bi] = &simBlockResult{fullTx: sim.fullTx, chainConfig: sim.chainConfig, Block: result, Calls: callResults}
results[bi] = &simBlockResult{fullTx: sim.fullTx, chainConfig: sim.chainConfig, Block: result, Calls: callResults, senders: senders}
parent = result.Header()
}
return results, nil
}
func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, parent *types.Header, headers []*types.Header, timeout time.Duration) (*types.Block, []simCallResult, error) {
func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, parent *types.Header, headers []*types.Header, timeout time.Duration) (*types.Block, []simCallResult, map[common.Hash]common.Address, error) {
// Set header fields that depend only on parent block.
// Parent hash is needed for evm.GetHashFn to work.
header.ParentHash = parent.Hash()
@ -222,7 +236,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
precompiles := sim.activePrecompiles(sim.base)
// State overrides are applied prior to execution of a block
if err := block.StateOverrides.Apply(sim.state, precompiles); err != nil {
return nil, nil, err
return nil, nil, nil, err
}
var (
gasUsed, blobGasUsed uint64
@ -235,6 +249,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
NoBaseFee: !sim.validate,
Tracer: tracer.Hooks(),
}
// senders is a map of transaction hashes to their senders.
// Transaction objects contain only the signature, and we lose track
// of the sender when translating the arguments into a transaction object.
senders = make(map[common.Hash]common.Address)
)
tracingStateDB := vm.StateDB(sim.state)
if hooks := tracer.Hooks(); hooks != nil {
@ -255,16 +273,17 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
var allLogs []*types.Log
for i, call := range block.Calls {
if err := ctx.Err(); err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if err := sim.sanitizeCall(&call, sim.state, header, blockContext, &gasUsed); err != nil {
return nil, nil, err
return nil, nil, nil, err
}
var (
tx = call.ToTransaction(types.DynamicFeeTxType)
txHash = tx.Hash()
)
txes[i] = tx
senders[txHash] = call.from()
tracer.reset(txHash, uint(i))
sim.state.SetTxContext(txHash, i)
// EoA check is always skipped, even in validation mode.
@ -272,7 +291,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp)
if err != nil {
txErr := txValidationError(err)
return nil, nil, txErr
return nil, nil, nil, txErr
}
// Update the state with pending changes.
var root []byte
@ -311,15 +330,15 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
requests = [][]byte{}
// EIP-6110
if err := core.ParseDepositLogs(&requests, allLogs, sim.chainConfig); err != nil {
return nil, nil, err
return nil, nil, nil, err
}
// EIP-7002
if err := core.ProcessWithdrawalQueue(&requests, evm); err != nil {
return nil, nil, err
return nil, nil, nil, err
}
// EIP-7251
if err := core.ProcessConsolidationQueue(&requests, evm); err != nil {
return nil, nil, err
return nil, nil, nil, err
}
}
if requests != nil {
@ -330,10 +349,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
chainHeadReader := &simChainHeadReader{ctx, sim.b}
b, err := sim.b.Engine().FinalizeAndAssemble(chainHeadReader, header, sim.state, blockBody, receipts)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
repairLogs(callResults, b.Hash())
return b, callResults, nil
return b, callResults, senders, nil
}
// repairLogs updates the block hash in the logs present in the result of