diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 58172ab353..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -20 \ No newline at end of file diff --git a/config/cspell-ts.json b/config/cspell-ts.json index e6c0116948..5157416ebc 100644 --- a/config/cspell-ts.json +++ b/config/cspell-ts.json @@ -24,6 +24,7 @@ } ], "words": [ + "prestateroot", "immediates", "unerasable", "bytelist", diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 58bd81db91..42588413ec 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -478,4 +478,7 @@ export const eipsDict: EIPsDict = { 7864: { minimumHardfork: Hardfork.London, }, + 9999: { + minimumHardfork: Hardfork.Cancun, + }, } diff --git a/packages/evm/src/binaryTreeAccessWitness.ts b/packages/evm/src/binaryTreeAccessWitness.ts index 8bde9f75a3..ad51f6d40b 100644 --- a/packages/evm/src/binaryTreeAccessWitness.ts +++ b/packages/evm/src/binaryTreeAccessWitness.ts @@ -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 @@ -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 @@ -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)) + ew.proof[stem] = proof const suffixes = accessedSuffixes.get(stem) if (suffixes === undefined || suffixes.length === 0) continue const currentValues = await tree.get(hexToBytes(stem), suffixes) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 5e157b9617..82d6f6da3e 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -8,6 +8,7 @@ import { KECCAK256_NULL, KECCAK256_RLP, MAX_INTEGER, + type PrefixedHexString, bigIntToBytes, bytesToUnprefixedHex, createZeroAddress, @@ -217,6 +218,8 @@ export class EVM implements EVMInterface { private _bn254: EVMBN254Interface + private executionBlobs: Map // Map of + /** * * Creates new EVM object @@ -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()) { @@ -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() } /** @@ -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, }) } diff --git a/packages/evm/src/params.ts b/packages/evm/src/params.ts index 4b28e728ff..7ac9f18ab4 100644 --- a/packages/evm/src/params.ts +++ b/packages/evm/src/params.ts @@ -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, + }, } diff --git a/packages/evm/src/precompiles/12-execute.ts b/packages/evm/src/precompiles/12-execute.ts new file mode 100644 index 0000000000..568a93dbe3 --- /dev/null +++ b/packages/evm/src/precompiles/12-execute.ts @@ -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 +}) + +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 = 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>, +) => { + 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 { + 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) + } + if (data.length !== 128) { + return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUT_LENGTH), opts.gasLimit) + } + 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 + if (traceBlob === undefined) { + opts._debug?.(`${pName} error - trace not found`) + return EVMErrorResult(new EVMError(EVMError.errorMessages.REVERT), opts.gasLimit) + } + 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) + } + 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 + } + if (!equalsBytes(postStateRoot, tree.root())) { + opts._debug?.(`${pName} post state root mismatch`) + executionResult = false + } + opts._debug?.(`${pName} trace executed successfully=${executionResult}`) + const returnValue = executionResult ? new Uint8Array(1).fill(1) : new Uint8Array(1).fill(0) + return { + executionGasUsed: gasUsed, + returnValue, + } +} diff --git a/packages/evm/src/precompiles/index.ts b/packages/evm/src/precompiles/index.ts index 1d2ac3605d..6be9b26459 100644 --- a/packages/evm/src/precompiles/index.ts +++ b/packages/evm/src/precompiles/index.ts @@ -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' @@ -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 = { @@ -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 = { diff --git a/packages/evm/test/precompiles/12-execute.spec.ts b/packages/evm/test/precompiles/12-execute.spec.ts new file mode 100644 index 0000000000..efa965aeb3 --- /dev/null +++ b/packages/evm/test/precompiles/12-execute.spec.ts @@ -0,0 +1,206 @@ +import { createBinaryTree } from '@ethereumjs/binarytree' +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +import { StatefulBinaryTreeStateManager } from '@ethereumjs/statemanager' +import { + bigIntToBytes, + bytesToHex, + concatBytes, + createAccount, + createAddressFromPrivateKey, + createAddressFromString, + hexToBytes, + randomBytes, + setLengthLeft, +} from '@ethereumjs/util' +import { sha256 } from 'ethereum-cryptography/sha256' +import { assert, describe, it } from 'vitest' + +import { + BinaryTreeAccessWitness, + createEVM, + generateBinaryExecutionWitness, + getActivePrecompiles, +} from '../../src/index.ts' +import { stateWitnessJSONToSSZ, traceContainer } from '../../src/precompiles/12-execute.ts' +describe('Precompiles: EXECUTE', () => { + it('should execute a trace', async () => { + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Prague, + eips: [7864, 9999], + }) + // Construct L2 state and transaction + const account = createAccount({ balance: 0xffffffffffffffffffffffffffffffffffffffffn }) + const address = createAddressFromString('0x999aebeac9619be18e0369d9cb8d0393cfb99021') + const receiver = createAddressFromPrivateKey( + hexToBytes('0xaeb51ceb07e4f6761ea6ad9a772d0e4a70367020fd6175b5e271d0d12e37d24d'), + ) + const tx = { + to: receiver.toBytes(), + from: address.toBytes(), + gasLimit: BigInt('0xffffffffff'), + gasPrice: BigInt('0x1'), + value: BigInt('0x1'), + data: new Uint8Array(), + } + const tree = await createBinaryTree() + const stateManager = new StatefulBinaryTreeStateManager({ common, tree }) + await stateManager.putAccount(address, account) + + const preStateRoot = tree.root() + const evm = await createEVM({ stateManager, common }) + + evm.binaryAccessWitness = new BinaryTreeAccessWitness({ + hashFunction: tree['_opts'].hashFunction, + }) + evm.systemBinaryAccessWitness = new BinaryTreeAccessWitness({ + hashFunction: tree['_opts'].hashFunction, + }) + const res = await evm.runCall({ + to: receiver, + caller: address, + gasLimit: tx.gasLimit, + gasPrice: tx.gasPrice, + value: tx.value, + }) + const executionGasUsed = res.execResult.executionGasUsed + const postStateRoot = tree.root() + const witness = await generateBinaryExecutionWitness( + stateManager, + evm.binaryAccessWitness, + preStateRoot, + ) + + // End of L2 state construction + + // Create a trace + const trace = { + witness: stateWitnessJSONToSSZ(witness), + txs: [tx], + } + + const traceBytes = traceContainer.encode(trace) + + // We use the sha256 hash of the serialized trace as a reference. This is standing in for the versionedHash that we should use + // once we have the trace properly converted to an Ethereum blob. + const hash = bytesToHex(sha256(traceBytes)) + evm['executionBlobs'].set(hash, traceBytes) + + const addressStr = '0000000000000000000000000000000000000012' + const EXECUTE = getActivePrecompiles(common).get(addressStr)! + const input = concatBytes( + preStateRoot, + postStateRoot, + hexToBytes(hash), + setLengthLeft(bigIntToBytes(executionGasUsed), 32), + ) + + const result = await EXECUTE({ + data: input, + gasLimit: 1000000000n, + common, + _EVM: evm, + }) + assert.equal(result.returnValue[0], 1) + }) +}) + +describe('runCall', () => { + it('should execute runCall successfully', async () => { + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Prague, + eips: [7864, 9999], + }) + + // Construct L2 state and transaction + const account = createAccount({ balance: 0xffffffffffffffffffffffffffffffffffffffffn }) + const address = createAddressFromString('0x999aebeac9619be18e0369d9cb8d0393cfb99021') + const receiver = createAddressFromPrivateKey( + hexToBytes('0xaeb51ceb07e4f6761ea6ad9a772d0e4a70367020fd6175b5e271d0d12e37d24d'), + ) + const l2Tx = { + to: receiver.toBytes(), + from: address.toBytes(), + gasLimit: BigInt('0xffffffffff'), + gasPrice: BigInt('0x1'), + value: BigInt('0x1'), + data: new Uint8Array(), + } + const l2Tree = await createBinaryTree() + const l2StateManager = new StatefulBinaryTreeStateManager({ common, tree: l2Tree }) + await l2StateManager.putAccount(address, account) + + // Add a random account to ensure that proof is providing enough inner nodes to validate state transition + await l2StateManager.putAccount( + createAddressFromPrivateKey(randomBytes(32)), + createAccount({ balance: 0x1n }), + ) + const preStateRoot = l2Tree.root() + const l2EVM = await createEVM({ stateManager: l2StateManager, common }) + + l2EVM.binaryAccessWitness = new BinaryTreeAccessWitness({ + hashFunction: l2Tree['_opts'].hashFunction, + }) + l2EVM.systemBinaryAccessWitness = new BinaryTreeAccessWitness({ + hashFunction: l2Tree['_opts'].hashFunction, + }) + const res = await l2EVM.runCall({ + to: receiver, + caller: address, + gasLimit: l2Tx.gasLimit, + gasPrice: l2Tx.gasPrice, + value: l2Tx.value, + }) + const executionGasUsed = res.execResult.executionGasUsed + const postStateRoot = l2Tree.root() + const stateWitness = await generateBinaryExecutionWitness( + l2StateManager, + l2EVM.binaryAccessWitness, + preStateRoot, + ) + // End of L2 state construction + + // Create mainnet state and EVM + const tree = await createBinaryTree() + const stateManager = new StatefulBinaryTreeStateManager({ common, tree }) + const evm = await createEVM({ stateManager, common }) + + evm.binaryAccessWitness = new BinaryTreeAccessWitness({ + hashFunction: tree['_opts'].hashFunction, + }) + evm.systemBinaryAccessWitness = new BinaryTreeAccessWitness({ + hashFunction: tree['_opts'].hashFunction, + }) + + // Create a trace + const trace = { + witness: stateWitnessJSONToSSZ(stateWitness), + txs: [l2Tx], + } + + const traceBytes = traceContainer.encode(trace) + + // We use the sha256 hash of the serialized trace as a reference. This is standing in for the versionedHash that we should use + // once we have the trace properly converted to an Ethereum blob. + const hash = bytesToHex(sha256(traceBytes)) + evm['executionBlobs'].set(hash, traceBytes) + + const precompileAddrStr = '0x0000000000000000000000000000000000000012' + + const input = concatBytes( + preStateRoot, + postStateRoot, + hexToBytes(hash), + setLengthLeft(bigIntToBytes(executionGasUsed), 32), + ) + + const mainnetTx = { + to: createAddressFromString(precompileAddrStr), + data: input, + } + + const res2 = await evm.runCall({ ...mainnetTx, skipBalance: true }) + assert.equal(res2.execResult.returnValue[0], 1) + }) +}) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index c2299dc349..23a07925e3 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -556,7 +556,7 @@ export async function accumulateParentBlockHash( 'Cannot call `accumulateParentBlockHash`: EIP 2935 is not active', ) } - const historyAddress = new Address(bigIntToAddressBytes(vm.common.param('historyStorageAddress'))) + const historyAddress = new Address(bigIntToAddressBytes(vm.common.param('systemAddress'))) const historyServeWindow = vm.common.param('historyServeWindow') // getAccount with historyAddress will throw error as witnesses are not bundled