From 454a735b715e953a528a2d1adbc930d9fa0cb488 Mon Sep 17 00:00:00 2001 From: jangko Date: Mon, 17 Mar 2025 13:33:35 +0700 Subject: [PATCH] Osaka: Mega EOF --- execution_chain/constants.nim | 4 + execution_chain/core/eof/eip4750.nim | 195 ++++++++ execution_chain/core/eof/eip5450.nim | 100 ++++ execution_chain/core/eof/eip5450_table.nim | 201 ++++++++ execution_chain/core/eof/eof_parser.nim | 138 ++++++ execution_chain/core/eof/eof_types.nim | 41 ++ execution_chain/core/eof/eof_utils.nim | 121 +++++ execution_chain/core/validate.nim | 22 +- execution_chain/evm/interpreter/op_codes.nim | 32 +- execution_chain/transaction/call_evm.nim | 6 + execution_chain/transaction/call_types.nim | 12 +- tests/test_eof/test_eip3540.nim | 50 ++ tests/test_eof/test_eip3670.nim | 246 +++++++++ tests/test_eof/test_eip4200.nim | 437 ++++++++++++++++ tests/test_eof/test_eip4750.nim | 495 +++++++++++++++++++ tests/test_eof/test_eip5450.nim | 176 +++++++ tests/test_eof/test_eof1_validation.nim | 83 ++++ 17 files changed, 2350 insertions(+), 9 deletions(-) create mode 100644 execution_chain/core/eof/eip4750.nim create mode 100644 execution_chain/core/eof/eip5450.nim create mode 100644 execution_chain/core/eof/eip5450_table.nim create mode 100644 execution_chain/core/eof/eof_parser.nim create mode 100644 execution_chain/core/eof/eof_types.nim create mode 100644 execution_chain/core/eof/eof_utils.nim create mode 100644 tests/test_eof/test_eip3540.nim create mode 100644 tests/test_eof/test_eip3670.nim create mode 100644 tests/test_eof/test_eip4200.nim create mode 100644 tests/test_eof/test_eip4750.nim create mode 100644 tests/test_eof/test_eip5450.nim create mode 100644 tests/test_eof/test_eof1_validation.nim diff --git a/execution_chain/constants.nim b/execution_chain/constants.nim index ed72024808..c450906865 100644 --- a/execution_chain/constants.nim +++ b/execution_chain/constants.nim @@ -107,4 +107,8 @@ const HISTORY_STORAGE_ADDRESS* = address"0x0000F90827F1C53a10cb7A02335B175320002935" WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS* = address"0x00000961Ef480Eb55e80D19ad83579A64c007002" CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS* = address"0x0000BBdDc7CE488642fb579F8B00f3a590007251" + + # EIP-7873 constants + MAX_INITCODE_COUNT* = 256 + # End diff --git a/execution_chain/core/eof/eip4750.nim b/execution_chain/core/eof/eip4750.nim new file mode 100644 index 0000000000..d361825acf --- /dev/null +++ b/execution_chain/core/eof/eip4750.nim @@ -0,0 +1,195 @@ +# nimbus-execution-client +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [].} + +import + std/[sequtils, intsets], + results, + ./eof_utils + +func validateEof*(code: openArray[byte]): Result[void, string] = + # Check version + if code.len < 3 or code[2] != VERSION: + return err("invalid version") + + # Process section headers + var + pos = 3 + codeSectionSize: seq[int] + dataSectionSize = Opt.none(int) + typeSectionSize = Opt.none(int) + + while true: + # Terminator not found + if pos >= code.len: + return err("no section terminator") + + let sectionId = code[pos] + inc pos + if sectionId == TERMINATOR: + break + + # Disallow unknown sections + if sectionId notin [CODE, DATA, TYPE]: + return err("invalid section id") + + # Data section preceding code section (i.e. code section following data section) + if sectionId == CODE and dataSectionSize.isSome: + return err("data section preceding code section") + + # Code section or data section preceding type section + if sectionId == TYPE and (codeSectionSize.len > 0 or dataSectionSize.isSome): + return err("code or data section preceding type section") + + # Multiple type or data sections + if sectionId == TYPE and typeSectionSize.isSome: + return err("multiple type sections") + if sectionId == DATA and dataSectionSize.isSome: + return err("multiple data sections") + + # Truncated section size + if (pos + 1) >= code.len: + return err("truncated section size") + + let sectionSize = code.parseUint16(pos) + if sectionId == DATA: + dataSectionSize = Opt.some(sectionSize) + elif sectionId == TYPE: + typeSectionSize = Opt.some(sectionSize) + else: + codeSectionSize.add sectionSize + + inc(pos, 2) + + # Empty section + if sectionSize == 0: + return err("empty section") + + # Code section cannot be absent + if codeSectionSize.len == 0: + return err("no code section") + + # Not more than 1024 code sections + if codeSectionSize.len > 1024: + return err("more than 1024 code sections") + + # Type section can be absent only if single code section is present + if typeSectionSize.isNone and codeSectionSize.len != 1: + return err("no obligatory type section") + + # Type section, if present, has size corresponding to number of code sections + if typeSectionSize.isSome and typeSectionSize.value != codeSectionSize.len * 2: + return err("invalid type section size") + + # The entire container must be scanned + if code.len != (pos + typeSectionSize.get(0) + codeSectionSize.foldl(a + b) + dataSectionSize.get(0)): + return err("container size not equal to sum of section sizes") + + # First type section, if present, has 0 inputs and 0 outputs + if typeSectionSize.isSome and (code[pos] != 0 or code[pos + 1] != 0): + return err("invalid type of section 0") + + ok() +#[ +# Raises ValidationException on invalid code +func validateCodeSection*(funcId: int, + code: openArray[byte], + types: openArray[FunctionType] = [ZeroFunctionType]): + Result[void, string] = + # Note that EOF1 already asserts this with the code section requirements + assert code.len > 0 + assert funcId < types.len + + var + pos = 0 + opcode = 0.byte + rjumpdests = initIntSet() + immediates = initIntSet() + + while pos < code.len: + # Ensure the opcode is valid + opcode = code[pos] + inc pos + if opcode notin ValidOpcodes: + return err("undefined instruction") + + var pcPostInstruction = pos + ImmediateSizes[opcode].int + + if opcode == OP_RJUMP or opcode == OP_RJUMPI: + if pos + 2 > code.len: + return err("truncated relative jump offset") + let offset = code.parseInt16(pos) + + let rjumpdest = pos + 2 + offset + if rjumpdest < 0 or rjumpdest >= code.len: + return err("relative jump destination out of bounds") + + rjumpdests.incl(rjumpdest) + + elif opcode == OP_RJUMPV: + if pos + 1 > code.len: + return err("truncated jump table") + let jumpTableSize = code[pos].int + if jumpTableSize == 0: + return err("empty jump table") + + pcPostInstruction = pos + 1 + 2 * jumpTableSize + if pcPostInstruction > code.len: + return err("truncated jump table") + + for offsetPos in countup(pos + 1, pcPostInstruction-1, 2): + let offset = code.parseInt16(offsetPos) + + let rjumpdest = pcPostInstruction + offset + if rjumpdest < 0 or rjumpdest >= code.len: + return err("relative jump destination out of bounds") + + rjumpdests.incl(rjumpdest) + + elif opcode == OP_CALLF: + if pos + 2 > code.len: + return err("truncated CALLF immediate") + let sectionId = code.parseUint16(pos) + + if sectionId >= types.len: + return err("invalid section id") + + elif opcode == OP_JUMPF: + if pos + 2 > code.len: + return err("truncated JUMPF immediate") + let sectionId = code.parseUint16(pos) + + if sectionId >= types.len: + return err("invalid section id") + + if types[sectionId].outputs != types[funcId].outputs: + return err("incompatible function type for JUMPF") + + # Save immediate value positions + for x in pos..= 0 + assert funcId < types.len + assert types[funcId].inputs >= 0 + assert types[funcId].outputs >= 0 + + ?validateCodeSection(funcId, code, types) + + var + stackHeights: Table[int, int] + startStackHeight = types[funcId].inputs + maxStackHeight = startStackHeight + + # queue of instructions to analyze, list of (pos, stackHeight) pairs + worklist = [(0, startStackHeight)].toDeque + + while worklist.len > 0: + var (pos, stackHeight) = worklist.popFirst + while true: + # Assuming code ends with a terminating instruction due to previous validation in validate_code_section() + assert pos < code.len, "code is invalid" + let + op = code[pos] + info = InstrTable[op.Op] + + # Check if stack height (type arity) at given position is the same + # for all control flow paths reaching this position. + if pos in stackHeights: + if stackHeight != stackHeights[pos]: + return err("stack height mismatch for different paths") + else: + break + else: + stackHeights[pos] = stackHeight + + + var + stackHeightRequired = info.stackHeightRequired + stackHeightChange = info.stackHeightChange + + if op == OP_CALLF: + let calledFuncId = code.parseUint16(pos + 1) + # Assuming calledFuncId is valid due to previous validation in validate_code_section() + stackHeightRequired += types[calledFuncId].inputs + stackHeightChange += types[calledFuncId].outputs - types[calledFuncId].inputs + + # Detect stack underflow + if stackHeight < stackHeightRequired: + return err("stack underflow") + + stackHeight += stackHeightChange + maxStackHeight = max(maxStackHeight, stackHeight) + + # Handle jumps + if op == OP_RJUMP: + let offset = code.parseInt16(pos + 1) + pos += info.immediateSize + 1 + offset # pos is valid for validated code. + + elif op == OP_RJUMPI: + let offset = code.parseInt16(pos + 1) + # Save true branch for later and continue to False branch. + worklist.addLast((pos + 3 + offset, stackHeight)) + pos += info.immediateSize + 1 + + elif info.isTerminating: + let expectedHeight = if op == OP_RETF: types[funcId].outputs else: 0 + if stackHeight != expectedHeight: + return err("non-empty stack on terminating instruction") + break + + else: + pos += info.immediateSize + 1 + + + if maxStackHeight >= 1023: + return err("max stack above limit") + + ok(maxStackHeight) diff --git a/execution_chain/core/eof/eip5450_table.nim b/execution_chain/core/eof/eip5450_table.nim new file mode 100644 index 0000000000..3006145605 --- /dev/null +++ b/execution_chain/core/eof/eip5450_table.nim @@ -0,0 +1,201 @@ +# nimbus-execution-client +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [].} + +import + ../../evm/interpreter/codes + +type + InstrInfo* = object + name*: string + immediateSize*: int + isTerminating*: bool + stackHeightRequired*: int + stackHeightChange*: int + +template iinfo(a, b, c, d, e): auto = + InstrInfo( + name: a, + immediateSize: b, + isTerminating: c, + stackHeightRequired: d, + stackHeightChange: e + ) + +const + InstrTable* = + block: + var map: array[Op, InstrInfo] + for x in Op: + map[x] = iinfo("undefined", 0, true, 0, 0) + + map[Stop] = iinfo("STOP", 0, true, 0, 0) + map[Add] = iinfo("ADD", 0, false, 2, -1) + map[Mul] = iinfo("MUL", 0, false, 2, -1) + map[Sub] = iinfo("SUB", 0, false, 2, -1) + map[Div] = iinfo("DIV", 0, false, 2, -1) + map[Sdiv] = iinfo("SDIV", 0, false, 2, -1) + map[Mod] = iinfo("MOD", 0, false, 2, -1) + map[Smod] = iinfo("SMOD", 0, false, 2, -1) + map[Addmod] = iinfo("ADDMOD", 0, false, 3, -2) + map[Mulmod] = iinfo("MULMOD", 0, false, 3, -2) + map[Exp] = iinfo("EXP", 0, false, 2, -1) + map[SignExtend] = iinfo("SIGNEXTEND", 0, false, 2, -1) + + map[Lt] = iinfo("LT", 0, false, 2, -1) + map[Gt] = iinfo("GT", 0, false, 2, -1) + map[Slt] = iinfo("SLT", 0, false, 2, -1) + map[Sgt] = iinfo("SGT", 0, false, 2, -1) + map[Eq] = iinfo("EQ", 0, false, 2, -1) + map[IsZero] = iinfo("ISZERO", 0, false, 1, 0) + map[And] = iinfo("AND", 0, false, 2, -1) + map[Or] = iinfo("OR", 0, false, 2, -1) + map[Xor] = iinfo("XOR", 0, false, 2, -1) + map[Not] = iinfo("NOT", 0, false, 1, 0) + map[Byte] = iinfo("BYTE", 0, false, 2, -1) + map[Shl] = iinfo("SHL", 0, false, 2, -1) + map[Shr] = iinfo("SHR", 0, false, 2, -1) + map[Sar] = iinfo("SAR", 0, false, 2, -1) + + map[Keccak256] = iinfo("KECCAK256", 0, false, 2, -1) + + map[Address] = iinfo("ADDRESS", 0, false, 0, 1) + map[Balance] = iinfo("BALANCE", 0, false, 1, 0) + map[Origin] = iinfo("ORIGIN", 0, false, 0, 1) + map[Caller] = iinfo("CALLER", 0, false, 0, 1) + map[CallCalue] = iinfo("CALLVALUE", 0, false, 0, 1) + map[CallDataLoad] = iinfo("CALLDATALOAD", 0, false, 1, 0) + map[CallDataSize] = iinfo("CALLDATASIZE", 0, false, 0, 1) + map[CallDataCopy] = iinfo("CALLDATACOPY", 0, false, 3, -3) + map[CodeSize] = iinfo("CODESIZE", 0, false, 0, 1) + map[CodeCopy] = iinfo("CODECOPY", 0, false, 3, -3) + map[GasPrice] = iinfo("GASPRICE", 0, false, 0, 1) + map[ExtCodeSize] = iinfo("EXTCODESIZE", 0, false, 1, 0) + map[ExtCodeCopy] = iinfo("EXTCODECOPY", 0, false, 4, -4) + map[ReturnDataSize] = iinfo("RETURNDATASIZE", 0, false, 0, 1) + map[ReturnDataCopy] = iinfo("RETURNDATACOPY", 0, false, 3, -3) + map[ExtCodeHash] = iinfo("EXTCODEHASH", 0, false, 1, 0) + + map[BlockHash] = iinfo("BLOCKHASH", 0, false, 1, 0) + map[CoinBase] = iinfo("COINBASE", 0, false, 0, 1) + map[Timestamp] = iinfo("TIMESTAMP", 0, false, 0, 1) + map[Number] = iinfo("NUMBER", 0, false, 0, 1) + map[Difficulty] = iinfo("PREVRANDAO", 0, false, 0, 1) + map[GasLimit] = iinfo("GASLIMIT", 0, false, 0, 1) + map[ChainIdOp] = iinfo("CHAINID", 0, false, 0, 1) + map[SelfBalance] = iinfo("SELFBALANCE", 0, false, 0, 1) + map[BaseFee] = iinfo("BASEFEE", 0, false, 0, 1) + map[BlobHash] = iinfo("BLobHash", 0, false, 0, 1) + map[BlobBaseFee] = iinfo("BlobBaseFee", 0, false, 0, 1) + + map[Pop] = iinfo("POP", 0, false, 1, -1) + map[Mload] = iinfo("MLOAD", 0, false, 1, 0) + map[Mstore] = iinfo("MSTORE", 0, false, 2, -2) + map[Mstore8] = iinfo("MSTORE8", 0, false, 2, -2) + map[Sload] = iinfo("SLOAD", 0, false, 1, 0) + map[Sstore] = iinfo("SSTORE", 0, false, 2, -2) + map[Jump] = iinfo("JUMP", 0, false, 1, -1) + map[JumpI] = iinfo("JUMPI", 0, false, 2, -2) + map[Pc] = iinfo("PC", 0, false, 0, 1) + map[Msize] = iinfo("MSIZE", 0, false, 0, 1) + map[Gas] = iinfo("GAS", 0, false, 0, 1) + map[JumpDest] = iinfo("JUMPDEST", 0, false, 0, 0) + map[Rjump] = iinfo("RJUMP", 2, false, 0, 0) + map[RJUMPI] = iinfo("RJUMPI", 2, false, 1, -1) + + map[Push0] = iinfo("PUSH0", 0, false, 0, 1) + + map[Push1] = iinfo("PUSH1", 1, false, 0, 1) + map[Push2] = iinfo("PUSH2", 2, false, 0, 1) + map[Push3] = iinfo("PUSH3", 3, false, 0, 1) + map[Push4] = iinfo("PUSH4", 4, false, 0, 1) + map[Push5] = iinfo("PUSH5", 5, false, 0, 1) + map[Push6] = iinfo("PUSH6", 6, false, 0, 1) + map[Push7] = iinfo("PUSH7", 7, false, 0, 1) + map[Push8] = iinfo("PUSH8", 8, false, 0, 1) + map[Push9] = iinfo("PUSH9", 9, false, 0, 1) + map[Push10] = iinfo("PUSH10", 10, false, 0, 1) + map[Push11] = iinfo("PUSH11", 11, false, 0, 1) + map[Push12] = iinfo("PUSH12", 12, false, 0, 1) + map[Push13] = iinfo("PUSH13", 13, false, 0, 1) + map[Push14] = iinfo("PUSH14", 14, false, 0, 1) + map[Push15] = iinfo("PUSH15", 15, false, 0, 1) + map[Push16] = iinfo("PUSH16", 16, false, 0, 1) + map[Push17] = iinfo("PUSH17", 17, false, 0, 1) + map[Push18] = iinfo("PUSH18", 18, false, 0, 1) + map[Push19] = iinfo("PUSH19", 19, false, 0, 1) + map[Push20] = iinfo("PUSH20", 20, false, 0, 1) + map[Push21] = iinfo("PUSH21", 21, false, 0, 1) + map[Push22] = iinfo("PUSH22", 22, false, 0, 1) + map[Push23] = iinfo("PUSH23", 23, false, 0, 1) + map[Push24] = iinfo("PUSH24", 24, false, 0, 1) + map[Push25] = iinfo("PUSH25", 25, false, 0, 1) + map[Push26] = iinfo("PUSH26", 26, false, 0, 1) + map[Push27] = iinfo("PUSH27", 27, false, 0, 1) + map[Push28] = iinfo("PUSH28", 28, false, 0, 1) + map[Push29] = iinfo("PUSH29", 29, false, 0, 1) + map[Push30] = iinfo("PUSH30", 30, false, 0, 1) + map[Push31] = iinfo("PUSH31", 31, false, 0, 1) + map[Push32] = iinfo("PUSH32", 32, false, 0, 1) + + map[Dup1] = iinfo("DUP1", 0, false, 1, 1) + map[Dup2] = iinfo("DUP2", 0, false, 2, 1) + map[Dup3] = iinfo("DUP3", 0, false, 3, 1) + map[Dup4] = iinfo("DUP4", 0, false, 4, 1) + map[Dup5] = iinfo("DUP5", 0, false, 5, 1) + map[Dup6] = iinfo("DUP6", 0, false, 6, 1) + map[Dup7] = iinfo("DUP7", 0, false, 7, 1) + map[Dup8] = iinfo("DUP8", 0, false, 8, 1) + map[Dup9] = iinfo("DUP9", 0, false, 9, 1) + map[Dup10] = iinfo("DUP10", 0, false, 10, 1) + map[Dup11] = iinfo("DUP11", 0, false, 11, 1) + map[Dup12] = iinfo("DUP12", 0, false, 12, 1) + map[Dup13] = iinfo("DUP13", 0, false, 13, 1) + map[Dup14] = iinfo("DUP14", 0, false, 14, 1) + map[Dup15] = iinfo("DUP15", 0, false, 15, 1) + map[Dup16] = iinfo("DUP16", 0, false, 16, 1) + + map[Swap1] = iinfo("SWAP1", 0, false, 2, 0) + map[Swap2] = iinfo("SWAP2", 0, false, 3, 0) + map[Swap3] = iinfo("SWAP3", 0, false, 4, 0) + map[Swap4] = iinfo("SWAP4", 0, false, 5, 0) + map[Swap5] = iinfo("SWAP5", 0, false, 6, 0) + map[Swap6] = iinfo("SWAP6", 0, false, 7, 0) + map[Swap7] = iinfo("SWAP7", 0, false, 8, 0) + map[Swap8] = iinfo("SWAP8", 0, false, 9, 0) + map[Swap9] = iinfo("SWAP9", 0, false, 10, 0) + map[Swap10] = iinfo("SWAP10", 0, false, 11, 0) + map[Swap11] = iinfo("SWAP11", 0, false, 12, 0) + map[Swap12] = iinfo("SWAP12", 0, false, 13, 0) + map[Swap13] = iinfo("SWAP13", 0, false, 14, 0) + map[Swap14] = iinfo("SWAP14", 0, false, 15, 0) + map[Swap15] = iinfo("SWAP15", 0, false, 16, 0) + map[Swap16] = iinfo("SWAP16", 0, false, 17, 0) + + map[Log0] = iinfo("LOG0", 0, false, 2, -2) + map[Log1] = iinfo("LOG1", 0, false, 3, -3) + map[Log2] = iinfo("LOG2", 0, false, 4, -4) + map[Log3] = iinfo("LOG3", 0, false, 5, -5) + map[Log4] = iinfo("LOG4", 0, false, 6, -6) + + map[CREATE] = iinfo("CREATE", 0, false, 3, -2) + map[CALL] = iinfo("CALL", 0, false, 7, -6) + map[CALLCODE] = iinfo("CALLCODE", 0, false, 7, -6) + map[RETURN] = iinfo("RETURN", 0, true, 2, -2) + map[DELEGATECALL] = iinfo("DELEGATECALL", 0, false, 6, -5) + map[CREATE2] = iinfo("CREATE2", 0, false, 4, -3) + map[STATICCALL] = iinfo("STATICCALL", 0, false, 6, -5) + map[CALLF] = iinfo("CALLF", 2, false, 0, 0) + map[RETF] = iinfo("RETF", 0, true, 0, 0) + map[REVERT] = iinfo("REVERT", 0, true, 2, -2) + map[INVALID] = iinfo("INVALID", 0, true, 0, 0) + map[SELFDESTRUCT] = iinfo("SELFDESTRUCT", 0, true, 1, -1) + + map diff --git a/execution_chain/core/eof/eof_parser.nim b/execution_chain/core/eof/eof_parser.nim new file mode 100644 index 0000000000..0256077eba --- /dev/null +++ b/execution_chain/core/eof/eof_parser.nim @@ -0,0 +1,138 @@ +# nimbus-execution-client +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [].} + +import + std/sequtils, + results, + ./eof_utils, + ./eof_types + +func eofParseHeader*(code: openArray[byte]): Result[EOFHeader, string] = + # TODO: the total size of a container must not exceed MAX_INITCODE_SIZE + + if code.len < 15: + return err("header size is less than 15 bytes") + + if code[2] != VERSION: + return err("invalid version") + + var + h: EOFHeader + p = initEOFParser(code.toOpenArray(3, code.len-1)) + + ? p.expectSection(KIND_TYPES) + h.typesSize = p.u16 + if not (h.typesSize >= 4 and h.typesSize <= 0x1000): + return err("types size out of range") + + if h.typesSize mod 4 != 0: + return err("types size must divisible by 4") + + ? p.expectSection(KIND_CODE) + let numCodeSections = p.u16 + if not (numCodeSections >= 1 and numCodeSections <= 0x400): + return err("num code sections out of range") + + if numCodeSections != h.typesSize div 4: + return err("num code sections not equal to fourth of types size") + + ? p.expectLen(numCodeSections * 2 + 4) + + for _ in 0..= 1 and codeSize <= 0xffff): + return err("code size out of range") + h.codeSizes.add codeSize + + let sectionId = p.u8 + var numContainerSections: int + if sectionId == KIND_CONTAINER: + numContainerSections = p.u16 + if not (numContainerSections >= 1 and numContainerSections <= 0x100): + return err("num container sections out of range") + ? p.expectLen(numContainerSections * 2 + 4) + + for _ in 0..= 1 and containerSize <= 0xffff): + return err("container size out of range") + h.containerSizes.add containerSize + else: + dec p.pos + + ? p.expectLen(4) # KIND_DATA(1) + DATA_SIZE(2) + TERMINATOR(1) + ? p.expectSection(KIND_DATA) + h.dataSize = p.u16 + + ? p.expectSection(TERMINATOR) + + let totalLen = if numContainerSections == 0: + 13 + 2*numCodeSections + + h.typesSize + + h.dataSize + + h.codeSizes.foldl(a + b) + else: + 16 + 2*numCodeSections + + h.typesSize + + h.dataSize + + h.codeSizes.foldl(a + b) + + 2*numContainerSections + + h.containerSizes.foldl(a + b) + + if totalLen != code.len: + return err("container size not equal to sum of section sizes") + + ok(move(h)) + +func eofParseBody*(code: openArray[byte], h: EOFHeader): Result[EOFBody, string] = + # Total code length should have been validated by eofParseHeader, + # and we don't have to do it again here. + + let + headerSize = h.size() + numTypes = h.typesSize div 4 + + var + p = initEOFParser(code.toOpenArray(headerSize, code.len-1)) + body: EOFBody + + body.types = newSeqOfCap[EOFType](numTypes) + body.codes = newSeqOfCap[CodeView](h.codeSizes.len) + body.containers = newSeqOfCap[CodeView](h.containerSizes.len) + + for _ in 0.. 0x7F: + return err("Invalid inputs value") + + let outputs = p.u8 + if outputs > 0x80: + return err("Invalid outputs value") + + let maxStackIncrease = p.u16 + if maxStackIncrease > 0x03FF: + return err("Invalid maxStackIncrease value") + + body.types.add EOFType( + inputs: inputs, + outputs: outputs, + maxStackIncrease: maxStackIncrease.uint16 + ) + + for c in h.codeSizes: + body.codes.add p.codeView(c) + + for c in h.containerSizes: + body.containers.add p.codeView(c) + + body.data = p.codeView(p.remainingSize) + + ok(move(body)) diff --git a/execution_chain/core/eof/eof_types.nim b/execution_chain/core/eof/eof_types.nim new file mode 100644 index 0000000000..147cb5a731 --- /dev/null +++ b/execution_chain/core/eof/eof_types.nim @@ -0,0 +1,41 @@ +# nimbus-execution-client +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [].} + +type + CodeView* = ptr UncheckedArray[byte] + + EOFHeader* = object + typesSize*: int + codeSizes*: seq[int] + containerSizes*: seq[int] + dataSize*: int + + EOFType* = object + inputs* : uint8 + outputs*: uint8 + maxStackIncrease*: uint16 + + EOFBody* = object + types*: seq[EOFType] + codes*: seq[CodeView] + containers*: seq[CodeView] + data*: CodeView + +func size*(h: EOFHeader): int = + # 3 bytes = magic, version + # 3 bytes = kind_types, types_size + # 3 + 2n = kind_code, num_code_sections, code_size+ + # 3 + 2n = kind_container, num_container_sections, container_size+ + # 4 bytes = kind_data, data_size, terminator + if h.containerSizes.len > 0: + 3 + 3 + 3 + h.codeSizes.len*2 + 3 + h.containerSizes.len*2 + 4 + else: + 3 + 3 + 3 + h.codeSizes.len*2 + 4 diff --git a/execution_chain/core/eof/eof_utils.nim b/execution_chain/core/eof/eof_utils.nim new file mode 100644 index 0000000000..8a61378098 --- /dev/null +++ b/execution_chain/core/eof/eof_utils.nim @@ -0,0 +1,121 @@ +# nimbus-execution-client +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [].} + +import + stew/bitseqs, + stew/endians2, + stew/ptrops, + results, + ./eof_types + +export + bitseqs + + +#[func setRange*(a: var BitArray, s: Natural, ss: Natural) = + for x in s..ss: + a.setBit x + +func setBit*(a: var BitArray, r: openArray[Natural]) = + for x in r: + a.setBit x + +template contains*(a: BitArray, pos: Natural): bool = + a[pos] + +# The ranges below are as specified in the Yellow Paper. +const + OP_RJUMP* = 0xe0.byte + OP_RJUMPI* = 0xe1.byte + OP_RJUMPV* = 0xe2.byte + OP_CALLF* = 0xe3.byte + OP_RETF* = 0xe4.byte + OP_JUMPF* = 0xe5.byte + OP_EOFCREATE* = 0xec.byte + OP_RETURNCODE*= 0xee.byte + + ValidOpcodes* = + block: + var bits: BitArray[256] + bits.setRange(0x00, 0x0b) + bits.setRange(0x10, 0x1d) + bits.setBit(0x20) + bits.setRange(0x30, 0x3f) + bits.setRange(0x40, 0x4a) + bits.setRange(0x50, 0x55) + bits.setRange(0x58, 0x5d) + bits.setRange(0x60, 0x6f) + bits.setRange(0x70, 0x7f) + bits.setRange(0x80, 0x8f) + bits.setRange(0x90, 0x9f) + bits.setRange(0xa0, 0xa4) + bits.setRange(0xd0, 0xd3) + bits.setRange(0xe6, 0xe8) + bits.setRange(0xec, 0xee) + + + # Note: 0xfe is considered assigned. + + bits +]# + +type + EOFParser* = object + codeView: CodeView + pos*: int + codeLen: int + +const + MAGIC* = [0xEF.byte, 0x00.byte] + VERSION* = 0x01.byte + TERMINATOR* = 0x00.byte + KIND_TYPES* = 0x01.byte + KIND_CODE* = 0x02.byte + KIND_CONTAINER* = 0x03.byte + KIND_DATA* = 0x04.byte + +func initEOFParser*(code: openArray[byte]): EOFParser = + EOFParser( + codeView: cast[CodeView](code[0].addr), + pos: 0, + codeLen: code.len, + ) + +func u16*(p: var EOFParser): int = + result = fromBytesBE(uint16, makeOpenArray(addr p.codeView[p.pos], byte, 2)).int + inc(p.pos, 2) + +func i16*(p: var EOFParser): int = + let val = fromBytesBE(uint16, makeOpenArray(addr p.codeView[p.pos], byte, 2)) + inc(p.pos, 2) + cast[int16](val).int + +func u8*(p: var EOFParser): byte = + result = p.codeView[p.pos] + inc p.pos + +func expectSection*(p: var EOFParser, id: byte): Result[void, string] = + let sectionId = p.u8 + if sectionId != id: + return err("expect section id: " & $(id.int)) + ok() + +func expectLen*(p: EOFParser, len: int): Result[void, string] = + if p.pos + len >= p.codeLen: + return err("no section terminator") + ok() + +func codeView*(p: var EOFParser, len: int): CodeView = + result = cast[CodeView](p.codeView[p.pos].addr) + inc(p.pos, len) + +func remainingSize*(p: EOFParser): int = + p.codeLen - p.pos diff --git a/execution_chain/core/validate.nim b/execution_chain/core/validate.nim index c14fb63cc4..0754554c2b 100644 --- a/execution_chain/core/validate.nim +++ b/execution_chain/core/validate.nim @@ -224,6 +224,9 @@ func validateTxBasic*( if tx.txType == TxEip7702 and fork < FkPrague: return err("invalid tx: Eip7702 Tx type detected before Prague") + if tx.txType == TxEip7873 and fork < FkOsaka: + return err("invalid tx: Eip7873 Tx type detected before Osaka") + if fork >= FkShanghai and tx.contractCreation and tx.payload.len > EIP3860_MAX_INITCODE_SIZE: return err("invalid tx: initcode size exceeds maximum") @@ -260,7 +263,7 @@ func validateTxBasic*( if tx.txType == TxEip4844: if tx.to.isNone: - return err("invalid tx: destination must be not empty") + return err("invalid tx: TxEip4844 destination must be not empty") if tx.versionedHashes.len == 0: return err("invalid tx: there must be at least one blob") @@ -278,6 +281,23 @@ func validateTxBasic*( if tx.authorizationList.len == 0: return err("invalid tx: authorization list must not empty") + if tx.txType == TxEip7873: + if tx.initCodes.len > MAX_INITCODE_COUNT: + return err("invalid tx: initcodes len exceeds MAX_INIT_COUNT") + + if tx.initCodes.len == 0: + return err("invalid tx: initcodes len should not zero") + + for i in 0.. EIP3860_MAX_INITCODE_SIZE: + return err("invalid tx: length of initcodes entry exceeeds EIP3860_MAX_INITCODE_SIZE") + + if tx.initCodes[i].len == 0: + return err("invalid tx: length of initcodes entry should not zero") + + if tx.to.isNone: + return err("invalid tx: TxEip7873 destination must be not empty") + ok() proc validateTransaction*( diff --git a/execution_chain/evm/interpreter/op_codes.nim b/execution_chain/evm/interpreter/op_codes.nim index 7c4b457921..58fe8fc8c7 100644 --- a/execution_chain/evm/interpreter/op_codes.nim +++ b/execution_chain/evm/interpreter/op_codes.nim @@ -1,5 +1,5 @@ # Nimbus -# Copyright (c) 2018-2024 Status Research & Development GmbH +# Copyright (c) 2018-2025 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or # http://www.apache.org/licenses/LICENSE-2.0) @@ -181,15 +181,27 @@ type Nop0xBD, Nop0xBE, Nop0xBF, Nop0xC0, Nop0xC1, Nop0xC2, Nop0xC3, Nop0xC4, Nop0xC5, Nop0xC6, Nop0xC7, Nop0xC8, Nop0xC9, Nop0xCA, Nop0xCB, Nop0xCC, Nop0xCD, Nop0xCE, - Nop0xCF, Nop0xD0, Nop0xD1, Nop0xD2, Nop0xD3, Nop0xD4, + Nop0xCF, + + DataLoad = 0xd0, + DataLoadN = 0xd1, + DataSize = 0xd2, + DataCopy = 0xd3, + + Nop0xD4, Nop0xD5, Nop0xD6, Nop0xD7, Nop0xD8, Nop0xD9, Nop0xDA, Nop0xDB, Nop0xDC, Nop0xDD, Nop0xDE, Nop0xDF, Nop0xE0, - Nop0xE1, Nop0xE2, Nop0xE3, Nop0xE4, Nop0xE5, Nop0xE6, - Nop0xE7, Nop0xE8, Nop0xE9, Nop0xEA, Nop0xEB, + Nop0xE1, Nop0xE2, Nop0xE3, Nop0xE4, Nop0xE5, + + DupN = 0xe6, + SwapN = 0xe7, + Exchange = 0xe8, + + Nop0xE9, Nop0xEA, Nop0xEB, EofCreate = 0xec, TxCreate = 0xed, - ReturnContract = 0xee, + ReturnCode = 0xee, Nop0xEF, ## .. @@ -205,11 +217,17 @@ type Create2 = 0xf5, ## Behaves identically to CREATE, except using ## keccak256 - Nop0xF6, Nop0xF7, Nop0xF8, Nop0xF9, ## .. + Nop0xF6, + + ReturnDataLoad = 0xf7, + ExtCall = 0xf8, + ExtDelegateCall= 0xf9, StaticCall = 0xfa, ## Static message-call into an account. - Nop0xFB, Nop0xFC, ## .. + ExtStaticCall = 0xfb, + + Nop0xFC, ## .. Revert = 0xfd, ## Halt execution reverting state changes but ## returning data and remaining gas. diff --git a/execution_chain/transaction/call_evm.nim b/execution_chain/transaction/call_evm.nim index 515e156899..1d1110155d 100644 --- a/execution_chain/transaction/call_evm.nim +++ b/execution_chain/transaction/call_evm.nim @@ -188,6 +188,9 @@ proc callParamsForTx(tx: Transaction, sender: Address, vmState: BaseVMState, bas if tx.txType == TxEip7702: assign(result.authorizationList, tx.authorizationList) + if tx.txType == TxEip7873: + assign(result.initCodes, tx.initCodes) + proc callParamsForTest(tx: Transaction, sender: Address, vmState: BaseVMState): CallParams = result = CallParams( vmState: vmState, @@ -211,6 +214,9 @@ proc callParamsForTest(tx: Transaction, sender: Address, vmState: BaseVMState): if tx.txType == TxEip7702: assign(result.authorizationList, tx.authorizationList) + if tx.txType == TxEip7873: + assign(result.initCodes, tx.initCodes) + proc txCallEvm*(tx: Transaction, sender: Address, vmState: BaseVMState, baseFee: GasInt): LogResult = diff --git a/execution_chain/transaction/call_types.nim b/execution_chain/transaction/call_types.nim index 300428d3e8..d73ab0cf23 100644 --- a/execution_chain/transaction/call_types.nim +++ b/execution_chain/transaction/call_types.nim @@ -36,6 +36,7 @@ type accessList*: AccessList # EIP-2930 (Berlin) tx access list. versionedHashes*: seq[VersionedHash] # EIP-4844 (Cancun) blob versioned hashes authorizationList*: seq[Authorization] # EIP-7702 (Prague) authorization list + initCodes*: seq[seq[byte]] # EIP-7873 (Osaka) init codes noIntrinsic*: bool # Don't charge intrinsic gas. noAccessList*: bool # Don't initialise EIP-2929 access list. noGasCharge*: bool # Don't charge sender account for gas. @@ -96,7 +97,16 @@ func intrinsicGas*(call: CallParams | Transaction, fork: EVMFork): (GasInt, GasI intrinsicGas += gasNonZero tokens += 4 - + # EIP-7873 (Osaka) init codes cost + for i in 0..= FkBerlin: for account in call.accessList: diff --git a/tests/test_eof/test_eip3540.nim b/tests/test_eof/test_eip3540.nim new file mode 100644 index 0000000000..86d6ba2a6b --- /dev/null +++ b/tests/test_eof/test_eip3540.nim @@ -0,0 +1,50 @@ +# nimbus-execution-client +# Copyright (c) 2018-2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +from eip3540 import is_valid_container, validate_eof, ValidationException +import pytest + +def is_invalid_with_error(code: bytes, error: str): + with pytest.raises(ValidationException, match=error): + validate_eof(code) + +def test_legacy_contracts(): + assert is_valid_container(b'') == True + assert is_valid_container(bytes.fromhex('00')) == True + assert is_valid_container(bytes.fromhex('ef')) == True # Magic second byte missing + +def test_no_eof_magic(): + # Any value outside the magic second byte + for m in range(1, 256): + assert is_valid_container(bytes((0xEF, m))) == True + +def test_eof1_container(): + is_invalid_with_error(bytes.fromhex('ef00'), "invalid version") + is_invalid_with_error(bytes.fromhex('ef0001'), "no section terminator") + is_invalid_with_error(bytes.fromhex('ef0000'), "invalid version") + is_invalid_with_error(bytes.fromhex('ef0002 010001 00 fe'), "invalid version") # Valid except version + is_invalid_with_error(bytes.fromhex('ef0001 00'), "no code section") # Only terminator + is_invalid_with_error(bytes.fromhex('ef0001 010001 00 fe aabbccdd'), "container size not equal to sum of section sizes") # Trailing bytes + is_invalid_with_error(bytes.fromhex('ef000101'), "truncated section size") + is_invalid_with_error(bytes.fromhex('ef000101000102'), "truncated section size") + is_invalid_with_error(bytes.fromhex('ef000103'), "invalid section id") + is_invalid_with_error(bytes.fromhex('ef00010100'), "truncated section size") + is_invalid_with_error(bytes.fromhex('ef00010100010200'), "truncated section size") + is_invalid_with_error(bytes.fromhex('ef000101000000'), "empty section") + is_invalid_with_error(bytes.fromhex('ef000101000102000000fe'), "empty section") + is_invalid_with_error(bytes.fromhex('ef0001010001'), "no section terminator") + is_invalid_with_error(bytes.fromhex('ef000101000100'), "container size not equal to sum of section sizes") # Missing section contents + is_invalid_with_error(bytes.fromhex('ef000102000100aa'), "data section preceding code section") # Only data section + is_invalid_with_error(bytes.fromhex('ef000101000101000100fefe'), "multiple sections with same id") # Multiple code sections + is_invalid_with_error(bytes.fromhex('ef000101000102000102000100feaabb'), "multiple sections with same id") # Multiple data sections + is_invalid_with_error(bytes.fromhex('ef000101000101000102000102000100fefeaabb'), "multiple sections with same id")# Multiple code and data sections + is_invalid_with_error(bytes.fromhex('ef000102000101000100aafe'), "data section preceding code section") + + assert is_valid_container(bytes.fromhex('ef000101000100fe')) == True # Valid format with 1-byte of code + assert is_valid_container(bytes.fromhex('ef000101000102000100feaa')) == True # Code and data section \ No newline at end of file diff --git a/tests/test_eof/test_eip3670.nim b/tests/test_eof/test_eip3670.nim new file mode 100644 index 0000000000..9eab332664 --- /dev/null +++ b/tests/test_eof/test_eip3670.nim @@ -0,0 +1,246 @@ +# nimbus-execution-client +# Copyright (c) 2018-2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +from eip3670 import * +import pytest + + +def is_invalid_with_error(code: bytes, error: str): + with pytest.raises(ValidationException, match=error): + validate_instructions(code) + + +def test_immediate_sizes_definition(): + assert len(immediate_sizes) == 256 + assert immediate_sizes[0x60] == 1 + assert immediate_sizes[0x7f] == 32 + + +def test_valid_opcodes(): + assert is_valid_code(bytes.fromhex("3000")) == True + assert is_valid_code(bytes.fromhex("5000")) == True + assert is_valid_code(bytes.fromhex("fe00")) == True + assert is_valid_code(bytes.fromhex("0000")) == True + + +def test_push_valid_immediate(): + assert is_valid_code(bytes.fromhex("600000")) == True + assert is_valid_code(b'\x61' + b'\x00' * 2 + b'\x00') == True + assert is_valid_code(b'\x62' + b'\x00' * 3 + b'\x00') == True + assert is_valid_code(b'\x63' + b'\x00' * 4 + b'\x00') == True + assert is_valid_code(b'\x64' + b'\x00' * 5 + b'\x00') == True + assert is_valid_code(b'\x65' + b'\x00' * 6 + b'\x00') == True + assert is_valid_code(b'\x66' + b'\x00' * 7 + b'\x00') == True + assert is_valid_code(b'\x67' + b'\x00' * 8 + b'\x00') == True + assert is_valid_code(b'\x68' + b'\x00' * 9 + b'\x00') == True + assert is_valid_code(b'\x69' + b'\x00' * 10 + b'\x00') == True + assert is_valid_code(b'\x6a' + b'\x00' * 11 + b'\x00') == True + assert is_valid_code(b'\x6b' + b'\x00' * 12 + b'\x00') == True + assert is_valid_code(b'\x6c' + b'\x00' * 13 + b'\x00') == True + assert is_valid_code(b'\x6d' + b'\x00' * 14 + b'\x00') == True + assert is_valid_code(b'\x6e' + b'\x00' * 15 + b'\x00') == True + assert is_valid_code(b'\x6f' + b'\x00' * 16 + b'\x00') == True + assert is_valid_code(b'\x70' + b'\x00' * 17 + b'\x00') == True + assert is_valid_code(b'\x71' + b'\x00' * 18 + b'\x00') == True + assert is_valid_code(b'\x72' + b'\x00' * 19 + b'\x00') == True + assert is_valid_code(b'\x73' + b'\x00' * 20 + b'\x00') == True + assert is_valid_code(b'\x74' + b'\x00' * 21 + b'\x00') == True + assert is_valid_code(b'\x75' + b'\x00' * 22 + b'\x00') == True + assert is_valid_code(b'\x76' + b'\x00' * 23 + b'\x00') == True + assert is_valid_code(b'\x77' + b'\x00' * 24 + b'\x00') == True + assert is_valid_code(b'\x78' + b'\x00' * 25 + b'\x00') == True + assert is_valid_code(b'\x79' + b'\x00' * 26 + b'\x00') == True + assert is_valid_code(b'\x7a' + b'\x00' * 27 + b'\x00') == True + assert is_valid_code(b'\x7b' + b'\x00' * 28 + b'\x00') == True + assert is_valid_code(b'\x7c' + b'\x00' * 29 + b'\x00') == True + assert is_valid_code(b'\x7d' + b'\x00' * 30 + b'\x00') == True + assert is_valid_code(b'\x7e' + b'\x00' * 31 + b'\x00') == True + assert is_valid_code(b'\x7f' + b'\x00' * 32 + b'\x00') == True + + +def test_valid_code_terminator(): + assert is_valid_code(b'\x00') == True + assert is_valid_code(b'\xf3') == True + assert is_valid_code(b'\xfd') == True + assert is_valid_code(b'\xfe') == True + + +def test_no_terminating_instruction(): + # Code does not need to finish with terminator anymore + assert is_valid_code(bytes.fromhex("5b")) + + +def test_undefined_instructions(): + # Invalid opcodes + is_invalid_with_error(bytes.fromhex("0c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("1e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("1f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("2100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2a00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2b00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("4900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4a00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4b00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("5c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("5d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("5e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("5f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("a500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("aa00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ab00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ac00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ad00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ae00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("af00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("b000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ba00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("be00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bf00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("c000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ca00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ce00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cf00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("d000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("da00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("db00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("dc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("dd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("de00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("df00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("e000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ea00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("eb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ec00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ed00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ee00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ef00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("f600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("fb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("fc00"), "undefined instruction") + + +def test_invalid_callcode(): + is_invalid_with_error(bytes.fromhex("f200"), "undefined instruction") + + +def test_invalid_selfdestruct(): + is_invalid_with_error(bytes.fromhex("ff00"), "undefined instruction") + + +def test_push_truncated_immediate(): + is_invalid_with_error(b'\x60', "truncated immediate") + is_invalid_with_error(b'\x61' + b'\x00' * 1, "truncated immediate") + is_invalid_with_error(b'\x62' + b'\x00' * 2, "truncated immediate") + is_invalid_with_error(b'\x63' + b'\x00' * 3, "truncated immediate") + is_invalid_with_error(b'\x64' + b'\x00' * 4, "truncated immediate") + is_invalid_with_error(b'\x65' + b'\x00' * 5, "truncated immediate") + is_invalid_with_error(b'\x66' + b'\x00' * 6, "truncated immediate") + is_invalid_with_error(b'\x67' + b'\x00' * 7, "truncated immediate") + is_invalid_with_error(b'\x68' + b'\x00' * 8, "truncated immediate") + is_invalid_with_error(b'\x69' + b'\x00' * 9, "truncated immediate") + is_invalid_with_error(b'\x6a' + b'\x00' * 10, "truncated immediate") + is_invalid_with_error(b'\x6b' + b'\x00' * 11, "truncated immediate") + is_invalid_with_error(b'\x6c' + b'\x00' * 12, "truncated immediate") + is_invalid_with_error(b'\x6d' + b'\x00' * 13, "truncated immediate") + is_invalid_with_error(b'\x6e' + b'\x00' * 14, "truncated immediate") + is_invalid_with_error(b'\x6f' + b'\x00' * 15, "truncated immediate") + is_invalid_with_error(b'\x70' + b'\x00' * 16, "truncated immediate") + is_invalid_with_error(b'\x71' + b'\x00' * 17, "truncated immediate") + is_invalid_with_error(b'\x72' + b'\x00' * 18, "truncated immediate") + is_invalid_with_error(b'\x73' + b'\x00' * 19, "truncated immediate") + is_invalid_with_error(b'\x74' + b'\x00' * 20, "truncated immediate") + is_invalid_with_error(b'\x75' + b'\x00' * 21, "truncated immediate") + is_invalid_with_error(b'\x76' + b'\x00' * 22, "truncated immediate") + is_invalid_with_error(b'\x77' + b'\x00' * 23, "truncated immediate") + is_invalid_with_error(b'\x78' + b'\x00' * 24, "truncated immediate") + is_invalid_with_error(b'\x79' + b'\x00' * 25, "truncated immediate") + is_invalid_with_error(b'\x7a' + b'\x00' * 26, "truncated immediate") + is_invalid_with_error(b'\x7b' + b'\x00' * 27, "truncated immediate") + is_invalid_with_error(b'\x7c' + b'\x00' * 28, "truncated immediate") + is_invalid_with_error(b'\x7d' + b'\x00' * 29, "truncated immediate") + is_invalid_with_error(b'\x7e' + b'\x00' * 30, "truncated immediate") + is_invalid_with_error(b'\x7f' + b'\x00' * 31, "truncated immediate") diff --git a/tests/test_eof/test_eip4200.nim b/tests/test_eof/test_eip4200.nim new file mode 100644 index 0000000000..be082248de --- /dev/null +++ b/tests/test_eof/test_eip4200.nim @@ -0,0 +1,437 @@ +# nimbus-execution-client +# Copyright (c) 2018-2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +from eip4200 import is_valid_code, validate_code, ValidationException +import pytest + + +def is_invalid_with_error(code: bytes, error: str): + with pytest.raises(ValidationException, match=error): + validate_code(code) + + +def test_valid_opcodes(): + assert is_valid_code(bytes.fromhex("3000")) == True + assert is_valid_code(bytes.fromhex("5000")) == True + assert is_valid_code(bytes.fromhex("5c000000")) == True + assert is_valid_code(bytes.fromhex("60005d000000")) == True + assert is_valid_code(bytes.fromhex("60005e01000000")) == True + assert is_valid_code(bytes.fromhex("fe00")) == True + assert is_valid_code(bytes.fromhex("0000")) == True + + +def test_push_valid_immediate(): + assert is_valid_code(b'\x60\x00\x00') == True + assert is_valid_code(b'\x61' + b'\x00' * 2 + b'\x00') == True + assert is_valid_code(b'\x62' + b'\x00' * 3 + b'\x00') == True + assert is_valid_code(b'\x63' + b'\x00' * 4 + b'\x00') == True + assert is_valid_code(b'\x64' + b'\x00' * 5 + b'\x00') == True + assert is_valid_code(b'\x65' + b'\x00' * 6 + b'\x00') == True + assert is_valid_code(b'\x66' + b'\x00' * 7 + b'\x00') == True + assert is_valid_code(b'\x67' + b'\x00' * 8 + b'\x00') == True + assert is_valid_code(b'\x68' + b'\x00' * 9 + b'\x00') == True + assert is_valid_code(b'\x69' + b'\x00' * 10 + b'\x00') == True + assert is_valid_code(b'\x6a' + b'\x00' * 11 + b'\x00') == True + assert is_valid_code(b'\x6b' + b'\x00' * 12 + b'\x00') == True + assert is_valid_code(b'\x6c' + b'\x00' * 13 + b'\x00') == True + assert is_valid_code(b'\x6d' + b'\x00' * 14 + b'\x00') == True + assert is_valid_code(b'\x6e' + b'\x00' * 15 + b'\x00') == True + assert is_valid_code(b'\x6f' + b'\x00' * 16 + b'\x00') == True + assert is_valid_code(b'\x70' + b'\x00' * 17 + b'\x00') == True + assert is_valid_code(b'\x71' + b'\x00' * 18 + b'\x00') == True + assert is_valid_code(b'\x72' + b'\x00' * 19 + b'\x00') == True + assert is_valid_code(b'\x73' + b'\x00' * 20 + b'\x00') == True + assert is_valid_code(b'\x74' + b'\x00' * 21 + b'\x00') == True + assert is_valid_code(b'\x75' + b'\x00' * 22 + b'\x00') == True + assert is_valid_code(b'\x76' + b'\x00' * 23 + b'\x00') == True + assert is_valid_code(b'\x77' + b'\x00' * 24 + b'\x00') == True + assert is_valid_code(b'\x78' + b'\x00' * 25 + b'\x00') == True + assert is_valid_code(b'\x79' + b'\x00' * 26 + b'\x00') == True + assert is_valid_code(b'\x7a' + b'\x00' * 27 + b'\x00') == True + assert is_valid_code(b'\x7b' + b'\x00' * 28 + b'\x00') == True + assert is_valid_code(b'\x7c' + b'\x00' * 29 + b'\x00') == True + assert is_valid_code(b'\x7d' + b'\x00' * 30 + b'\x00') == True + assert is_valid_code(b'\x7e' + b'\x00' * 31 + b'\x00') == True + assert is_valid_code(b'\x7f' + b'\x00' * 32 + b'\x00') == True + + +def test_rjump_valid_immediate(): + # offset = 0 + assert is_valid_code(bytes.fromhex("5c000000")) == True + # offset = 1 + assert is_valid_code(bytes.fromhex("5c00010000")) == True + # offset = 4 + assert is_valid_code(bytes.fromhex("5c00010000000000")) == True + # offset = 256 + assert is_valid_code(bytes.fromhex("5c0100") + b'\x00' * 256 + b'\x00') == True + # offset = 32767 + assert is_valid_code(bytes.fromhex("5c7fff") + b'\x00' * 32767 + b'\x00') == True + # offset = -3 + assert is_valid_code(bytes.fromhex("5cfffd0000")) == True + # offset = -4 + assert is_valid_code(bytes.fromhex("005cfffc00")) == True + # offset = -256 + assert is_valid_code(b'\x00' * 253 + bytes.fromhex("5cff0000")) == True + # offset = -32768 + assert is_valid_code(b'\x00' * 32765 + bytes.fromhex("5c800000")) == True + + +def test_rjumpi_valid_immediate(): + # offset = 0 + assert is_valid_code(bytes.fromhex("60015d000000")) == True + # offset = 1 + assert is_valid_code(bytes.fromhex("60015d00010000")) == True + # offset = 4 + assert is_valid_code(bytes.fromhex("60015d00010000000000")) == True + # offset = 256 + assert is_valid_code(bytes.fromhex("60015d0100") + b'\x5b' * 256 + b'\x00') == True + # offset = 32767 + assert is_valid_code(bytes.fromhex("60015d7fff") + b'\x5b' * 32767 + b'\x00') == True + # offset = -3 + assert is_valid_code(bytes.fromhex("60015dfffd0000")) == True + # offset = -5 + assert is_valid_code(bytes.fromhex("60015dfffb00")) == True + # offset = -256 + assert is_valid_code(b'\x00' * 252 + bytes.fromhex("60015dff0000")) == True + # offset = -32768 + assert is_valid_code(b'\x00' * 32763 + bytes.fromhex("60015d800000")) == True + # RJUMP without PUSH before - still valid + assert is_valid_code(bytes.fromhex("5d000000")) == True + + +def test_rjumptable_valid_immediate(): + # offset1 = 0 + assert is_valid_code(bytes.fromhex("60015e01000000")) == True + # offset1 = 0, offset2 = 1 + assert is_valid_code(bytes.fromhex("60015e02000000010000")) == True + # offset1 = 0, offset2 = 4, offset3 = 256 + assert is_valid_code(bytes.fromhex("60015e03000000040100") + b'\x5b' * 256 + b'\x00') == True + # offset1 = 0, offset2 = 4, offset3 = 256, offset4 = 32767 + assert is_valid_code(bytes.fromhex("60015e040000000401007fff") + b'\x5b' * 32767 + b'\x00') == True + # offset1 = -4 + assert is_valid_code(bytes.fromhex("60015e01fffc0000")) == True + # offset1 = -6, offset2 = -256 + assert is_valid_code(b'\x5b' * 248 + bytes.fromhex("60015e02fffaff0000")) == True + # offset1 = -6, offset = -32768 + assert is_valid_code(b'\x5b' * 32760 + bytes.fromhex("60015e02fffa800000")) == True + # RJUMPV without PUSH before - still valid + assert is_valid_code(bytes.fromhex("5e01000000")) == True + + +def test_valid_code_terminator(): + assert is_valid_code(b'\x00') == True + assert is_valid_code(b'\xf3') == True + assert is_valid_code(b'\xfd') == True + assert is_valid_code(b'\xfe') == True + + +def test_invalid_code(): + # Empty code + assert is_valid_code(b'') == False + + # Valid opcode, but invalid as terminator + assert is_valid_code(bytes.fromhex("5b")) # TODO + assert is_valid_code(bytes.fromhex("5cfffd")) + assert is_valid_code(bytes.fromhex("60005dfffd")) + assert is_valid_code(bytes.fromhex("60005e01fffc")) + + # Trunc imm + is_invalid_with_error(bytes.fromhex("61ff"), "truncated immediate") + + # Invalid opcodes + is_invalid_with_error(bytes.fromhex("0c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("1e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("1f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("2100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2a00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2b00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("4900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4a00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4b00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("5f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("a500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("aa00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ab00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ac00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ad00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ae00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("af00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("b000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ba00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("be00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bf00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("c000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ca00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ce00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cf00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("d000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("da00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("db00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("dc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("dd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("de00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("df00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("e000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ea00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("eb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ec00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ed00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ee00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ef00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("f600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("fb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("fc00"), "undefined instruction") + + +def test_push_truncated_immediate(): + is_invalid_with_error(b'\x60', "truncated immediate") + is_invalid_with_error(b'\x61' + b'\x00' * 1, "truncated immediate") + is_invalid_with_error(b'\x62' + b'\x00' * 2, "truncated immediate") + is_invalid_with_error(b'\x63' + b'\x00' * 3, "truncated immediate") + is_invalid_with_error(b'\x64' + b'\x00' * 4, "truncated immediate") + is_invalid_with_error(b'\x65' + b'\x00' * 5, "truncated immediate") + is_invalid_with_error(b'\x66' + b'\x00' * 6, "truncated immediate") + is_invalid_with_error(b'\x67' + b'\x00' * 7, "truncated immediate") + is_invalid_with_error(b'\x68' + b'\x00' * 8, "truncated immediate") + is_invalid_with_error(b'\x69' + b'\x00' * 9, "truncated immediate") + is_invalid_with_error(b'\x6a' + b'\x00' * 10, "truncated immediate") + is_invalid_with_error(b'\x6b' + b'\x00' * 11, "truncated immediate") + is_invalid_with_error(b'\x6c' + b'\x00' * 12, "truncated immediate") + is_invalid_with_error(b'\x6d' + b'\x00' * 13, "truncated immediate") + is_invalid_with_error(b'\x6e' + b'\x00' * 14, "truncated immediate") + is_invalid_with_error(b'\x6f' + b'\x00' * 15, "truncated immediate") + is_invalid_with_error(b'\x70' + b'\x00' * 16, "truncated immediate") + is_invalid_with_error(b'\x71' + b'\x00' * 17, "truncated immediate") + is_invalid_with_error(b'\x72' + b'\x00' * 18, "truncated immediate") + is_invalid_with_error(b'\x73' + b'\x00' * 19, "truncated immediate") + is_invalid_with_error(b'\x74' + b'\x00' * 20, "truncated immediate") + is_invalid_with_error(b'\x75' + b'\x00' * 21, "truncated immediate") + is_invalid_with_error(b'\x76' + b'\x00' * 22, "truncated immediate") + is_invalid_with_error(b'\x77' + b'\x00' * 23, "truncated immediate") + is_invalid_with_error(b'\x78' + b'\x00' * 24, "truncated immediate") + is_invalid_with_error(b'\x79' + b'\x00' * 25, "truncated immediate") + is_invalid_with_error(b'\x7a' + b'\x00' * 26, "truncated immediate") + is_invalid_with_error(b'\x7b' + b'\x00' * 27, "truncated immediate") + is_invalid_with_error(b'\x7c' + b'\x00' * 28, "truncated immediate") + is_invalid_with_error(b'\x7d' + b'\x00' * 29, "truncated immediate") + is_invalid_with_error(b'\x7e' + b'\x00' * 30, "truncated immediate") + is_invalid_with_error(b'\x7f' + b'\x00' * 31, "truncated immediate") + + +def test_rjump_truncated_immediate(): + is_invalid_with_error(bytes.fromhex("5c"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("5c00"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("5c0000"), "relative jump destination out of bounds") + + +def test_rjumpi_truncated_immediate(): + is_invalid_with_error(bytes.fromhex("60015d"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("60015d00"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("60015d0000"), "relative jump destination out of bounds") + + +def test_rjumpv_truncated_immediate(): + is_invalid_with_error(bytes.fromhex("60015e"), "truncated jump table") + is_invalid_with_error(bytes.fromhex("60015e01"), "truncated jump table") + is_invalid_with_error(bytes.fromhex("60015e0100"), "truncated jump table") + is_invalid_with_error(bytes.fromhex("60015e030000"), "truncated jump table") + is_invalid_with_error(bytes.fromhex("60015e0300000001"), "truncated jump table") + is_invalid_with_error(bytes.fromhex("60015e030000000100"), "truncated jump table") + + +def test_rjumps_out_of_bounds(): + # RJUMP destination out of bounds + # offset = 1 + is_invalid_with_error(bytes.fromhex("5c000100"), "relative jump destination out of bounds") + # offset = -4 + is_invalid_with_error(bytes.fromhex("5cfffc00"), "relative jump destination out of bounds") + # RJUMPI destination out of bounds + # offset = 1 + is_invalid_with_error(bytes.fromhex("60015d000100"), "relative jump destination out of bounds") + # offset = -6 + is_invalid_with_error(bytes.fromhex("60015dfffa00"), "relative jump destination out of bounds") + # RJUMPV destination out of bounds + # offset = 1 + is_invalid_with_error(bytes.fromhex("60015e01000100"), "relative jump destination out of bounds") + # offset = -7 + is_invalid_with_error(bytes.fromhex("60015e01fff900"), "relative jump destination out of bounds") + + +def test_rjumps_into_immediate(): + for n in range(1, 33): + for offset in range(1, n + 1): + code = [0x5c, 0x00, offset] # RJUMP offset + code += [0x60 + n - 1] # PUSHn + code += [0x00] * n # push data + code += [0x00] # STOP + + is_invalid_with_error(code, "relative jump destination targets immediate") + + code = [0x60, 0x01, 0x5d, 0x00, offset] # PUSH1 1 RJUMI offset + code += [0x60 + n - 1] # PUSHn + code += [0x00] * n # push data + code += [0x00] # STOP + + is_invalid_with_error(code, "relative jump destination targets immediate") + + code = [0x60, 0x01, 0x5e, 0x01, 0x00, offset] # PUSH1 1 RJUMV size offset + code += [0x60 + n - 1] # PUSHn + code += [0x00] * n # push data + code += [0x00] # STOP + + is_invalid_with_error(code, "relative jump destination targets immediate") + + # RJUMP into RJUMP immediate + is_invalid_with_error(bytes.fromhex("5cffff00"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5cfffe00"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c00015c000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c00025c000000"), "relative jump destination targets immediate") + # RJUMPI into RJUMP immediate + is_invalid_with_error(bytes.fromhex("60015d00015c000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d00025c000000"), "relative jump destination targets immediate") + # RJUMPV into RJUMP immediate + is_invalid_with_error(bytes.fromhex("60015e0100015c000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100025c000000"), "relative jump destination targets immediate") + + # RJUMP into RJUMPI immediate + is_invalid_with_error(bytes.fromhex("5c000360015d000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c000460015d000000"), "relative jump destination targets immediate") + # RJUMPI into RJUMPI immediate + is_invalid_with_error(bytes.fromhex("60015dffff00"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015dfffe00"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d000360015d000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d000460015d000000"), "relative jump destination targets immediate") + # RJUMPV into RJUMPI immediate + is_invalid_with_error(bytes.fromhex("60015e01000360015d000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e01000460015d000000"), "relative jump destination targets immediate") + + # RJUMP into RJUMPV immediate + is_invalid_with_error(bytes.fromhex("5c00015e01000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c00025e01000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c00035e01000000"), "relative jump destination targets immediate") + # RJUMPI into RJUMPV immediate + is_invalid_with_error(bytes.fromhex("60015d00015e01000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d00025e01000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d00035e01000000"), "relative jump destination targets immediate") + # RJUMPV into RJUMPV immediate + is_invalid_with_error(bytes.fromhex("60015e01ffff00"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e01fffe00"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e01fffd00"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100015e01000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100025e01000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100035e01000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100015e020000fff400"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100025e020000fff400"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100035e020000fff400"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100045e020000fff400"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015e0100055e020000fff400"), "relative jump destination targets immediate") + + +def test_rjumpv_empty_table(): + is_invalid_with_error(bytes.fromhex("60015e0000"), "empty jump table") + + +def test_immediate_contains_opcode(): + # 0x5c byte which could be interpreted a RJUMP, but it's not because it's in PUSH data + assert is_valid_code(bytes.fromhex("605c001000")) == True + assert is_valid_code(bytes.fromhex("61005c001000")) == True + # 0x5d byte which could be interpreted a RJUMPI, but it's not because it's in PUSH data + assert is_valid_code(bytes.fromhex("605d001000")) == True + assert is_valid_code(bytes.fromhex("61005d001000")) == True + # 0x5e byte which could be interpreted a RJUMPV, but it's not because it's in PUSH data + assert is_valid_code(bytes.fromhex("605e01000000")) == True + assert is_valid_code(bytes.fromhex("61005e01000000")) == True + + # 0x60 byte which could be interpreted as PUSH, but it's not because it's in RJUMP data + # offset = -160 + assert is_valid_code(b'\x5b' * 160 + bytes.fromhex("5cff6000")) == True + # # 0x60 byte which could be interpreted as PUSH, but it's not because it's in RJUMPI data + # # offset = -160 + assert is_valid_code(b'\x5b' * 160 + bytes.fromhex("5dff6000")) == True + # 0x60 byte which could be interpreted as PUSH, but it's not because it's in RJUMPV data + # offset = -160 + assert is_valid_code(b'\x5b' * 160 + bytes.fromhex("5e01ff6000")) == True diff --git a/tests/test_eof/test_eip4750.nim b/tests/test_eof/test_eip4750.nim new file mode 100644 index 0000000000..e5b4f06d07 --- /dev/null +++ b/tests/test_eof/test_eip4750.nim @@ -0,0 +1,495 @@ +# nimbus-execution-client +# Copyright (c) 2018-2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +from eip4750 import is_valid_code, is_valid_eof, validate_eof, validate_code_section, FunctionType, ValidationException +import pytest + + +def is_invalid_eof_with_error(code: bytes, error: str): + with pytest.raises(ValidationException, match=error): + validate_eof(code) + + +def is_invalid_with_error(code: bytes, error: str, types: list[FunctionType] = [FunctionType(0, 0)]): + with pytest.raises(ValidationException, match=error): + validate_code_section(0, code, types) + + +def test_eof1_container(): + is_invalid_eof_with_error(bytes.fromhex('ef00'), "invalid version") + is_invalid_eof_with_error(bytes.fromhex('ef0001'), "no section terminator") + is_invalid_eof_with_error(bytes.fromhex('ef0000'), "invalid version") + is_invalid_eof_with_error(bytes.fromhex('ef0002 010001 00 fe'), "invalid version") # Valid except version + is_invalid_eof_with_error(bytes.fromhex('ef0001 00'), "no code section") # Only terminator + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 00 fe aabbccdd'), "container size not equal to sum of section sizes") # Trailing bytes + is_invalid_eof_with_error(bytes.fromhex('ef0001 01'), "truncated section size") + is_invalid_eof_with_error(bytes.fromhex('ef0001 02'), "truncated section size") + is_invalid_eof_with_error(bytes.fromhex('ef0001 03'), "truncated section size") + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 02'), "truncated section size") + is_invalid_eof_with_error(bytes.fromhex('ef0001 040002 010001 00 0000 fe'), "invalid section id") + is_invalid_eof_with_error(bytes.fromhex('ef0001 0100'), "truncated section size") + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 0200'), "truncated section size") + is_invalid_eof_with_error(bytes.fromhex('ef0001 010000 00'), "empty section") + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 020000 00 fe'), "empty section") + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001'), "no section terminator") + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 00'), "container size not equal to sum of section sizes") # Missing section contents + is_invalid_eof_with_error(bytes.fromhex('ef0001 020001 00 aa'), "no code section") # Only data section + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 010001 00 fe fe'), "no obligatory type section") # Multiple code sections without type section + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 020001 020001 00 fe aa bb'), "multiple data sections") # Multiple data sections + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 010001 020001 020001 00 fe fe aa bb'), "multiple data sections") # Multiple code and data sections + is_invalid_eof_with_error(bytes.fromhex('ef0001 020001 010001 00 aa fe'), "data section preceding code section") + + assert is_valid_eof(bytes.fromhex('ef000101000100fe')) == True # Valid format with 1-byte of code + assert is_valid_eof(bytes.fromhex('ef000101000102000100feaa')) == True # Code and data section + + +def test_eof_type_section(): + is_invalid_eof_with_error(bytes.fromhex('ef0001 03'), "truncated section size") # Truncated type section header + is_invalid_eof_with_error(bytes.fromhex('ef0001 0300'), "truncated section size") # Truncated type section size + is_invalid_eof_with_error(bytes.fromhex('ef0001 030000 010001 00 fe'), "empty section") # 0 type section size + + # Valid with one code section and implicit type section + assert is_valid_eof(bytes.fromhex('ef0001 010001 00 fe')) == True + # Valid with one code section and explicit type section + assert is_valid_eof(bytes.fromhex('ef0001 030002 010001 00 0000 fe')) == True + # Valid with two code sections, 2nd code sections has 0 inputs and 1 output + assert is_valid_eof(bytes.fromhex('ef0001 030004 010001 010003 00 00000001 fe 6000b1')) == True + # Valid with two code sections, 2nd code sections has 2 inputs and 0 outputs + assert is_valid_eof(bytes.fromhex('ef0001 030004 010001 010003 00 00000200 fe 5050b1')) == True + # Valid with two code sections, 2nd code sections has 2 inputs and 1 output + assert is_valid_eof(bytes.fromhex('ef0001 030004 010001 010002 00 00000201 fe 50b1')) == True + # Valid with two code sections and one data section + assert is_valid_eof(bytes.fromhex('ef0001 030004 010001 010002 020004 00 00000201 fe 50b1 aabbccdd')) == True + + # Invalid with two code sections and no type section + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 010003 00 fe 6000b1'), "no obligatory type section") + # Invalid with two code sections, second one following data section + is_invalid_eof_with_error(bytes.fromhex('ef0001 030004 010001 020004 010002 00 00000201 fe aabbccdd 50b1'), "data section preceding code section") + # Invalid with multiple type sections + is_invalid_eof_with_error(bytes.fromhex('ef0001 030002 030002 010001 010002 00 0000 0201 fe 50b1'), "multiple type sections") + # Invalid with type section after code sections (but before data section) + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 010002 030004 020004 00 fe 50b1 00000201 aabbccdd'), "code or data section preceding type section") + # Invalid with type section after code sections and data section + is_invalid_eof_with_error(bytes.fromhex('ef0001 010001 010002 020004 030004 00 fe 50b1 aabbccdd 00000201'), "code or data section preceding type section") + # Invalid with incorrect type section size + is_invalid_eof_with_error(bytes.fromhex('ef0001 030002 010001 010002 020004 00 0000 fe 50b1 aabbccdd'), "invalid type section size") + is_invalid_eof_with_error(bytes.fromhex('ef0001 030006 010001 010002 020004 00 000002010000 fe 50b1 aabbccdd'), "invalid type section size") + # Invalid with type section without code section + is_invalid_eof_with_error(bytes.fromhex('ef0001 030004 020004 00 00000201 aabbccdd'), "no code section") + # Invalid with first code code sections not having 0 inputs 0 outputs + is_invalid_eof_with_error(bytes.fromhex('ef0001 030004 010003 010001 020004 00 00010000 6000b1 fe aabbccdd'), "invalid type of section 0") + is_invalid_eof_with_error(bytes.fromhex('ef0001 030004 010003 010001 020004 00 02000000 505000 fe aabbccdd'), "invalid type of section 0") + is_invalid_eof_with_error(bytes.fromhex('ef0001 030004 010002 010001 020004 00 02010000 50b1 fe aabbccdd'), "invalid type of section 0") + + # Valid with 1024 code sections + assert is_valid_eof(bytes.fromhex('ef0001 030800') + b'\x01\x00\x01' * 1024 + b'\x00' + b'\x00\x00' * 1024 + b'\xfe' * 1024) == True + # Invalid with 1025 code sections + is_invalid_eof_with_error(bytes.fromhex('ef0001 030802') + b'\x01\x00\x01' * 1025 + b'\x00' + b'\x00\x00' * 1025 + b'\xfe' * 1025, "more than 1024 code sections") + + +def test_valid_opcodes(): + assert is_valid_code(0, bytes.fromhex("3000")) == True + assert is_valid_code(0, bytes.fromhex("5000")) == True + assert is_valid_code(0, bytes.fromhex("b0000000")) == True + assert is_valid_code(0, bytes.fromhex("b1")) == True + assert is_valid_code(0, bytes.fromhex("fe00")) == True + assert is_valid_code(0, bytes.fromhex("0000")) == True + assert is_valid_code(0, bytes.fromhex("5b00")) == True + + +def test_push_valid_immediate(): + assert is_valid_code(0, b'\x60\x00\x00') == True + assert is_valid_code(0, b'\x61' + b'\x00' * 2 + b'\x00') == True + assert is_valid_code(0, b'\x62' + b'\x00' * 3 + b'\x00') == True + assert is_valid_code(0, b'\x63' + b'\x00' * 4 + b'\x00') == True + assert is_valid_code(0, b'\x64' + b'\x00' * 5 + b'\x00') == True + assert is_valid_code(0, b'\x65' + b'\x00' * 6 + b'\x00') == True + assert is_valid_code(0, b'\x66' + b'\x00' * 7 + b'\x00') == True + assert is_valid_code(0, b'\x67' + b'\x00' * 8 + b'\x00') == True + assert is_valid_code(0, b'\x68' + b'\x00' * 9 + b'\x00') == True + assert is_valid_code(0, b'\x69' + b'\x00' * 10 + b'\x00') == True + assert is_valid_code(0, b'\x6a' + b'\x00' * 11 + b'\x00') == True + assert is_valid_code(0, b'\x6b' + b'\x00' * 12 + b'\x00') == True + assert is_valid_code(0, b'\x6c' + b'\x00' * 13 + b'\x00') == True + assert is_valid_code(0, b'\x6d' + b'\x00' * 14 + b'\x00') == True + assert is_valid_code(0, b'\x6e' + b'\x00' * 15 + b'\x00') == True + assert is_valid_code(0, b'\x6f' + b'\x00' * 16 + b'\x00') == True + assert is_valid_code(0, b'\x70' + b'\x00' * 17 + b'\x00') == True + assert is_valid_code(0, b'\x71' + b'\x00' * 18 + b'\x00') == True + assert is_valid_code(0, b'\x72' + b'\x00' * 19 + b'\x00') == True + assert is_valid_code(0, b'\x73' + b'\x00' * 20 + b'\x00') == True + assert is_valid_code(0, b'\x74' + b'\x00' * 21 + b'\x00') == True + assert is_valid_code(0, b'\x75' + b'\x00' * 22 + b'\x00') == True + assert is_valid_code(0, b'\x76' + b'\x00' * 23 + b'\x00') == True + assert is_valid_code(0, b'\x77' + b'\x00' * 24 + b'\x00') == True + assert is_valid_code(0, b'\x78' + b'\x00' * 25 + b'\x00') == True + assert is_valid_code(0, b'\x79' + b'\x00' * 26 + b'\x00') == True + assert is_valid_code(0, b'\x7a' + b'\x00' * 27 + b'\x00') == True + assert is_valid_code(0, b'\x7b' + b'\x00' * 28 + b'\x00') == True + assert is_valid_code(0, b'\x7c' + b'\x00' * 29 + b'\x00') == True + assert is_valid_code(0, b'\x7d' + b'\x00' * 30 + b'\x00') == True + assert is_valid_code(0, b'\x7e' + b'\x00' * 31 + b'\x00') == True + assert is_valid_code(0, b'\x7f' + b'\x00' * 32 + b'\x00') == True + + +def test_rjump_valid_immediate(): + # offset = 0 + assert is_valid_code(0, bytes.fromhex("5c000000")) == True + # offset = 1 + assert is_valid_code(0, bytes.fromhex("5c00010000")) == True + # offset = 4 + assert is_valid_code(0, bytes.fromhex("5c00010000000000")) == True + # offset = 256 + assert is_valid_code(0, bytes.fromhex("5c0100") + b'\x00' * 256 + b'\x00') == True + # offset = 32767 + assert is_valid_code(0, bytes.fromhex("5c7fff") + b'\x00' * 32767 + b'\x00') == True + # offset = -3 + assert is_valid_code(0, bytes.fromhex("5cfffd0000")) == True + # offset = -4 + assert is_valid_code(0, bytes.fromhex("005cfffc00")) == True + # offset = -256 + assert is_valid_code(0, b'\x00' * 253 + bytes.fromhex("5cff0000")) == True + # offset = -32768 + assert is_valid_code(0, b'\x00' * 32765 + bytes.fromhex("5c800100")) == True + + +def test_rjumpi_valid_immediate(): + # offset = 0 + assert is_valid_code(0, bytes.fromhex("60015d000000")) == True + # offset = 1 + assert is_valid_code(0, bytes.fromhex("60015d00010000")) == True + # offset = 4 + assert is_valid_code(0, bytes.fromhex("60015d00010000000000")) == True + # offset = 256 + assert is_valid_code(0, bytes.fromhex("60015d0100") + b'\x00' * 256 + b'\x00') == True + # offset = 32767 + assert is_valid_code(0, bytes.fromhex("60015d7fff") + b'\x00' * 32767 + b'\x00') == True + # offset = -3 + assert is_valid_code(0, bytes.fromhex("60015dfffd0000")) == True + # offset = -5 + assert is_valid_code(0, bytes.fromhex("60015dfffb00")) == True + # offset = -256 + assert is_valid_code(0, b'\x00' * 252 + bytes.fromhex("60015dff0000")) == True + # offset = -32768 + assert is_valid_code(0, b'\x00' * 32763 + bytes.fromhex("60015d800100")) == True + # RJUMP without PUSH before - still valid + assert is_valid_code(0, bytes.fromhex("5d000000")) == True + + +def test_callf_valid_immediate(): + assert is_valid_code(0, bytes.fromhex("b0000000")) == True + assert is_valid_code(0, bytes.fromhex("b0000100"), [FunctionType(0, 0), FunctionType(0, 0)]) == True + assert is_valid_code(0, bytes.fromhex("b0000000"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000100"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000200"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000300"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000400"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000500"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000600"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000700"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000800"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0000900"), [FunctionType(0, 0)] * 10) == True + assert is_valid_code(0, bytes.fromhex("b0ffff00"), [FunctionType(0, 0)] * 65536) == True + + +# TODO tailcallf_valid_immediate + +def test_valid_code_terminator(): + assert is_valid_code(0, b'\x00') == True + assert is_valid_code(0, b'\xb1') == True + assert is_valid_code(0, b'\xf3') == True + assert is_valid_code(0, b'\xfd') == True + assert is_valid_code(0, b'\xfe') == True + validate_code_section(0, b'\xb2\x00\x00') + assert is_valid_code(0, b'\xb2\x00\x00') == True + + +def test_invalid_code(): + # Empty code + assert is_valid_code(0, b'') == False + + # Valid opcode, but invalid as terminator + is_invalid_with_error(bytes.fromhex("5b"), "no terminating instruction") + is_invalid_with_error(bytes.fromhex("b00000"), "no terminating instruction") + # Invalid opcodes + is_invalid_with_error(bytes.fromhex("0c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("0f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("1e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("1f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("2100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2a00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2b00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("2f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("4900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4a00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4b00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4c00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4d00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("4f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("5600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("5700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("5e00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("5f00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("a500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("a900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("aa00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ab00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ac00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ad00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ae00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("af00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("b300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("b900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ba00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("be00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("bf00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("c000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("c900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ca00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ce00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("cf00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("d000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("d900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("da00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("db00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("dc00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("dd00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("de00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("df00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("e000"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e100"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e200"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e300"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e400"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e500"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("e900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ea00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("eb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ec00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ed00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ee00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("ef00"), "undefined instruction") + + is_invalid_with_error(bytes.fromhex("f600"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f700"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f800"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("f900"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("fb00"), "undefined instruction") + is_invalid_with_error(bytes.fromhex("fc00"), "undefined instruction") + + +def test_push_truncated_immediate(): + is_invalid_with_error(b'\x60', "truncated immediate") + is_invalid_with_error(b'\x61' + b'\x00' * 1, "truncated immediate") + is_invalid_with_error(b'\x62' + b'\x00' * 2, "truncated immediate") + is_invalid_with_error(b'\x63' + b'\x00' * 3, "truncated immediate") + is_invalid_with_error(b'\x64' + b'\x00' * 4, "truncated immediate") + is_invalid_with_error(b'\x65' + b'\x00' * 5, "truncated immediate") + is_invalid_with_error(b'\x66' + b'\x00' * 6, "truncated immediate") + is_invalid_with_error(b'\x67' + b'\x00' * 7, "truncated immediate") + is_invalid_with_error(b'\x68' + b'\x00' * 8, "truncated immediate") + is_invalid_with_error(b'\x69' + b'\x00' * 9, "truncated immediate") + is_invalid_with_error(b'\x6a' + b'\x00' * 10, "truncated immediate") + is_invalid_with_error(b'\x6b' + b'\x00' * 11, "truncated immediate") + is_invalid_with_error(b'\x6c' + b'\x00' * 12, "truncated immediate") + is_invalid_with_error(b'\x6d' + b'\x00' * 13, "truncated immediate") + is_invalid_with_error(b'\x6e' + b'\x00' * 14, "truncated immediate") + is_invalid_with_error(b'\x6f' + b'\x00' * 15, "truncated immediate") + is_invalid_with_error(b'\x70' + b'\x00' * 16, "truncated immediate") + is_invalid_with_error(b'\x71' + b'\x00' * 17, "truncated immediate") + is_invalid_with_error(b'\x72' + b'\x00' * 18, "truncated immediate") + is_invalid_with_error(b'\x73' + b'\x00' * 19, "truncated immediate") + is_invalid_with_error(b'\x74' + b'\x00' * 20, "truncated immediate") + is_invalid_with_error(b'\x75' + b'\x00' * 21, "truncated immediate") + is_invalid_with_error(b'\x76' + b'\x00' * 22, "truncated immediate") + is_invalid_with_error(b'\x77' + b'\x00' * 23, "truncated immediate") + is_invalid_with_error(b'\x78' + b'\x00' * 24, "truncated immediate") + is_invalid_with_error(b'\x79' + b'\x00' * 25, "truncated immediate") + is_invalid_with_error(b'\x7a' + b'\x00' * 26, "truncated immediate") + is_invalid_with_error(b'\x7b' + b'\x00' * 27, "truncated immediate") + is_invalid_with_error(b'\x7c' + b'\x00' * 28, "truncated immediate") + is_invalid_with_error(b'\x7d' + b'\x00' * 29, "truncated immediate") + is_invalid_with_error(b'\x7e' + b'\x00' * 30, "truncated immediate") + is_invalid_with_error(b'\x7f' + b'\x00' * 31, "truncated immediate") + + +def test_rjump_truncated_immediate(): + is_invalid_with_error(bytes.fromhex("5c"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("5c00"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("5c0000"), "relative jump destination out of bounds") + + +def test_rjumpi_truncated_immediate(): + is_invalid_with_error(bytes.fromhex("60015d"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("60015d00"), "truncated relative jump offset") + is_invalid_with_error(bytes.fromhex("60015d0000"), "relative jump destination out of bounds") + + +def test_rjumps_out_of_bounds(): + # RJUMP destination out of bounds + # offset = 1 + is_invalid_with_error(bytes.fromhex("5c000100"), "relative jump destination out of bounds") + # offset = -4 + is_invalid_with_error(bytes.fromhex("5cfffc00"), "relative jump destination out of bounds") + # RJUMPI destination out of bounds + # offset = 1 + is_invalid_with_error(bytes.fromhex("60015d000100"), "relative jump destination out of bounds") + # offset = -6 + is_invalid_with_error(bytes.fromhex("60015dfffa00"), "relative jump destination out of bounds") + + +def test_rjumps_into_immediate(): + for n in range(1, 33): + for offset in range(1, n + 1): + code = [0x5c, 0x00, offset] # RJUMP offset + code += [0x60 + n - 1] # PUSHn + code += [0x00] * n # push data + code += [0x00] # STOP + + is_invalid_with_error(bytes(code), "relative jump destination targets immediate") + + code = [0x60, 0x01, 0x5d, 0x00, offset] # PUSH1 1 RJUMI offset + code += [0x60 + n - 1] # PUSHn + code += [0x00] * n # push data + code += [0x00] # STOP + + is_invalid_with_error(bytes(code), "relative jump destination targets immediate") + + # RJUMP into RJUMP immediate + is_invalid_with_error(bytes.fromhex("5c00015c000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c00025c000000"), "relative jump destination targets immediate") + # RJUMPI into RJUMP immediate + is_invalid_with_error(bytes.fromhex("60015d00015c000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d00025c000000"), "relative jump destination targets immediate") + # RJUMP into RJUMPI immediate + is_invalid_with_error(bytes.fromhex("5c000360015d000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c000460015d000000"), "relative jump destination targets immediate") + # RJUMPI into RJUMPI immediate + is_invalid_with_error(bytes.fromhex("60015d000360015d000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d000460015d000000"), "relative jump destination targets immediate") + # RJUMP into CALLF immediate + is_invalid_with_error(bytes.fromhex("5c0001b0000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("5c0002b0000000"), "relative jump destination targets immediate") + # RJUMPI into CALLF immediate + is_invalid_with_error(bytes.fromhex("60015d0001b0000000"), "relative jump destination targets immediate") + is_invalid_with_error(bytes.fromhex("60015d0001b0000000"), "relative jump destination targets immediate") + + +def test_callf_truncated_immediate(): + is_invalid_with_error(bytes.fromhex("b0"), "truncated CALLF immediate") + is_invalid_with_error(bytes.fromhex("b000"), "truncated CALLF immediate") + + +def test_jumpf_truncated_immediate(): + is_invalid_with_error(bytes.fromhex("b2"), "truncated JUMPF immediate") + is_invalid_with_error(bytes.fromhex("b200"), "truncated JUMPF immediate") + + +def test_callf_invalid_section_id(): + is_invalid_with_error(bytes.fromhex("b0000100"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b0000200"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b0000a00"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b0ffff00"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b0000a00"), "invalid section id", [FunctionType(0, 0)] * 10) + is_invalid_with_error(bytes.fromhex("b0ffff00"), "invalid section id", [FunctionType(0, 0)] * 65535) + + +def test_jumpf_invalid_section_id(): + is_invalid_with_error(bytes.fromhex("b2000100"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b2000200"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b2000a00"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b2ffff00"), "invalid section id", [FunctionType(0, 0)]) + is_invalid_with_error(bytes.fromhex("b2000a00"), "invalid section id", [FunctionType(0, 0)] * 10) + is_invalid_with_error(bytes.fromhex("b2ffff00"), "invalid section id", [FunctionType(0, 0)] * 65535) + + +def test_jumpf_incompatible_return_type(): + is_invalid_with_error(bytes.fromhex("b2000100"), "incompatible function type for JUMPF", [FunctionType(0, 0), FunctionType(0, 1)]) + + +def test_immediate_contains_opcode(): + # 0x5c byte which could be interpreted a RJUMP, but it's not because it's in PUSH data + assert is_valid_code(0, bytes.fromhex("605c001000")) == True + assert is_valid_code(0, bytes.fromhex("61005c001000")) == True + # 0x5d byte which could be interpreted a RJUMPI, but it's not because it's in PUSH data + assert is_valid_code(0, bytes.fromhex("605d001000")) == True + assert is_valid_code(0, bytes.fromhex("61005d001000")) == True + + # 0x60 byte which could be interpreted as PUSH, but it's not because it's in RJUMP data + # offset = -160 + assert is_valid_code(0, b'0x00' * 160 + bytes.fromhex("5cff6000")) == True + # 0x60 byte which could be interpreted as PUSH, but it's not because it's in RJUMPI data + # offset = -160 + assert is_valid_code(0, b'0x00' * 160 + bytes.fromhex("5dff6000")) == True + # 0x60 byte which could be interpreted as PUSH, but it's not because it's in CALLF data + # section_id = 96 + assert is_valid_code(0, bytes.fromhex("b0006000"), [FunctionType(0, 0)] * 97) == True + + # 0x5c byte which could be interpreted a RJUMP, but it's not because it's in CALLF data + # section_id = 92 + assert is_valid_code(0, bytes.fromhex("b0005c0000"), [FunctionType(0, 0)] * 93) == True + # 0x5d byte which could be interpreted a RJUMPI, but it's not because it's in CALLF data + # section_id = 93 + assert is_valid_code(0, bytes.fromhex("b0005d0000"), [FunctionType(0, 0)] * 94) == True \ No newline at end of file diff --git a/tests/test_eof/test_eip5450.nim b/tests/test_eof/test_eip5450.nim new file mode 100644 index 0000000000..90ce00af53 --- /dev/null +++ b/tests/test_eof/test_eip5450.nim @@ -0,0 +1,176 @@ +# nimbus-execution-client +# Copyright (c) 2018-2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +from eip5450 import validate_function, FunctionType, ValidationException +from eip5450_table import * +import pytest + + +def test_empty(): + assert validate_function(0, bytes((OP_STOP,))) == 0 + + +def test_stack_empty_at_exit(): + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(0, bytes((OP_NUMBER, OP_STOP))) + assert validate_function(0, bytes((OP_NUMBER, OP_POP, OP_STOP))) == 1 + assert validate_function(1, bytes((OP_POP, OP_STOP)), [FunctionType(0, 0), FunctionType(1, 0)]) == 1 + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(1, bytes((OP_STOP,)), [FunctionType(0, 0), FunctionType(1, 0)]) + + +def test_immediate_bytes(): + assert validate_function(0, bytes((OP_PUSH1, 0x01, OP_POP, OP_STOP))) == 1 + + +def test_stack_underflow(): + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_POP, OP_STOP))) + + +def test_jump_forward(): + assert validate_function(0, bytes((OP_RJUMP, 0x00, 0x00, OP_STOP))) == 0 + assert validate_function(0, bytes((OP_RJUMP, 0x00, 0x01, OP_NUMBER, OP_STOP))) == 0 + assert validate_function(0, bytes((OP_RJUMP, 0x00, 0x02, OP_NUMBER, OP_POP, OP_STOP))) == 0 + assert validate_function(0, bytes((OP_RJUMP, 0x00, 0x03, OP_ADD, OP_POP, OP_STOP, OP_PUSH1, 0x01, OP_PUSH1, 0x01, OP_RJUMP, 0xff, 0xf6, OP_STOP))) == 2 + + +def test_jump_backwards(): + assert validate_function(0, bytes((OP_RJUMP, 0xff, 0xfd, OP_STOP))) == 0 + assert validate_function(0, bytes((OP_JUMPDEST, OP_RJUMP, 0xff, 0xfc, OP_STOP))) == 0 + with pytest.raises(ValidationException, match="stack height mismatch for different paths"): + validate_function(0, bytes((OP_NUMBER, OP_RJUMP, 0xff, 0xfc, OP_POP, OP_STOP))) + with pytest.raises(ValidationException, match="stack height mismatch for different paths"): + validate_function(0, bytes((OP_NUMBER, OP_POP, OP_RJUMP, 0xff, 0xfc, OP_STOP))) + assert validate_function(0, bytes((OP_NUMBER, OP_POP, OP_RJUMP, 0xff, 0xfd, OP_STOP))) == 1 + assert validate_function(0, bytes((OP_NUMBER, OP_POP, OP_JUMPDEST, OP_RJUMP, 0xff, 0xfc, OP_STOP))) == 1 + assert validate_function(0, bytes((OP_NUMBER, OP_POP, OP_NUMBER, OP_RJUMP, 0xff, 0xfb, OP_POP, OP_STOP))) == 1 + with pytest.raises(ValidationException, match="stack height mismatch for different paths"): + validate_function(0, bytes((OP_NUMBER, OP_POP, OP_NUMBER, OP_RJUMP, 0xff, 0xfc, OP_POP, OP_STOP))) + +def test_conditional_jump(): + # Each branch ending with STOP + assert validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x02, OP_POP, OP_STOP, OP_POP, OP_STOP))) == 2 + # One branch ending with RJUMP + assert validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x04, OP_POP, OP_RJUMP, 0x00, 0x01, OP_POP, OP_STOP))) == 2 + # Fallthrough + assert validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x04, OP_DUP1, OP_DUP1, OP_POP, OP_POP, OP_POP, OP_STOP))) == 3 + # Offset 0 + assert validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x00, OP_POP, OP_STOP))) == 2 + # Simple loop (RJUMP offset = -5) + assert validate_function(0, bytes((OP_PUSH1, 0x01, OP_PUSH1, 0xff, OP_DUP2, OP_SUB, OP_DUP1, OP_RJUMPI, 0xff, 0xfa, OP_POP, OP_POP, OP_STOP))) == 3 + # One branch increasing max stack more stack than another + assert validate_function(0, bytes((OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x07, OP_ADDRESS, OP_ADDRESS, OP_ADDRESS, OP_POP, OP_POP, OP_POP, OP_STOP, OP_ADDRESS, OP_POP, OP_STOP))) == 3 + assert validate_function(0, bytes((OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x03, OP_ADDRESS, OP_POP, OP_STOP, OP_ADDRESS, OP_ADDRESS, OP_ADDRESS, OP_POP, OP_POP, OP_POP, OP_STOP))) == 3 + + # Missing stack argument + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_RJUMPI, 0x00, 0x00, OP_STOP))) + # Stack underflow in one branch + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x02, OP_POP, OP_STOP, OP_SUB, OP_POP, OP_STOP))) + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x02, OP_SUB, OP_STOP, OP_NOT, OP_POP, OP_STOP))) + # Stack not empty in the end of one branch + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x02, OP_POP, OP_STOP, OP_NOT, OP_STOP))) + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(0, bytes((OP_PUSH1, 0xff, OP_PUSH1, 0x01, OP_RJUMPI, 0x00, 0x02, OP_NOT, OP_STOP, OP_POP, OP_STOP))) + +def test_callf(): + # 0 inputs, 0 outpus + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x01, OP_STOP)), [FunctionType(0, 0), FunctionType(0, 0)]) == 0 + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x02, OP_STOP)), [FunctionType(0, 0), FunctionType(1, 1), FunctionType(0, 0)]) == 0 + + # more than 0 inputs + assert validate_function(0, bytes((OP_ADDRESS, OP_CALLF, 0x00, 0x01, OP_STOP)), [FunctionType(0, 0), FunctionType(1, 0)]) == 1 + # forwarding an argument + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x01, OP_STOP)), [FunctionType(1, 0), FunctionType(1, 0)]) == 1 + + # more than 1 inputs + assert validate_function(0, bytes((OP_ADDRESS, OP_DUP1, OP_CALLF, 0x00, 0x01, OP_STOP)), [FunctionType(0, 0), FunctionType(2, 0)]) == 2 + + # more than 0 outputs + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x01, OP_POP, OP_STOP)), [FunctionType(0, 0), FunctionType(0, 1)]) == 1 + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x02, OP_POP, OP_STOP)), [FunctionType(0, 0), FunctionType(0, 0), FunctionType(0, 1)]) == 1 + + # more than 1 outputs + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x01, OP_POP, OP_POP, OP_STOP)), [FunctionType(0, 0), FunctionType(0, 2)]) == 2 + + # more than 0 inputs, more than 0 outputs + assert validate_function(0, bytes((OP_ADDRESS, OP_ADDRESS, OP_CALLF, 0x00, 0x01, OP_POP, OP_POP, OP_POP, OP_STOP)), [FunctionType(0, 0), FunctionType(2, 3)]) == 3 + + # recursion + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x00, OP_STOP)), [FunctionType(0, 0)]) == 0 + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x00, OP_STOP)), [FunctionType(2, 0)]) == 2 + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x00, OP_POP, OP_POP, OP_STOP)), [FunctionType(2, 2)]) == 2 + assert validate_function(1, bytes((OP_ADDRESS, OP_ADDRESS, OP_CALLF, 0x00, 0x01, OP_POP, OP_POP, OP_POP, OP_STOP)), [FunctionType(0, 0), FunctionType(2, 1)]) == 4 + + # multiple CALLFs with different types + assert validate_function(0, bytes((OP_PREVRANDAO, OP_CALLF, 0x00, 0x01, OP_DUP1, OP_DUP1, OP_CALLF, 0x00, 0x02, + OP_PREVRANDAO, OP_DUP1, OP_CALLF, 0x00, 0x03, OP_POP, OP_POP, OP_STOP)), [FunctionType(0, 0), FunctionType(1, 1), FunctionType(3, 0), FunctionType(2, 2)]) == 3 + + # underflow + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_CALLF, 0x00, 0x01, OP_STOP)), [FunctionType(0, 0), FunctionType(1, 0)]) + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_ADDRESS, OP_CALLF, 0x00, 0x01, OP_STOP)), [FunctionType(0, 0), FunctionType(2, 0)]) + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_POP, OP_CALLF, 0x00, 0x00, OP_STOP)), [FunctionType(1, 0)]) + with pytest.raises(ValidationException, match="stack underflow"): + validate_function(0, bytes((OP_PREVRANDAO, OP_CALLF, 0x00, 0x01, OP_DUP1, OP_CALLF, 0x00, 0x02, OP_STOP)), + [FunctionType(0, 0), FunctionType(1, 1), FunctionType(3, 0)]) + +def test_retf(): + # 0 outpus + assert validate_function(0, bytes((OP_RETF,)), [FunctionType(0, 0), FunctionType(0, 0)]) == 0 + assert validate_function(1, bytes((OP_RETF,)), [FunctionType(0, 0), FunctionType(0, 0)]) == 0 + assert validate_function(2, bytes((OP_RETF,)), [FunctionType(0, 0), FunctionType(1, 1), FunctionType(0, 0)]) == 0 + + # more than 0 outputs + assert validate_function(0, bytes((OP_PREVRANDAO, OP_RETF)), [FunctionType(0, 1), FunctionType(0, 1)]) == 1 + assert validate_function(1, bytes((OP_PREVRANDAO, OP_RETF)), [FunctionType(0, 1), FunctionType(0, 1)]) == 1 + + # more than 1 outputs + assert validate_function(1, bytes((OP_PREVRANDAO, OP_DUP1, OP_RETF)), [FunctionType(0, 0), FunctionType(0, 2)]) == 2 + + # forwarding return value + assert validate_function(0, bytes((OP_RETF,)), [FunctionType(1, 1)]) == 1 + assert validate_function(0, bytes((OP_CALLF, 0x00, 0x01, OP_RETF)), [FunctionType(0, 1), FunctionType(0, 1)]) == 1 + + # multiple RETFs + assert validate_function(0, bytes((OP_RJUMPI, 0x00, 0x03, OP_PREVRANDAO, OP_DUP1, OP_RETF, OP_ADDRESS, OP_DUP1, OP_RETF)), [FunctionType(1, 2)]) == 2 + + # underflow + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(0, bytes((OP_RETF,)), [FunctionType(0, 1), FunctionType(0, 1)]) + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(1, bytes((OP_RETF,)), [FunctionType(0, 1), FunctionType(0, 1)]) + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(0, bytes((OP_RETF,)), [FunctionType(0, 1)]) + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(1, bytes((OP_PREVRANDAO, OP_RETF)), [FunctionType(0, 0), FunctionType(0, 2)]) + with pytest.raises(ValidationException, match="non-empty stack on terminating instruction"): + validate_function(0, bytes((OP_RJUMPI, 0x00, 0x03, OP_PREVRANDAO, OP_DUP1, OP_RETF, OP_ADDRESS, OP_RETF)), [FunctionType(1, 2)]) + + +def test_unreachable(): + # Max stack not changed by unreachable code + assert validate_function(0, bytes((OP_ADDRESS, OP_POP, OP_STOP, OP_ADDRESS, OP_ADDRESS, OP_ADDRESS, OP_POP, OP_POP, OP_POP, OP_STOP))) == 1 + assert validate_function(0, bytes((OP_ADDRESS, OP_POP, OP_RETF, OP_ADDRESS, OP_ADDRESS, OP_ADDRESS, OP_POP, OP_POP, OP_POP, OP_STOP))) == 1 + assert validate_function(0, bytes((OP_ADDRESS, OP_POP, OP_RJUMP, 0x00, 0x06, OP_ADDRESS, OP_ADDRESS, OP_ADDRESS, OP_POP, OP_POP, OP_POP, OP_STOP))) == 1 + # Stack underflow in unreachable code + assert validate_function(0, bytes((OP_ADDRESS, OP_POP, OP_STOP, OP_POP, OP_STOP))) == 1 + assert validate_function(0, bytes((OP_ADDRESS, OP_POP, OP_RETF, OP_POP, OP_STOP))) == 1 + assert validate_function(0, bytes((OP_ADDRESS, OP_POP, OP_RJUMP, 0x00, 0x01, OP_POP, OP_STOP))) == 1 + +def test_stack_overflow(): + assert validate_function(0, bytes([OP_NUMBER] * 1022 + [OP_POP] * 1022 + [OP_STOP])) == 1022 + with pytest.raises(ValidationException, match="max stack above limit"): + validate_function(0, bytes([OP_NUMBER] * 1023 + [OP_POP] * 1023 + [OP_STOP])) diff --git a/tests/test_eof/test_eof1_validation.nim b/tests/test_eof/test_eof1_validation.nim new file mode 100644 index 0000000000..df07bf9116 --- /dev/null +++ b/tests/test_eof/test_eof1_validation.nim @@ -0,0 +1,83 @@ +# nimbus-execution-client +# Copyright (c) 2018-2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +from eof1_validation import * +import pytest + + +def is_valid_eof(code: bytes) -> bool: + try: + validate_eof1(code) + except ValidationException: + return False + return True + + +def is_invalid_eof_with_error(code: bytes, error: str): + with pytest.raises(ValidationException, match=error): + validate_eof1(code) + + +def test_read_eof1_header(): + # Code and data section + assert read_eof1_header(bytes.fromhex('ef000101000102000100feaa')) \ + == EOF([FunctionType(0, 0)], [bytes.fromhex('fe')]) + + # Valid with one code section and implicit type section + assert read_eof1_header(bytes.fromhex('ef0001 010001 00 fe')) \ + == EOF([FunctionType(0, 0)], [bytes.fromhex('fe')]) + + # Valid with one code section and explicit type section + assert read_eof1_header(bytes.fromhex('ef0001 030002 010001 00 0000 fe')) \ + == EOF([FunctionType(0, 0)], [bytes.fromhex('fe')]) + + # Valid with two code sections, 2nd code sections has 0 inputs and 1 output + assert read_eof1_header(bytes.fromhex('ef0001 030004 010001 010003 00 00000001 fe 6000fc')) \ + == EOF([FunctionType(0, 0), FunctionType(0, 1)], [bytes.fromhex('fe'), bytes.fromhex('6000fc')]) + + # Valid with two code sections, 2nd code sections has 2 inputs and 0 outputs + assert read_eof1_header(bytes.fromhex('ef0001 030004 010001 010003 00 00000200 fe 5050fc')) \ + == EOF([FunctionType(0, 0), FunctionType(2, 0)], [bytes.fromhex('fe'), bytes.fromhex('5050fc')]) + + # Valid with two code sections, 2nd code sections has 2 inputs and 1 output + assert read_eof1_header(bytes.fromhex('ef0001 030004 010001 010002 00 00000201 fe 50fc')) \ + == EOF([FunctionType(0, 0), FunctionType(2, 1)], [bytes.fromhex('fe'), bytes.fromhex('50fc')]) + + # Valid with two code sections and one data section + assert read_eof1_header(bytes.fromhex('ef0001 030004 010001 010002 020004 00 00000201 fe 50fc aabbccdd')) \ + == EOF([FunctionType(0, 0), FunctionType(2, 1)], [bytes.fromhex('fe'), bytes.fromhex('50fc')]) + + +def test_valid_eof1_container(): + # Single code section + assert is_valid_eof(bytes.fromhex("ef000101000100fe")) + # Code section and data section + assert is_valid_eof(bytes.fromhex("ef000101000102000100feaa")) + # Type section and two code sections + assert is_valid_eof(bytes.fromhex("ef0001 030004 010001 010003 00 00000001 fe 6000b1")) + # Type section, two code sections, data section + assert is_valid_eof(bytes.fromhex("ef0001 030004 010001 010002 020004 00 00000201 fe 50b1 aabbccdd")) + + # Example with 3 functions + assert is_valid_eof(bytes.fromhex("ef0001 030006 01003b 010017 01001d 00 000001010101 " + "60043560e06000351c639b0890d581145d001c6320cb776181145d00065050600080fd50b0000260005260206000f350b0000160005260206000f3" + "600181115d0004506001b160018103b0000281029050b1 600281115d0004506001b160028103b0000160018203b00001019050b1")) + + +def test_invalid_eof1_container(): + # EIP-3540 violation - malformed container + is_invalid_eof_with_error(bytes.fromhex("ef0001 010001 020002 00 fe aa"), "container size not equal to sum of section sizes") + # EIP-3670 violation - undefined opcode + is_invalid_eof_with_error(bytes.fromhex("ef0001 010002 00 f600"), "undefined instruction") + # EIP-4200 violation - invalid RJUMP + is_invalid_eof_with_error(bytes.fromhex("ef0001 010004 00 5c00ff00"), "relative jump destination out of bounds") + # EIP-4750 violation - invalid CALLF + is_invalid_eof_with_error(bytes.fromhex("ef0001 030004 010005 010003 00 00000001 b0ffff5000 6000b1"), "invalid section id") + # EIP-5450 violation - stack underflow + is_invalid_eof_with_error(bytes.fromhex("ef0001 030004 010005 010004 00 00000001 b000015000 600001b1"), "stack underflow") \ No newline at end of file