Skip to content

Commit 1240c27

Browse files
committed
fix: add call path to revert errors and improve sender recovery
- Include target address in revert error messages for both eth_simulateV1 and debug_traceCall to help identify which call failed - Handle contract creation case consistently (show "contract creation" when to address is empty) - Add recoverSender function that handles different tx types correctly: pre-EIP-155 (HomesteadSigner), EIP-155, EIP-2930, EIP-1559, EIP-4844 - Add tests with real mainnet transactions (pre-EIP-155 and EIP-1559) to verify parsing and sender recovery end-to-end
1 parent 0dc5384 commit 1240c27

2 files changed

Lines changed: 140 additions & 7 deletions

File tree

tools/preconf-rpc/sim/inline_simulator.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func NewInlineSimulator(rpcURLs []string, logger *slog.Logger) (*InlineSimulator
9494
}
9595

9696
if len(endpoints) == 0 {
97-
return nil, fmt.Errorf("failed to connect to any RPC endpoint")
97+
return nil, errors.New("failed to connect to any RPC endpoint")
9898
}
9999

100100
if logger == nil {
@@ -140,11 +140,10 @@ func (s *InlineSimulator) Simulate(ctx context.Context, txRaw string, state SimS
140140
return nil, false, fmt.Errorf("invalid transaction: %w", err)
141141
}
142142

143-
signer := types.LatestSignerForChainID(tx.ChainId())
144-
sender, err := types.Sender(signer, tx)
143+
sender, err := recoverSender(tx)
145144
if err != nil {
146145
s.metrics.fail.Inc()
147-
return nil, false, fmt.Errorf("failed to get sender: %w", err)
146+
return nil, false, fmt.Errorf("failed to recover sender: %w", err)
148147
}
149148

150149
// Build call object. We use "input" here; debug_traceCall expects "data" so we convert later.
@@ -158,7 +157,6 @@ func (s *InlineSimulator) Simulate(ctx context.Context, txRaw string, state SimS
158157
callObj["to"] = tx.To().Hex()
159158
}
160159

161-
// Set gas price fields based on tx type (EIP-1559 vs legacy)
162160
switch tx.Type() {
163161
case types.DynamicFeeTxType, types.BlobTxType:
164162
callObj["maxFeePerGas"] = hexutil.EncodeBig(tx.GasFeeCap())
@@ -245,6 +243,12 @@ func (s *InlineSimulator) executeSimulateV1(ctx context.Context, client *rpc.Cli
245243

246244
call := block.Calls[0]
247245

246+
// Extract call target for error messages
247+
toAddr := "contract creation"
248+
if to, ok := callObj["to"].(string); ok && to != "" {
249+
toAddr = to
250+
}
251+
248252
// status 0 means reverted
249253
if call.Status == 0 {
250254
reason := "execution reverted"
@@ -253,7 +257,7 @@ func (s *InlineSimulator) executeSimulateV1(ctx context.Context, client *rpc.Cli
253257
} else if len(call.ReturnData) > 0 {
254258
reason = decodeRevert(hexutil.Encode(call.ReturnData), reason)
255259
}
256-
return nil, false, &NonRetryableError{Err: fmt.Errorf("reverted: %s", reason)}
260+
return nil, false, &NonRetryableError{Err: fmt.Errorf("reverted: %s (to=%s)", reason, toAddr)}
257261
}
258262

259263
if call.GasUsed == 0 {
@@ -296,7 +300,11 @@ func (s *InlineSimulator) executeDebugTraceCall(ctx context.Context, client *rpc
296300

297301
if result.Error != "" {
298302
reason := decodeRevertFromTrace(result.Output, result.Error)
299-
return nil, false, &NonRetryableError{Err: fmt.Errorf("reverted: %s", reason)}
303+
toAddr := result.To
304+
if toAddr == "" {
305+
toAddr = "contract creation"
306+
}
307+
return nil, false, &NonRetryableError{Err: fmt.Errorf("reverted: %s (to=%s)", reason, toAddr)}
300308
}
301309

302310
// Check nested calls for reverts (e.g., inner contract call failed)
@@ -422,3 +430,30 @@ func (s *InlineSimulator) Close() error {
422430
}
423431
return nil
424432
}
433+
434+
// recoverSender extracts the sender address from a signed transaction.
435+
// Uses the appropriate signer based on transaction type to handle edge cases
436+
// like pre-EIP-155 transactions that lack chain ID replay protection.
437+
func recoverSender(tx *types.Transaction) (common.Address, error) {
438+
var signer types.Signer
439+
440+
switch tx.Type() {
441+
case types.LegacyTxType:
442+
chainID := tx.ChainId()
443+
if chainID.Sign() == 0 {
444+
signer = types.HomesteadSigner{}
445+
} else {
446+
signer = types.NewEIP155Signer(chainID)
447+
}
448+
case types.AccessListTxType:
449+
signer = types.NewEIP2930Signer(tx.ChainId())
450+
case types.DynamicFeeTxType:
451+
signer = types.NewLondonSigner(tx.ChainId())
452+
case types.BlobTxType:
453+
signer = types.NewCancunSigner(tx.ChainId())
454+
default:
455+
signer = types.LatestSignerForChainID(tx.ChainId())
456+
}
457+
458+
return types.Sender(signer, tx)
459+
}

tools/preconf-rpc/sim/inline_simulator_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package sim_test
22

33
import (
44
"context"
5+
"encoding/hex"
56
"encoding/json"
67
"net/http"
78
"net/http/httptest"
89
"strings"
910
"testing"
1011

1112
"github.com/ethereum/go-ethereum/common"
13+
"github.com/ethereum/go-ethereum/core/types"
1214
"github.com/primev/mev-commit/tools/preconf-rpc/sim"
1315
)
1416

@@ -255,6 +257,102 @@ var traceCallResponseBalancer = `{
255257
]
256258
}`
257259

260+
// Real mainnet transaction test vectors for e2e validation of parsing and sender recovery.
261+
var realTxVectors = []struct {
262+
name string
263+
rawHex string
264+
expectedType uint8
265+
expectedSender string
266+
hasChainID bool
267+
}{
268+
{
269+
// Pre-EIP-155 legacy transaction (no chain ID replay protection)
270+
// Block 46147 - early mainnet transaction
271+
name: "PreEIP155_Legacy",
272+
rawHex: "f86780862d79883d2000825208945df9b87991262f6ba471f09758cde1c0fc1de734827a69801ca088ff6cf0fefd94db46111149ae4bfc179e9b94721fffd821d38d16464b3f71d0a045e0aff800961cfce805daef7016f9ae479c0a24afba38dd33c2ecdbb01dcacf",
273+
expectedType: types.LegacyTxType,
274+
expectedSender: "0xD3678D173368032b34E00AE057C31b083FBAb830",
275+
hasChainID: false,
276+
},
277+
{
278+
// EIP-1559 dynamic fee transaction (type 2)
279+
name: "EIP1559_DynamicFee",
280+
rawHex: "02f8730101843b9aca00850c92a69c0082520894d8da6bf26964af9d7eed9e03e53415d37aa9604588016345785d8a000080c001a0a9f0aabbfa2b831dd37d0f8d48d941f35f4fd40a1f2e2fa74a7df3e60aa534c8a0488e799fae157d086b8e0b624ab63627f14509482fe037e88f516a3725070896",
281+
expectedType: types.DynamicFeeTxType,
282+
expectedSender: "0xcEC000D467698070C6D8D73D8ff1F60FD7DCb531",
283+
hasChainID: true,
284+
},
285+
}
286+
287+
func TestTransactionParsingAndSenderRecovery(t *testing.T) {
288+
for _, tc := range realTxVectors {
289+
t.Run(tc.name, func(t *testing.T) {
290+
rawBytes, err := hex.DecodeString(tc.rawHex)
291+
if err != nil {
292+
t.Fatalf("failed to decode hex: %v", err)
293+
}
294+
295+
tx := new(types.Transaction)
296+
if err := tx.UnmarshalBinary(rawBytes); err != nil {
297+
t.Fatalf("failed to parse tx: %v", err)
298+
}
299+
300+
if tx.Type() != tc.expectedType {
301+
t.Errorf("expected tx type %d, got %d", tc.expectedType, tx.Type())
302+
}
303+
304+
if tc.hasChainID {
305+
if tx.ChainId().Sign() == 0 {
306+
t.Error("expected non-zero chainId")
307+
}
308+
} else {
309+
if tx.ChainId().Sign() != 0 {
310+
t.Errorf("expected chainId 0, got %s", tx.ChainId().String())
311+
}
312+
}
313+
314+
sender, err := recoverSenderForTest(tx)
315+
if err != nil {
316+
t.Fatalf("failed to recover sender: %v", err)
317+
}
318+
319+
if sender == (common.Address{}) {
320+
t.Error("recovered zero address")
321+
}
322+
323+
if tc.expectedSender != "" {
324+
expected := common.HexToAddress(tc.expectedSender)
325+
if sender != expected {
326+
t.Errorf("sender mismatch: got %s, want %s", sender.Hex(), expected.Hex())
327+
}
328+
}
329+
330+
t.Logf("tx=%s sender=%s chainId=%s", tc.name, sender.Hex(), tx.ChainId().String())
331+
})
332+
}
333+
}
334+
335+
func recoverSenderForTest(tx *types.Transaction) (common.Address, error) {
336+
var signer types.Signer
337+
switch tx.Type() {
338+
case types.LegacyTxType:
339+
if tx.ChainId().Sign() == 0 {
340+
signer = types.HomesteadSigner{}
341+
} else {
342+
signer = types.NewEIP155Signer(tx.ChainId())
343+
}
344+
case types.AccessListTxType:
345+
signer = types.NewEIP2930Signer(tx.ChainId())
346+
case types.DynamicFeeTxType:
347+
signer = types.NewLondonSigner(tx.ChainId())
348+
case types.BlobTxType:
349+
signer = types.NewCancunSigner(tx.ChainId())
350+
default:
351+
signer = types.LatestSignerForChainID(tx.ChainId())
352+
}
353+
return types.Sender(signer, tx)
354+
}
355+
258356
func TestInlineSimulator(t *testing.T) {
259357
// eth_simulateV1 responses
260358
simV1Responses := map[string]string{

0 commit comments

Comments
 (0)