Skip to content

Commit

Permalink
Implement t8ntool to use for execution-spec-tests (#3603)
Browse files Browse the repository at this point in the history
* 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 <17355484+acolytec3@users.noreply.github.com>
  • Loading branch information
jochem-brouwer and acolytec3 authored Sep 14, 2024
1 parent 2cf4ddb commit d4d9b37
Show file tree
Hide file tree
Showing 21 changed files with 917 additions and 27 deletions.
2 changes: 2 additions & 0 deletions config/cspell-md.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"language": "en-US",
"ignoreRegExpList": ["/0x[0-9A-Fa-f]+/"],
"words": [
"t8ntool",
"calldatasize",
"Dencun",
"Hardfork",
"acolytec",
Expand Down
1 change: 1 addition & 0 deletions config/cspell-ts.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
}
],
"words": [
"t8ntool",
"!Json",
"!Rpc",
"Hardfork",
Expand Down
2 changes: 2 additions & 0 deletions packages/vm/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.cachedb
benchmarks/*.js
test/t8n/testdata/output/allocTEST.json
test/t8n/testdata/output/resultTEST.json
13 changes: 10 additions & 3 deletions packages/vm/src/buildBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class BlockBuilder {

this.headerData = {
...opts.headerData,
parentHash: opts.parentBlock.hash(),
parentHash: opts.headerData?.parentHash ?? opts.parentBlock.hash(),
number: opts.headerData?.number ?? opts.parentBlock.header.number + BIGINT_1,
gasLimit: opts.headerData?.gasLimit ?? opts.parentBlock.header.gasLimit,
timestamp: opts.headerData?.timestamp ?? Math.round(Date.now() / 1000),
Expand Down Expand Up @@ -213,7 +213,10 @@ export class BlockBuilder {
*/
async addTransaction(
tx: TypedTransaction,
{ skipHardForkValidation }: { skipHardForkValidation?: boolean } = {},
{
skipHardForkValidation,
allowNoBlobs,
}: { skipHardForkValidation?: boolean; allowNoBlobs?: boolean } = {},
) {
this.checkStatus()

Expand Down Expand Up @@ -242,7 +245,11 @@ export class BlockBuilder {

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

if (this.blobGasUsed + BigInt(blobTx.numBlobs()) * blobGasPerBlob > blobGasLimit) {
Expand Down
33 changes: 12 additions & 21 deletions packages/vm/src/runBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,14 +494,12 @@ export async function accumulateParentBlockHash(

// getAccount with historyAddress will throw error as witnesses are not bundled
// but we need to put account so as to query later for slot
try {
if ((await vm.stateManager.getAccount(historyAddress)) === undefined) {
const emptyHistoryAcc = new Account(BigInt(1))
await vm.evm.journal.putAccount(historyAddress, emptyHistoryAcc)
}
} catch (_e) {
const emptyHistoryAcc = new Account(BigInt(1))
await vm.evm.journal.putAccount(historyAddress, emptyHistoryAcc)
const code = await vm.stateManager.getCode(historyAddress)

if (code.length === 0) {
// Exit early, system contract has no code so no storage is written
// TODO: verify with Gabriel that this is fine regarding verkle (should we put an empty account?)
return
}

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

/**
* Note: (by Jochem)
* If we don't do vm (put account if undefined / non-existent), block runner crashes because the beacon root address does not exist
* vm is hence (for me) again a reason why it should /not/ throw if the address does not exist
* All ethereum accounts have empty storage by default
*/

/**
* Note: (by Gabriel)
* Get account will throw an error in stateless execution b/c witnesses are not bundled
* But we do need an account so we are able to put the storage
*/
try {
if ((await vm.stateManager.getAccount(parentBeaconBlockRootAddress)) === undefined) {
await vm.evm.journal.putAccount(parentBeaconBlockRootAddress, new Account())
}
} catch (_) {
await vm.evm.journal.putAccount(parentBeaconBlockRootAddress, new Account())
const code = await vm.stateManager.getCode(parentBeaconBlockRootAddress)

if (code.length === 0) {
// Exit early, system contract has no code so no storage is written
// TODO: verify with Gabriel that this is fine regarding verkle (should we put an empty account?)
return
}

await vm.stateManager.putStorage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ describe('EIP 2935: historical block hashes', () => {
validateConsensus: false,
})
const vm = await createVM({ common: commonGenesis, blockchain })
// Ensure 2935 system code exists
await vm.stateManager.putCode(historyAddress, contract2935Code)
commonGenesis.setHardforkBy({
timestamp: 1,
})
Expand Down Expand Up @@ -216,6 +218,8 @@ describe('EIP 2935: historical block hashes', () => {
validateConsensus: false,
})
const vm = await createVM({ common, blockchain })
// Ensure 2935 system code exists
await vm.stateManager.putCode(historyAddress, contract2935Code)
let lastBlock = (await vm.blockchain.getBlock(0)) as Block
for (let i = 1; i <= blocksToBuild; i++) {
lastBlock = await (
Expand Down
44 changes: 44 additions & 0 deletions packages/vm/test/api/t8ntool/t8ntool.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { readFileSync } from 'fs'
import { assert, describe, it } from 'vitest'

import { TransitionTool } from '../../t8n/t8ntool.js'

import type { T8NOptions } from '../../t8n/types.js'

const t8nDir = 'test/t8n/testdata/'

const args: T8NOptions = {
state: {
fork: 'shanghai',
reward: BigInt(0),
chainid: BigInt(1),
},
input: {
alloc: `${t8nDir}input/alloc.json`,
txs: `${t8nDir}input/txs.json`,
env: `${t8nDir}input/env.json`,
},
output: {
basedir: t8nDir,
result: `output/resultTEST.json`,
alloc: `output/allocTEST.json`,
},
log: false,
}

// This test is generated using `execution-spec-tests` commit 88cab2521322191b2ec7ef7d548740c0b0a264fc, running:
// fill -k test_push0_contracts[fork_Shanghai-blockchain_test-key_sstore] --fork Shanghai tests/shanghai/eip3855_push0 --evm-bin=<ETHEREUMJS_T8NTOOL_LAUNCHER.sh>

// The test will run the TransitionTool using the inputs, and then compare if the output matches

describe('test runner config tests', () => {
it('should run t8ntool with inputs and report the expected output', async () => {
await TransitionTool.run(args)
const expectedResult = JSON.parse(readFileSync(`${t8nDir}/output/result.json`).toString())
const expectedAlloc = JSON.parse(readFileSync(`${t8nDir}/output/alloc.json`).toString())
const reportedResult = JSON.parse(readFileSync(`${t8nDir}/output/resultTEST.json`).toString())
const reportedAlloc = JSON.parse(readFileSync(`${t8nDir}/output/allocTEST.json`).toString())
assert.deepStrictEqual(reportedResult, expectedResult, 'result matches expected result')
assert.deepStrictEqual(reportedAlloc, expectedAlloc, 'alloc matches expected alloc')
})
})
40 changes: 40 additions & 0 deletions packages/vm/test/t8n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# EVM T8NTool

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`.

## Using T8Ntool to fill `execution-spec-tests`

To fill `execution-spec-tests` (or write own tests, and test those against the monorepo), follow these steps:

1. Clone <https://github.com/ethereum/execution-spec-tests/>.
2. Follow the installation steps: <https://github.com/ethereum/execution-spec-tests?tab=readme-ov-file#quick-start>.

To fill tests, such as the EIP-1153 TSTORE/TLOAD tests, run:

- `fill -vv -x --fork Cancun tests/cancun/eip1153_tstore/ --evm-bin=../ethereumjs-monorepo/packages/vm/test/t8n/ethereumjs-t8ntool.sh`

Breaking down these arguments:

- `-vv`: Verbose output
- `-x`: Fail early if any of the test fillers fails
- `--fork`: Fork to fill for
- `--evm-bin`: relative/absolute path to t8ns `ethereumjs-t8ntool.sh`

Optionally, it is also possible to add the `-k <TEST>` option which will only fill this certain test.

## Debugging T8NTool with `execution-spec-tests`

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`.

This will produce small EVM traces, like this:

```typescript
Processing new transaction...
{
gasLeft: '9976184',
stack: [],
opName: 'CALLDATASIZE',
depth: 0,
address: '0x0000000000000000000000000000000000001000'
}
```
8 changes: 8 additions & 0 deletions packages/vm/test/t8n/ethereumjs-t8ntool.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash
if [[ "$1" == "--version" ]]; then
echo "ethereumjs t8n v1"
exit 0
fi
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
export NODE_OPTIONS="--max-old-space-size=4096"
tsx "$SCRIPT_DIR/launchT8N.ts" "$@"
126 changes: 126 additions & 0 deletions packages/vm/test/t8n/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'

import type { T8NOptions } from './types.js'

export function getArguments() {
const argsParsed = yargs(hideBin(process.argv))
.parserConfiguration({
'dot-notation': false,
})
.option('state.fork', {
describe: 'Fork to use',
type: 'string',
demandOption: true,
})
.option('state.chainid', {
describe: 'ChainID to use',
type: 'string',
default: '1',
})
.option('state.reward', {
describe:
'Coinbase reward after running txs. If 0: coinbase account is touched and rewarded 0 wei. If -1, the coinbase account is not touched (default)',
type: 'string',
default: '-1',
})
.option('input.alloc', {
describe: 'Initial state allocation',
type: 'string',
demandOption: true,
})
.option('input.txs', {
describe: 'JSON input of txs to run on top of the initial state allocation',
type: 'string',
demandOption: true,
})
.option('input.env', {
describe: 'Input environment (coinbase, difficulty, etc.)',
type: 'string',
demandOption: true,
})
.option('output.basedir', {
describe: 'Base directory to write output to',
type: 'string',
demandOption: true,
})
.option('output.result', {
describe: 'File to write output results to (relative to `output.basedir`)',
type: 'string',
demandOption: true,
})
.option('output.alloc', {
describe: 'File to write output allocation to (after running the transactions)',
type: 'string',
demandOption: true,
})
.option('output.body', {
deprecate: true,
description: 'File to write transaction RLPs to (currently unused)',
type: 'string',
})
.option('log', {
describe: 'Optionally write light-trace logs to stdout',
type: 'boolean',
default: false,
})
.strict()
.help().argv

const args = argsParsed as any as T8NOptions

args.input = {
alloc: (<any>args)['input.alloc'],
txs: (<any>args)['input.txs'],
env: (<any>args)['input.env'],
}
args.output = {
basedir: (<any>args)['output.basedir'],
result: (<any>args)['output.result'],
alloc: (<any>args)['output.alloc'],
}
args.state = {
fork: (<any>args)['state.fork'],
reward: BigInt((<any>args)['state.reward']),
chainid: BigInt((<any>args)['state.chainid']),
}

return args
}

/**
* This function accepts an `inputs.env` which converts non-hex-prefixed numbers
* to a BigInt value, to avoid errors when converting non-prefixed hex strings to
* numbers
* @param input
* @returns converted input
*/
export function normalizeNumbers(input: any) {
const keys = [
'currentGasLimit',
'currentNumber',
'currentTimestamp',
'currentRandom',
'currentDifficulty',
'currentBaseFee',
'currentBlobGasUsed',
'currentExcessBlobGas',
'parentDifficulty',
'parentTimestamp',
'parentBaseFee',
'parentGasUsed',
'parentGasLimit',
'parentBlobGasUsed',
'parentExcessBlobGas',
]

for (const key of keys) {
const value = input[key]
if (value !== undefined) {
if (value.substring(0, 2) !== '0x') {
input[key] = BigInt(value)
}
}
}
return input
}
4 changes: 4 additions & 0 deletions packages/vm/test/t8n/launchT8N.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { getArguments } from './helpers.js'
import { TransitionTool } from './t8ntool.js'

await TransitionTool.run(getArguments())
Loading

0 comments on commit d4d9b37

Please sign in to comment.