Skip to content

Implement EXECUTE precompile #3865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 32 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
49d3e9d
Update historyStorage address
acolytec3 Feb 4, 2025
a49acd6
Remove execution prestate since unused
acolytec3 Feb 4, 2025
9f86549
Finish generateExecutionWitness function
acolytec3 Feb 5, 2025
b92efed
execution witness fixes
acolytec3 Feb 5, 2025
4454abd
Fix logic in building suffix diffs
acolytec3 Feb 5, 2025
9094e38
Add test
acolytec3 Feb 5, 2025
4592328
spell check
acolytec3 Feb 5, 2025
b1380bc
Merge branch 'master' into verkle-execution-witness
gabrocheleau Feb 9, 2025
2251c2c
Address feedback
acolytec3 Feb 10, 2025
ab92809
Lint
acolytec3 Feb 10, 2025
d4c56ec
Merge remote-tracking branch 'origin/master' into verkle-execution-wi…
acolytec3 Feb 11, 2025
12f5180
the current set of hacks
acolytec3 Feb 4, 2025
8411345
Finish constructing test
acolytec3 Feb 6, 2025
44a447a
Add clarifying comments
acolytec3 Feb 6, 2025
bfb14c5
Convert trace to ssz container
acolytec3 Feb 7, 2025
7d65062
Fix return value computation
acolytec3 Feb 7, 2025
d63fd82
add runCall test
acolytec3 Feb 7, 2025
5db3a51
simplify account setup
acolytec3 Feb 7, 2025
11bdca8
Remove unneeded test data
acolytec3 Feb 11, 2025
e2b5195
Change execution witness to stateWitness
acolytec3 Feb 12, 2025
deb9aa7
Update comments
acolytec3 Feb 12, 2025
f498932
update test title
acolytec3 Feb 20, 2025
e352fee
Merge remote-tracking branch 'origin' into execute-precompile
acolytec3 Mar 4, 2025
08f21c7
Add binary access witness processing to evm and vm
acolytec3 Mar 4, 2025
c2daf3a
Merge remote-tracking branch 'origin/master' into execute-precompile
acolytec3 Mar 5, 2025
5a5aa61
Merge remote-tracking branch 'origin/master' into execute-precompile
acolytec3 Mar 6, 2025
8ed78ca
convert EXECUTE to use binaryTrees
acolytec3 Mar 6, 2025
61ea997
Update shape of trace/witness
acolytec3 Mar 6, 2025
92ab0ef
Add extra account to state to verify state transitions
acolytec3 Mar 6, 2025
87e399a
Merge remote-tracking branch 'origin/master' into execute-precompile
acolytec3 Mar 18, 2025
32b9d73
Merge remote-tracking branch 'origin/master' into execute-precompile
acolytec3 May 1, 2025
e4fbfcb
clean up error messages
acolytec3 May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

