Skip to content

debug_traceTransaction returns null for transactions preceded by a gasless (zero-fee) tx in the same block #277

Description

@AntonieDavid

Summary

debug_traceTransaction returns {"result": null} (HTTP 200) for a large fraction of X Layer mainnet transactions, even though the transactions exist, succeeded on-chain, and are traceable via other methods. Through some investigation this appears to be caused by the gasless (zero-fee) transaction feature interacting badly with the transaction-replay path used by the RPC tracer.

Environment

xlayer-reth-node Version: 0.1.0-dev (dbc1495)   # tag v0.0.6.3
Upstream Reth Version: 5101851b

Observed behavior

For a transaction that is not the first non-deposit transaction in its block, debug_traceTransaction returns null:

# Example on X Layer mainnet: tx at block 64,564,751, index 5
curl -s -X POST "$RPC" -H 'Content-Type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"debug_traceTransaction",
  "params":["0xfeba105bc2c9a813712fe78f6619be7bea4601ddc8df99943f843737ffe8cd06",
            {"tracer":"callTracer"}]
}'
# -> {"jsonrpc":"2.0","id":1,"result":null}

The transaction clearly exists and succeeded:

  • eth_getTransactionByHash / eth_getTransactionReceipt return it (status: 0x1, transactionIndex: 0x5).
  • debug_traceBlockByNumber on the same block returns a full, correct trace for all transactions in the block, including this one.
  • The parity-style trace_transaction on the same hash does not return null but surfaces an error instead:
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"max fee per gas less than block base fee"}}

So the state and history are present; only the single-transaction trace path fails.

Reproduction pattern (root cause)

Tracing each transaction in block 64,564,751 individually with debug_traceTransaction shows a clear boundary:

index tx type effective gas price debug_traceTransaction
0 126 (deposit) 0 OK
1 2 (EIP-1559) maxFeePerGas = 0 OK (traced directly)
2..8 mixed null

Index 1 is a gasless transaction: a type 0x2 tx with maxFeePerGas = 0 / maxPriorityFeePerGas = 0, included in a block whose baseFeePerGas = 20000000.

  • Tracing index 1 directly works, because the tracer disables the base-fee check for the target transaction.
  • Tracing any transaction at index ≥ 2 fails, because debug_traceTransaction first replays the preceding transactions in the block to reconstruct the pre-state — and replaying the gasless index-1 tx goes through the normal execution path with base-fee validation enabled. The zero-fee tx is rejected with max fee per gas less than block base fee, the replay aborts, and debug_traceTransaction swallows the error and returns null.

In other words: any transaction that is preceded in its block by a gasless (zero-fee) transaction cannot be traced with debug_traceTransaction / trace_transaction / trace_get / trace_replayTransaction. Since gasless transactions are common, this affects a large share of trace requests. Block-level tracers (debug_traceBlockByNumber / debug_traceBlockByHash) are unaffected because they execute the block through the block executor, which already applies the gasless base-fee relaxation.

Suggested cause / fix

The gasless base-fee relaxation is applied in the block executor (OpBlockExecutor::execute_transaction_without_commit) and is also re-implemented for the flashblocks builder. The builder code even documents this explicitly — see crates/builder/src/flashblocks/context.rs, transact_maybe_gasless / is_gasless:

"Mirrors the gasless detection in the upstream block executor … The flashblocks builder executes pool transactions directly via Evm::transact rather than through the block executor, so the detection and base-fee relaxation have to be replicated here, otherwise zero-priced (whitelisted) transactions would be rejected by base-fee validation even when gasless is enabled."

The RPC tracing/replay path (the eth/debug/trace namespaces, delegated to the upstream OpEthApi replay_transactions_until) is a third place that executes transactions via Evm::transact outside the block executor, and it does not replicate the gasless detection. As a result, preceding gasless transactions are rejected during replay.

Two possible directions:

  1. Apply the same gasless detection (set_gasless(true) when the tx is a whitelisted zero-fee tx) when replaying preceding transactions in the RPC tracing path, mirroring transact_maybe_gasless.
  2. Disable base-fee validation for all transactions replayed during tracing (the tracer already disables it for the target transaction; extending this to the replayed predecessors would also resolve it and is arguably correct for tracing in general).

Impact

debug_traceTransaction, trace_transaction, trace_get, and trace_replayTransaction return null / error for any transaction positioned after a gasless transaction in the same block, despite the data being fully available on the node.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions