Skip to content

t8n: forged authorizationList signer is trusted over EIP-7702 signature recovery #1483

@chfast

Description

@chfast

Summary

evmone-t8n accepts a forged authorizationList[*].signer field and applies EIP-7702 delegation even when the tuple signature is invalid (r = s = 0).

Compared with go-ethereum evm t8n, this produces a different post-state root for the same tx JSON.

Why this is a consensus mismatch

EIP-7702 requires recovering authority = ecrecover(...) from the authorization tuple signature and skipping invalid tuples.

In evmone test transition logic:

  • process_authorization_list() explicitly has a TODO for partial verification and only requires that JSON signer is present.
    • test/state/state.cpp:
      • checks if (!auth.signer.has_value()) continue;
      • then mutates the recovered authority account using *auth.signer
  • JSON loader accepts authorizationList[*].signer from input.
    • test/utils/statetest_loader.cpp

This allows unauthorized delegation when JSON injects signer but (v,r,s) does not authorize that account.

Geth, by contrast, recovers authority from signature (auth.Authority()) and skips invalid tuples.

Reproducer (Prague t8n)

Files:

  • alloc.json
  • env.json
  • txs.json (contains one set-code tx with invalid auth tuple signature and injected signer)

alloc.json

{
  "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
    "balance": "0xde0b6b3a7640000",
    "nonce": "0x0",
    "code": "0x",
    "storage": {}
  }
}

env.json

{
  "currentCoinbase": "0x0000000000000000000000000000000000000000",
  "currentDifficulty": "0x0",
  "currentGasLimit": "0x1c9c380",
  "currentNumber": "0x1",
  "currentTimestamp": "0x65",
  "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "currentBaseFee": "0x1",
  "currentExcessBlobGas": "0x0",
  "parentBaseFee": "0x1",
  "parentBlobGasUsed": "0x0",
  "parentExcessBlobGas": "0x0",
  "parentTimestamp": "0x64",
  "withdrawals": [],
  "blockHashes": {}
}

txs.json (single tx)

[
  {
    "accessList": [],
    "authorizationList": [
      {
        "address": "0x3333333333333333333333333333333333333333",
        "chainId": "0x1",
        "nonce": "0x0",
        "r": "0x0",
        "s": "0x0",
        "signer": "0x4444444444444444444444444444444444444444",
        "v": "0x0",
        "yParity": "0x0"
      }
    ],
    "chainId": "0x1",
    "gas": "0x186a0",
    "hash": "0x5bdaa8f08555964cd65128ef79af2c7bf65596c06670d0aa8c33d9bead7ec5af",
    "input": "0x",
    "maxFeePerGas": "0x1",
    "maxPriorityFeePerGas": "0x0",
    "nonce": "0x0",
    "r": "0x10f7a81f0fa804da890e4f8272e69a880630781bc0bbe4fe34b6e64118c2f2fc",
    "s": "0x700afd6b475fc99513e9212d83ce57fb52240f0b28ac5bf3652bb4c7ebc65011",
    "sender": "0xa94f5374Fce5edBC8E2a8697C15331677e6EbF0B",
    "to": "0x2222222222222222222222222222222222222222",
    "type": "0x4",
    "v": "0x1",
    "value": "0x0"
  }
]

Commands

# evmone
evmone-t8n \
  --state.fork Prague --state.chainid 1 \
  --input.alloc alloc.json --input.env env.json --input.txs txs.json \
  --output.basedir out-evmone --output.result result.json --output.alloc alloc.json

# geth
evm t8n \
  --state.fork Prague --state.chainid 1 \
  --input.alloc alloc.json --input.env env.json --input.txs txs.json \
  --output.basedir out-geth --output.result result.json --output.alloc alloc.json

Observed divergence

  • txRoot, receiptsRoot, gasUsed all match.
  • stateRoot differs:
    • evmone: 0xcd295605f1bb96688cd47da9ac94498b0a5905a474ba9742a2f75194de92329f
    • geth: 0x69b57230cae2a6e923000a238e27e47ab2dbfcbd336bf2c6307beccdfade2ac1

Post-state account diff:

  • evmone creates/mutates 0x4444...4444:
    • nonce = 1
    • code = 0xef0100 || 0x3333...3333
  • geth does not mutate/create that authority account (invalid auth signature is skipped).

Control experiment

If the non-standard JSON field authorizationList[*].signer is removed (same tx otherwise), evmone and geth agree on state root (0x69b572...de2ac1).

This isolates the mismatch to trusting JSON signer instead of recovering authority from (yParity,r,s).

Suggested fix

In test transition logic (test/state/state.cpp / loader path), recover authority from tuple signature (as EIP-7702 specifies) and ignore any externally supplied signer for consensus logic.

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