1 change: 1 addition & 0 deletions config/cspell-ts.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
}
],
"words": [
"prestateroot",
"immediates",
"unerasable",
"bytelist",
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/eips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,4 +478,7 @@ export const eipsDict: EIPsDict = {
7864: {
minimumHardfork: Hardfork.London,
},
9999: {
minimumHardfork: Hardfork.Cancun,
},
}
8 changes: 5 additions & 3 deletions packages/evm/src/binaryTreeAccessWitness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const WitnessChunkFillCost = BigInt(6200)

// read is a default access event if stem or chunk is present
export type BinaryStemAccessEvent = { write?: boolean }

export type BinaryChunkAccessEvent = BinaryStemAccessEvent & { fill?: boolean }

// Since stem is hashed, it is useful to maintain the reverse relationship
Expand Down Expand Up @@ -399,7 +398,7 @@ export const generateBinaryExecutionWitness = async (
const ew: BinaryTreeExecutionWitness = {
stateDiff: [],
parentStateRoot: bytesToHex(parentStateRoot),
proof: undefined as any, // Binary proofs are not implemented
proof: {},
}

// Generate a map of all stems with their accessed suffixes
Expand All @@ -415,9 +414,12 @@ export const generateBinaryExecutionWitness = async (
}
}

// Get values from the tree for each stem and suffix
// Get values and proofs from the tree for each stem and suffix
for (const stem of accessedSuffixes.keys()) {
tree.root(parentStateRoot)
// Generate proofs for each stem from prestate root
const proof = await tree.createBinaryProof(hexToBytes(stem))
Copy link
Contributor

@g11tech g11tech Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do multiproof instead of per stem proof? (may be later i guess)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, yes. We just don't have multiproofs implemented for binary trees yet. This is as rudimentary as it gets.

ew.proof[stem] = proof
const suffixes = accessedSuffixes.get(stem)
if (suffixes === undefined || suffixes.length === 0) continue
const currentValues = await tree.get(hexToBytes(stem), suffixes)
Expand Down
9 changes: 7 additions & 2 deletions packages/evm/src/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
KECCAK256_NULL,
KECCAK256_RLP,
MAX_INTEGER,
type PrefixedHexString,
bigIntToBytes,
bytesToUnprefixedHex,
createZeroAddress,
Expand Down Expand Up @@ -217,6 +218,8 @@ export class EVM implements EVMInterface {

private _bn254: EVMBN254Interface

private executionBlobs: Map<PrefixedHexString, Uint8Array> // Map of <sha256 hash of trace, trace bytes>

/**
*
* Creates new EVM object
Expand Down Expand Up @@ -251,7 +254,7 @@ export class EVM implements EVMInterface {
const supportedEIPs = [
663, 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3198, 3529, 3540, 3541, 3607, 3651, 3670,
3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6780, 6800,
7002, 7069, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709,
7002, 7069, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709, 7864, 9999,
]

for (const eip of this.common.eips()) {
Expand Down Expand Up @@ -312,6 +315,8 @@ export class EVM implements EVMInterface {
// Additional window check is to prevent vite browser bundling (and potentially other) to break
this.DEBUG =
typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false

this.executionBlobs = new Map()
}

/**
Expand Down Expand Up @@ -954,7 +959,7 @@ export class EVM implements EVMInterface {
createdAddresses: opts.createdAddresses ?? new Set(),
delegatecall: opts.delegatecall,
blobVersionedHashes: opts.blobVersionedHashes,
accessWitness: this.verkleAccessWitness,
accessWitness: this.verkleAccessWitness ?? this.binaryAccessWitness,
})
}

Expand Down
7 changes: 7 additions & 0 deletions packages/evm/src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,11 @@ export const paramsEVM: ParamsDict = {
eofcreateGas: 32000, // Base fee of the EOFCREATE opcode (Same as CREATE/CREATE2)
returncontractGas: 0, // Base fee of the RETURNCONTRACT opcode
},
9999: {
/* Not an actual EIP, but a placeholder for future EXECUTE precompile EIP */
// gasPrices
executeGasCost: 50000,
executeCumulativeGasLimit: 10000000,
executeCumulativeGasTarget: 100000,
},
}
170 changes: 170 additions & 0 deletions packages/evm/src/precompiles/12-execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { binaryTreeFromProof, decodeBinaryNode } from '@ethereumjs/binarytree'
import { StatefulBinaryTreeStateManager } from '@ethereumjs/statemanager'
import {
type PrefixedHexString,
bytesToBigInt,
bytesToHex,
createAddressFromString,
equalsBytes,
hexToBytes,
} from '@ethereumjs/util'
import * as ssz from 'micro-eth-signer/ssz'

import {
BinaryTreeAccessWitness,
type generateBinaryExecutionWitness,
} from '../binaryTreeAccessWitness.ts'
import { createEVM } from '../constructors.ts'
import { EVMErrorResult, OOGResult } from '../evm.ts'

import { gasLimitCheck } from './util.ts'

import { getPrecompileName } from './index.ts'

import type { BinaryNode } from '@ethereumjs/binarytree'
import { EVMError } from '../errors.ts'
import type { EVM } from '../evm.ts'
import type { ExecResult } from '../types.ts'
import type { PrecompileInput } from './types.ts'

// For suffix diffs in state diff
const SuffixDiff = ssz.container({
suffix: ssz.uint8,
currentValue: ssz.bytevector(32),
newValue: ssz.bytevector(32),
})

// For state diff entries
const StateDiff = ssz.container({
stem: ssz.bytevector(31), // The stem as a hex string
suffixDiffs: ssz.list(256, SuffixDiff), // List of suffix diffs
})

// For proof entries
const ProofEntry = ssz.container({
stem: ssz.bytevector(31), // 31-byte vector for the stem
proofData: ssz.list(32, ssz.bytelist(16384)), // List of byte arrays, each up to 16384 bytes
})

// Define the BinaryTreeExecutionWitness container
const BinaryTreeExecutionWitness = ssz.container({
stateDiff: ssz.list(1024, StateDiff), // List of state diffs
parentStateRoot: ssz.bytevector(32), // Parent state root as hex
proof: ssz.list(256, ProofEntry), // List of proof entries with stems and proof data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming these are super low test limits

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, completely arbitrary.

})

const MAX_CALL_DATA_SIZE = 7500000 // Assuming a transaction with all zero bytes fills up an entire block worth of gas
export const traceContainer: ssz.SSZCoder<any> = ssz.container({
txs: ssz.list(
// An ssz list of tx objects that match the `eth_call` tx object format
256,
ssz.container({
to: ssz.bytevector(20),
from: ssz.bytevector(20),
gasLimit: ssz.uint64,
gasPrice: ssz.uint64,
value: ssz.uint64,
data: ssz.bytelist(MAX_CALL_DATA_SIZE),
}),
),
witness: BinaryTreeExecutionWitness,
})

export const stateWitnessJSONToSSZ = (
witness: Awaited<ReturnType<typeof generateBinaryExecutionWitness>>,
) => {
return {
stateDiff: witness.stateDiff.map((diff) => ({
stem: hexToBytes(diff.stem),
suffixDiffs: diff.suffixDiffs.map((suffixDiff) => ({
suffix: suffixDiff.suffix,
currentValue:
suffixDiff.currentValue !== null
? hexToBytes(suffixDiff.currentValue)
: new Uint8Array(32),
newValue:
suffixDiff.newValue !== null ? hexToBytes(suffixDiff.newValue) : new Uint8Array(32),
})),
})),
parentStateRoot: hexToBytes(witness.parentStateRoot),
proof: Object.entries(witness.proof).map(([stem, proof]) => ({
stem: hexToBytes(stem as PrefixedHexString),
proofData: proof,
})),
}
}

export async function precompile12(opts: PrecompileInput): Promise<ExecResult> {
const pName = getPrecompileName('12')
const data = opts.data
const evm = opts._EVM as EVM
const gasUsed = opts.common.param('executeGasCost')
if (!gasLimitCheck(opts, gasUsed, pName)) {
return OOGResult(opts.gasLimit)
}

Check warning on line 104 in packages/evm/src/precompiles/12-execute.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/precompiles/12-execute.ts#L103-L104

Added lines #L103 - L104 were not covered by tests
if (data.length !== 128) {
return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUT_LENGTH), opts.gasLimit)
}

