Skip to content

Commit d4d9b37

Browse files
Implement t8ntool to use for execution-spec-tests (#3603)
* tx: implement strict 7702 validation * vm: update 7702 tx validation * evm: update 7702 [no ci] * tx: add / fix 7702 tests * vm: fix test encoding of authorization lists [no ci] * vm: correctly put authority nonce * vm: add test 7702 extcodehash/extcodesize evm: fix extcodehash/extcodesize for delegated accounts * vm: expand extcode* tests 7702 [no ci] * tx/vm: update tests [no ci] * evm/vm: update opcodes and fix tests 7702 * fix cspell [no ci] * Implement t8n * cleanup * make start.sh executable * remove console.log [no ci] * change readme [no ci] * update t8n [no ci] * add sample (delete me later) * update t8n [no ci] * update t8n to correctly output alloc [no ci] * remove console.logs [no ci] * fix certain values for expected output [no ci] * some t8n fixes regarding output * lint [no ci] * t8n fixes for paris format [no ci] * vm: get params from tx for 7702 [no ci] * t8n console.log dumps [no ci] * vm: 7702 correctly apply the refund [no ci] * vm: 7702: correctly handle self-sponsored txs [no ci] * tx: throw if authorization list is empty * vm: requests do not throw if code is non-existant * evm: ensure correct extcodehash reporting if account is delegated to a non-existing account * update t8n to generate logs [no ci] * t8n correctly output log format [no ci] * change t8ntool start script name [no ci] * putcode log * vm: 7702 ensure delegated accounts are not deleted [no ci] * t8n: output CLrequests [no ci] * t8n: add initKzg / blob tx support * t8n: add blockhash support * t8n: produce allocation for system contracts * vm/buildBlock: take parentHash from headerData if present * t8n: lint [no ci] * vm: exit early if system contract has no code [no ci] * t8n: use mcl instead of noble for bls [no ci] * remove console.logs * evm: 7702 correctly check for gas on delegated code * evm: add verkle gas logic for 7702 * t8n: delete unwanted files [no ci] * vm/tx: fix 7702 tests * tx: throw if 7702-tx has no `to` field * vm/tx: fix 7702 tests * t8n: first cleanup * t8ntool: WIP [no ci] * VM: exit early on non-existing system contracts * VM: exit early on non-existing system contracts * backup [no ci] * t8ntool: do not delete coinbase * correctly exit early on requests * backup * 7702: add delegated account to warm address * 7702: add delegated account to warm address * increase memory limit * export node options * vm: requests do restore system account * vm: requests do restore system account * t8ntool: convert edge cases to correct values * 7702: continue processing once auth ecrecover is invalid * evm/vm: add 7702 delegation constant * vm: fix requests * vm: unduplify 3607 error msg * update wip t8n cleanup [no ci] * add TODO to buildblock * update t8ntool to use createVM [no ci] * update clean version as well [no ci] * fix example * t8ntool attempt to cleanup * t8ntool: cleanup * t8ntool fix import * remove old files * use noble bls t8ntool * add loggers to t8n args * add readme [no ci] * add t8ntool test * fix cspell * add deprecated output.body * make tsc happy * vm: fix 2935 test * Skip t8n tests in browser --------- Co-authored-by: acolytec3 <[email protected]>
1 parent 2cf4ddb commit d4d9b37

21 files changed

+917
-27
lines changed

config/cspell-md.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"language": "en-US",
33
"ignoreRegExpList": ["/0x[0-9A-Fa-f]+/"],
44
"words": [
5+
"t8ntool",
6+
"calldatasize",
57
"Dencun",
68
"Hardfork",
79
"acolytec",

config/cspell-ts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
}
1313
],
1414
"words": [
15+
"t8ntool",
1516
"!Json",
1617
"!Rpc",
1718
"Hardfork",

packages/vm/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
.cachedb
22
benchmarks/*.js
3+
test/t8n/testdata/output/allocTEST.json
4+
test/t8n/testdata/output/resultTEST.json

packages/vm/src/buildBlock.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class BlockBuilder {
9090

9191
this.headerData = {
9292
...opts.headerData,
93-
parentHash: opts.parentBlock.hash(),
93+
parentHash: opts.headerData?.parentHash ?? opts.parentBlock.hash(),
9494
number: opts.headerData?.number ?? opts.parentBlock.header.number + BIGINT_1,
9595
gasLimit: opts.headerData?.gasLimit ?? opts.parentBlock.header.gasLimit,
9696
timestamp: opts.headerData?.timestamp ?? Math.round(Date.now() / 1000),
@@ -213,7 +213,10 @@ export class BlockBuilder {
213213
*/
214214
async addTransaction(
215215
tx: TypedTransaction,
216-
{ skipHardForkValidation }: { skipHardForkValidation?: boolean } = {},
216+
{
217+
skipHardForkValidation,
218+
allowNoBlobs,
219+
}: { skipHardForkValidation?: boolean; allowNoBlobs?: boolean } = {},
217220
) {
218221
this.checkStatus()
219222

@@ -242,7 +245,11 @@ export class BlockBuilder {
242245

243246
// Guard against the case if a tx came into the pool without blobs i.e. network wrapper payload
244247
if (blobTx.blobs === undefined) {
245-
throw new Error('blobs missing for 4844 transaction')
248+
// TODO: verify if we want this, do we want to allow the block builder to accept blob txs without the actual blobs?
249+
// (these must have at least one `blobVersionedHashes`, this is verified at tx-level)
250+
if (allowNoBlobs !== true) {
251+
throw new Error('blobs missing for 4844 transaction')
252+
}
246253
}
247254

248255
if (this.blobGasUsed + BigInt(blobTx.numBlobs()) * blobGasPerBlob > blobGasLimit) {

packages/vm/src/runBlock.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -494,14 +494,12 @@ export async function accumulateParentBlockHash(
494494

495495
// getAccount with historyAddress will throw error as witnesses are not bundled
496496
// but we need to put account so as to query later for slot
497-
try {
498-
if ((await vm.stateManager.getAccount(historyAddress)) === undefined) {
499-
const emptyHistoryAcc = new Account(BigInt(1))
500-
await vm.evm.journal.putAccount(historyAddress, emptyHistoryAcc)
501-
}
502-
} catch (_e) {
503-
const emptyHistoryAcc = new Account(BigInt(1))
504-
await vm.evm.journal.putAccount(historyAddress, emptyHistoryAcc)
497+
const code = await vm.stateManager.getCode(historyAddress)
498+
499+
if (code.length === 0) {
500+
// Exit early, system contract has no code so no storage is written
501+
// TODO: verify with Gabriel that this is fine regarding verkle (should we put an empty account?)
502+
return
505503
}
506504

507505
async function putBlockHash(vm: VM, hash: Uint8Array, number: bigint) {
@@ -536,24 +534,17 @@ export async function accumulateParentBeaconBlockRoot(vm: VM, root: Uint8Array,
536534
const timestampIndex = timestamp % historicalRootsLength
537535
const timestampExtended = timestampIndex + historicalRootsLength
538536

539-
/**
540-
* Note: (by Jochem)
541-
* If we don't do vm (put account if undefined / non-existent), block runner crashes because the beacon root address does not exist
542-
* vm is hence (for me) again a reason why it should /not/ throw if the address does not exist
543-
* All ethereum accounts have empty storage by default
544-
*/
545-
546537
/**
547538
* Note: (by Gabriel)
548539
* Get account will throw an error in stateless execution b/c witnesses are not bundled
549540
* But we do need an account so we are able to put the storage
550541
*/
551-
try {
552-
if ((await vm.stateManager.getAccount(parentBeaconBlockRootAddress)) === undefined) {
553-
await vm.evm.journal.putAccount(parentBeaconBlockRootAddress, new Account())
554-
}
555-
} catch (_) {
556-
await vm.evm.journal.putAccount(parentBeaconBlockRootAddress, new Account())
542+
const code = await vm.stateManager.getCode(parentBeaconBlockRootAddress)
543+
544+
if (code.length === 0) {
545+
// Exit early, system contract has no code so no storage is written
546+
// TODO: verify with Gabriel that this is fine regarding verkle (should we put an empty account?)
547+
return
557548
}
558549

559550
await vm.stateManager.putStorage(

packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ describe('EIP 2935: historical block hashes', () => {
178178
validateConsensus: false,
179179
})
180180
const vm = await createVM({ common: commonGenesis, blockchain })
181+
// Ensure 2935 system code exists
182+
await vm.stateManager.putCode(historyAddress, contract2935Code)
181183
commonGenesis.setHardforkBy({
182184
timestamp: 1,
183185
})
@@ -216,6 +218,8 @@ describe('EIP 2935: historical block hashes', () => {
216218
validateConsensus: false,
217219
})
218220
const vm = await createVM({ common, blockchain })
221+
// Ensure 2935 system code exists
222+
await vm.stateManager.putCode(historyAddress, contract2935Code)
219223
let lastBlock = (await vm.blockchain.getBlock(0)) as Block
220224
for (let i = 1; i <= blocksToBuild; i++) {
221225
lastBlock = await (
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { readFileSync } from 'fs'
2+
import { assert, describe, it } from 'vitest'
3+
4+
import { TransitionTool } from '../../t8n/t8ntool.js'
5+
6+
import type { T8NOptions } from '../../t8n/types.js'
7+
8+
const t8nDir = 'test/t8n/testdata/'
9+
10+
const args: T8NOptions = {
11+
state: {
12+
fork: 'shanghai',
13+
reward: BigInt(0),
14+
chainid: BigInt(1),
15+
},
16+
input: {
17+
alloc: `${t8nDir}input/alloc.json`,
18+
txs: `${t8nDir}input/txs.json`,
19+
env: `${t8nDir}input/env.json`,
20+
},
21+
output: {
22+
basedir: t8nDir,
23+
result: `output/resultTEST.json`,
24+
alloc: `output/allocTEST.json`,
25+
},
26+
log: false,
27+
}
28+
29+
// This test is generated using `execution-spec-tests` commit 88cab2521322191b2ec7ef7d548740c0b0a264fc, running:
30+
// fill -k test_push0_contracts[fork_Shanghai-blockchain_test-key_sstore] --fork Shanghai tests/shanghai/eip3855_push0 --evm-bin=<ETHEREUMJS_T8NTOOL_LAUNCHER.sh>
31+
32+
// The test will run the TransitionTool using the inputs, and then compare if the output matches
33+
34+
describe('test runner config tests', () => {
35+
it('should run t8ntool with inputs and report the expected output', async () => {
36+
await TransitionTool.run(args)
37+
const expectedResult = JSON.parse(readFileSync(`${t8nDir}/output/result.json`).toString())
38+
const expectedAlloc = JSON.parse(readFileSync(`${t8nDir}/output/alloc.json`).toString())
39+
const reportedResult = JSON.parse(readFileSync(`${t8nDir}/output/resultTEST.json`).toString())
40+
const reportedAlloc = JSON.parse(readFileSync(`${t8nDir}/output/allocTEST.json`).toString())
41+
assert.deepStrictEqual(reportedResult, expectedResult, 'result matches expected result')
42+
assert.deepStrictEqual(reportedAlloc, expectedAlloc, 'alloc matches expected alloc')
43+
})
44+
})

packages/vm/test/t8n/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# EVM T8NTool
2+
3+
T8NTool, or Transition Tool, is a tool used to "fill tests" by test runners, for instance <https://github.com/ethereum/execution-spec-tests/>. These files take an input allocation (the pre-state), which contains the accounts (their balances, nonces, storage and code). It also provides an environment, which holds relevant data as current timestamp, previous block hashes, current gas limit, etc. Finally, it also provides a transactions file, which are the transactions to run on top of this pre-state and environment. It outputs the post-state and relevant other artifacts, such as tx receipts and their logs. Test fillers will take this output to generate relevant tests, such as Blockchain tests or State tests, which can then be directly ran in other clients, or using EthereumJS `npm run test:blockchain` or `npm run test:state`.
4+
5+
## Using T8Ntool to fill `execution-spec-tests`
6+
7+
To fill `execution-spec-tests` (or write own tests, and test those against the monorepo), follow these steps:
8+
9+
1. Clone <https://github.com/ethereum/execution-spec-tests/>.
10+
2. Follow the installation steps: <https://github.com/ethereum/execution-spec-tests?tab=readme-ov-file#quick-start>.
11+
12+
To fill tests, such as the EIP-1153 TSTORE/TLOAD tests, run:
13+
14+
- `fill -vv -x --fork Cancun tests/cancun/eip1153_tstore/ --evm-bin=../ethereumjs-monorepo/packages/vm/test/t8n/ethereumjs-t8ntool.sh`
15+
16+
Breaking down these arguments:
17+
18+
- `-vv`: Verbose output
19+
- `-x`: Fail early if any of the test fillers fails
20+
- `--fork`: Fork to fill for
21+
- `--evm-bin`: relative/absolute path to t8ns `ethereumjs-t8ntool.sh`
22+
23+
Optionally, it is also possible to add the `-k <TEST>` option which will only fill this certain test.
24+
25+
## Debugging T8NTool with `execution-spec-tests`
26+
27+
Sometimes it is unclear why a test fails, and one wants more verbose output (from the EthereumJS side). To do so, raw output from `execution-spec-tests` can be dumped by adding the `evm-dump-dir=<DIR>` flag to the `fill` command above. This will output `stdout`, `stderr`, the raw output allocation and the raw results (logs, receipts, etc.) to the `evm-dump-dir`. Additionally, if traces are wanted in `stdout`, add the `--log` flag to `ethereumjs-t8ntool.sh`, i.e. `tsx "$SCRIPT_DIR/launchT8N.ts" "$@" --log`.
28+
29+
This will produce small EVM traces, like this:
30+
31+
```typescript
32+
Processing new transaction...
33+
{
34+
gasLeft: '9976184',
35+
stack: [],
36+
opName: 'CALLDATASIZE',
37+
depth: 0,
38+
address: '0x0000000000000000000000000000000000001000'
39+
}
40+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
if [[ "$1" == "--version" ]]; then
3+
echo "ethereumjs t8n v1"
4+
exit 0
5+
fi
6+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
7+
export NODE_OPTIONS="--max-old-space-size=4096"
8+
tsx "$SCRIPT_DIR/launchT8N.ts" "$@"

packages/vm/test/t8n/helpers.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import yargs from 'yargs'
2+
import { hideBin } from 'yargs/helpers'
3+
4+
import type { T8NOptions } from './types.js'
5+
6+
export function getArguments() {
7+
const argsParsed = yargs(hideBin(process.argv))
8+
.parserConfiguration({
9+
'dot-notation': false,
10+
})
11+
.option('state.fork', {
12+
describe: 'Fork to use',
13+
type: 'string',
14+
demandOption: true,
15+
})
16+
.option('state.chainid', {
17+
describe: 'ChainID to use',
18+
type: 'string',
19+
default: '1',
20+
})
21+
.option('state.reward', {
22+
describe:
23+
'Coinbase reward after running txs. If 0: coinbase account is touched and rewarded 0 wei. If -1, the coinbase account is not touched (default)',
24+
type: 'string',
25+
default: '-1',
26+
})
27+
.option('input.alloc', {
28+
describe: 'Initial state allocation',
29+
type: 'string',
30+
demandOption: true,
31+
})
32+
.option('input.txs', {
33+
describe: 'JSON input of txs to run on top of the initial state allocation',
34+
type: 'string',
35+
demandOption: true,
36+
})
37+
.option('input.env', {
38+
describe: 'Input environment (coinbase, difficulty, etc.)',
39+
type: 'string',
40+
demandOption: true,
41+
})
42+
.option('output.basedir', {
43+
describe: 'Base directory to write output to',
44+
type: 'string',
45+
demandOption: true,
46+
})
47+
.option('output.result', {
48+
describe: 'File to write output results to (relative to `output.basedir`)',
49+
type: 'string',
50+
demandOption: true,
51+
})
52+
.option('output.alloc', {
53+
describe: 'File to write output allocation to (after running the transactions)',
54+
type: 'string',
55+
demandOption: true,
56+
})
57+
.option('output.body', {
58+
deprecate: true,
59+
description: 'File to write transaction RLPs to (currently unused)',
60+
type: 'string',
61+
})
62+
.option('log', {
63+
describe: 'Optionally write light-trace logs to stdout',
64+
type: 'boolean',
65+
default: false,
66+
})
67+
.strict()
68+
.help().argv
69+
70+
const args = argsParsed as any as T8NOptions
71+
72+
args.input = {
73+
alloc: (<any>args)['input.alloc'],
74+
txs: (<any>args)['input.txs'],
75+
env: (<any>args)['input.env'],
76+
}
77+
args.output = {
78+
basedir: (<any>args)['output.basedir'],
79+
result: (<any>args)['output.result'],
80+
alloc: (<any>args)['output.alloc'],
81+
}
82+
args.state = {
83+
fork: (<any>args)['state.fork'],
84+
reward: BigInt((<any>args)['state.reward']),
85+
chainid: BigInt((<any>args)['state.chainid']),
86+
}
87+
88+
return args
89+
}
90+
91+
/**
92+
* This function accepts an `inputs.env` which converts non-hex-prefixed numbers
93+
* to a BigInt value, to avoid errors when converting non-prefixed hex strings to
94+
* numbers
95+
* @param input
96+
* @returns converted input
97+
*/
98+
export function normalizeNumbers(input: any) {
99+
const keys = [
100+
'currentGasLimit',
101+
'currentNumber',
102+
'currentTimestamp',
103+
'currentRandom',
104+
'currentDifficulty',
105+
'currentBaseFee',
106+
'currentBlobGasUsed',
107+
'currentExcessBlobGas',
108+
'parentDifficulty',
109+
'parentTimestamp',
110+
'parentBaseFee',
111+
'parentGasUsed',
112+
'parentGasLimit',
113+
'parentBlobGasUsed',
114+
'parentExcessBlobGas',
115+
]
116+
117+
for (const key of keys) {
118+
const value = input[key]
119+
if (value !== undefined) {
120+
if (value.substring(0, 2) !== '0x') {
121+
input[key] = BigInt(value)
122+
}
123+
}
124+
}
125+
return input
126+
}

0 commit comments

Comments
 (0)