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.
Summary
evmone-t8naccepts a forgedauthorizationList[*].signerfield 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 JSONsigneris present.test/state/state.cpp:if (!auth.signer.has_value()) continue;*auth.signerauthorizationList[*].signerfrom input.test/utils/statetest_loader.cppThis allows unauthorized delegation when JSON injects
signerbut(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.jsonenv.jsontxs.json(contains one set-code tx with invalid auth tuple signature and injectedsigner)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
Observed divergence
txRoot,receiptsRoot,gasUsedall match.stateRootdiffers:0xcd295605f1bb96688cd47da9ac94498b0a5905a474ba9742a2f75194de92329f0x69b57230cae2a6e923000a238e27e47ab2dbfcbd336bf2c6307beccdfade2ac1Post-state account diff:
0x4444...4444:nonce = 1code = 0xef0100 || 0x3333...3333Control experiment
If the non-standard JSON field
authorizationList[*].signeris removed (same tx otherwise), evmone and geth agree on state root (0x69b572...de2ac1).This isolates the mismatch to trusting JSON
signerinstead of recovering authority from(yParity,r,s).Suggested fix
In test transition logic (
test/state/state.cpp/ loader path), recoverauthorityfrom tuple signature (as EIP-7702 specifies) and ignore any externally suppliedsignerfor consensus logic.