Check warning on line 107 in packages/evm/src/precompiles/12-execute.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/precompiles/12-execute.ts#L106-L107

Added lines #L106 - L107 were not covered by tests
const _preStateRoot = data.subarray(0, 32) // prestateroot for L2 state
const postStateRoot = data.subarray(32, 64) // post state root for L2 state
const traceBlob = evm['executionBlobs'].get(bytesToHex(data.subarray(64, 96))) // reference to state access and transactions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this is like versioned hash? only 1 traceblob is supported for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this is just a hack to reference the blob of data needed for re-executing the transactions until I figured out a clean way to access the data in an actual blob attached to the tx.

if (traceBlob === undefined) {
opts._debug?.(`${pName} error - trace not found`)
return EVMErrorResult(new EVMError(EVMError.errorMessages.REVERT), opts.gasLimit)
}

Check warning on line 114 in packages/evm/src/precompiles/12-execute.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/precompiles/12-execute.ts#L112-L114

Added lines #L112 - L114 were not covered by tests
const decodedTrace = traceContainer.decode(traceBlob)
if (decodedTrace.txs === undefined || decodedTrace.witness === undefined) {
opts._debug?.(`${pName} error - trace is invalid`)
return EVMErrorResult(new EVMError(EVMError.errorMessages.REVERT), opts.gasLimit)
}

Check warning on line 119 in packages/evm/src/precompiles/12-execute.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/precompiles/12-execute.ts#L117-L119

Added lines #L117 - L119 were not covered by tests
const executeGasUsed = bytesToBigInt(data.subarray(96))

// Populate the L2 state trie with the prestate

const witness = decodedTrace.witness
const tree = await binaryTreeFromProof(witness.proof[0].proofData)
for (const proof of witness.proof.slice(1)) {
const putStack: [Uint8Array, BinaryNode][] = proof.proofData.map((bytes: Uint8Array) => {
const node = decodeBinaryNode(bytes)
return [tree['merkelize'](node), node]
})
await tree.saveStack(putStack)
}

let executionResult = true
const stateManager = new StatefulBinaryTreeStateManager({ common: opts.common, tree })
const l2EVM = await createEVM({ stateManager, common: opts.common })
l2EVM.binaryAccessWitness = new BinaryTreeAccessWitness({
hashFunction: tree['_opts'].hashFunction,
})
l2EVM.systemBinaryAccessWitness = new BinaryTreeAccessWitness({
hashFunction: tree['_opts'].hashFunction,
})
let computedGasUsed = 0n
// Run each transaction in the trace
for (const tx of decodedTrace.txs) {
const res = await l2EVM.runCall({
to: createAddressFromString(bytesToHex(tx.to)),
caller: createAddressFromString(bytesToHex(tx.from)),
gasLimit: BigInt(tx.gasLimit),
gasPrice: BigInt(tx.gasPrice),
value: BigInt(tx.value),
data: tx.data !== undefined ? tx.data : undefined,
})
computedGasUsed += res.execResult.executionGasUsed
}
if (computedGasUsed !== executeGasUsed) {
opts._debug?.(`${pName} gas used mismatch: ${computedGasUsed} !== ${executeGasUsed}`)
executionResult = false
}

Check warning on line 159 in packages/evm/src/precompiles/12-execute.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/precompiles/12-execute.ts#L157-L159

Added lines #L157 - L159 were not covered by tests
if (!equalsBytes(postStateRoot, tree.root())) {
opts._debug?.(`${pName} post state root mismatch`)
executionResult = false
}

Check warning on line 163 in packages/evm/src/precompiles/12-execute.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/precompiles/12-execute.ts#L161-L163

Added lines #L161 - L163 were not covered by tests
opts._debug?.(`${pName} trace executed successfully=${executionResult}`)
const returnValue = executionResult ? new Uint8Array(1).fill(1) : new Uint8Array(1).fill(0)
return {
executionGasUsed: gasUsed,
returnValue,
}
}
11 changes: 11 additions & 0 deletions packages/evm/src/precompiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { precompile08 } from './08-bn254-pairing.ts'
import { precompile09 } from './09-blake2f.ts'
import { precompile10 } from './10-bls12-map-fp-to-g1.ts'
import { precompile11 } from './11-bls12-map-fp2-to-g2.ts'
import { precompile12 } from './12-execute.ts'
import { MCLBLS, NobleBLS } from './bls12_381/index.ts'
import { NobleBN254, RustBN254 } from './bn254/index.ts'

Expand Down Expand Up @@ -213,6 +214,15 @@ const precompileEntries: PrecompileEntry[] = [
precompile: precompile11,
name: 'BLS12_MAP_FP_TO_G2 (0x11)',
},
{
address: BYTES_19 + '12',
check: {
type: PrecompileAvailabilityCheck.EIP,
param: 9999,
},
precompile: precompile12,
name: 'EXECUTE (0x12)',
},
]

const precompiles: Precompiles = {
Expand All @@ -233,6 +243,7 @@ const precompiles: Precompiles = {
[BYTES_19 + '0f']: precompile0f,
[BYTES_19 + '10']: precompile10,
[BYTES_19 + '11']: precompile11,
[BYTES_19 + '12']: precompile12,
}

type DeletePrecompile = {
Expand Down
Loading
Loading