From 50c241de78cc8623db586ae36778c718630cdb2e Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Thu, 24 Feb 2022 10:31:58 +0800 Subject: [PATCH 01/12] feat: calldataload specs (squashed and rebased) --- specs/opcode/35CALLDATALOAD.md | 38 +++++++ src/zkevm_specs/evm/execution/__init__.py | 2 + src/zkevm_specs/evm/execution/calldataload.py | 34 ++++++ tests/evm/test_calldataload.py | 102 ++++++++++++++++++ tests/evm/test_calldatasize.py | 1 - 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 specs/opcode/35CALLDATALOAD.md create mode 100644 src/zkevm_specs/evm/execution/calldataload.py create mode 100644 tests/evm/test_calldataload.py diff --git a/specs/opcode/35CALLDATALOAD.md b/specs/opcode/35CALLDATALOAD.md new file mode 100644 index 000000000..403d1328a --- /dev/null +++ b/specs/opcode/35CALLDATALOAD.md @@ -0,0 +1,38 @@ +# CALLDATALOAD opcode + +## Procedure + +The `CALLDATALOAD` opcode gets input data of current environment. + +## EVM Behaviour + +Stack input is the byte offset to read call data from. Stack output is a 32-byte value starting from the given offset of the call data. All bytes after the end of the call data are set to `0`. + +## Circuit Behaviour + +1. Do busmapping lookup for stack read operation. +2. Construct call context table in RW table (for `tx.id`). +3. Construct tx context table in RW table (for every single byte of `tx.calldata`). +4. Do busmapping lookup for call context `txid` read operation. +5. Do busmapping lookup for tx context `calldata` read operation (for each one of the 32 bytes of the stack output). +6. Do busmapping lookup for stack write operation. + +## Constraints + +1. opId == 0x35 +2. State Transition: + - gc + 3 (1 stack read, 1 call context read, 1 stack write) + - stack_pointer unchanged + - pc + 1 + - gas - 3 +3. Lookups: + - for index `i in range(32)`: `stack_top[i]` is in the RW table {tx context, call data, i} + +## Exceptions + +1. Stack overflow: stack is full, stack pointer = 0 +2. Out of gas: remaining gas is not enough for this opcode + +## Code + +Please refer to `src/zkevm_specs/evm/execution/calldataload.py`. diff --git a/src/zkevm_specs/evm/execution/__init__.py b/src/zkevm_specs/evm/execution/__init__.py index be699cbec..6adff7800 100644 --- a/src/zkevm_specs/evm/execution/__init__.py +++ b/src/zkevm_specs/evm/execution/__init__.py @@ -15,6 +15,7 @@ from .caller import * from .callvalue import * from .calldatacopy import * +from .calldataload import * from .gas import * from .jump import * from .jumpi import * @@ -35,6 +36,7 @@ ExecutionState.CALLER: caller, ExecutionState.CALLVALUE: callvalue, ExecutionState.CALLDATACOPY: calldatacopy, + ExecutionState.CALLDATALOAD: calldataload, ExecutionState.CALLDATASIZE: calldatasize, ExecutionState.COINBASE: coinbase, ExecutionState.TIMESTAMP: timestamp, diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py new file mode 100644 index 000000000..a4f551379 --- /dev/null +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -0,0 +1,34 @@ +from ..instruction import Instruction, Transition +from ..opcode import Opcode +from ..table import RW, CallContextFieldTag, TxContextFieldTag +from ..util import BufferReaderGadget +from ...util.param import N_BYTES_WORD + + +def calldataload(instruction: Instruction): + opcode = instruction.opcode_lookup(True) + instruction.constrain_equal(opcode, Opcode.CALLDATALOAD) + + # callldata_start is the 64-bit offset to start reading 32-bytes from calldata. + calldata_start = instruction.rlc_to_fq_exact(instruction.stack_pop(), n_bytes=8) + calldata_end = calldata_start + N_BYTES_WORD + + tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId, RW.Read) + calldata_size = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CallDataLength) + + expected_stack_top = instruction.rlc_to_le_bytes(instruction.stack_push()) + + bytes_left = N_BYTES_WORD if calldata_size.n > calldata_end.n else calldata_size - calldata_start + buffer_reader = BufferReaderGadget(instruction, N_BYTES_WORD, calldata_start, calldata_end, bytes_left) + for idx in range(32): + if buffer_reader.read_flag(idx): + buffer_reader.constrain_byte(idx, instruction.tx_calldata_lookup(tx_id, calldata_start + idx)) + else: + buffer_reader.constrain_byte(idx, 0) + + instruction.step_state_transition_in_same_context( + opcode, + rw_counter=Transition.delta(3), + program_counter=Transition.delta(1), + stack_pointer=Transition.delta(0), + ) diff --git a/tests/evm/test_calldataload.py b/tests/evm/test_calldataload.py new file mode 100644 index 000000000..e2acbd8b4 --- /dev/null +++ b/tests/evm/test_calldataload.py @@ -0,0 +1,102 @@ +import pytest + +from zkevm_specs.evm import ( + Bytecode, + CallContextFieldTag, + ExecutionState, + RW, + RWTableTag, + StepState, + Tables, + Transaction, + verify_steps, +) +from zkevm_specs.util import rand_fp, RLC, U64 + +TESTING_DATA = ( + ( + bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0x00, + bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + ), + ( + bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0x1F, + bytes.fromhex("FF00000000000000000000000000000000000000000000000000000000000000"), + ), + ( + bytes.fromhex("a1bacf5488bfafc33bad736db41f06866eaeb35e1c1dd81dfc268357ec98563f"), + 0x10, + bytes.fromhex("6eaeb35e1c1dd81dfc268357ec98563f00000000000000000000000000000000"), + ), +) + + +@pytest.mark.parametrize("call_data, offset, expected_stack_top", TESTING_DATA) +def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes): + randomness = rand_fp() + + tx = Transaction( + id=1, + call_data=call_data, + ) + + offset = RLC(offset, randomness) + expected_stack_top = RLC(expected_stack_top, randomness) + + bytecode = Bytecode().push(offset, n_bytes=32).calldataload().stop() + bytecode_hash = RLC(bytecode.hash(), randomness) + + tables = Tables( + block_table=set(), + tx_table=set(tx.table_assignments(randomness)), + bytecode_table=set(bytecode.table_assignments(randomness)), + rw_table=set( + [ + (1, RW.Write, RWTableTag.Stack, 1, 1023, 0, offset, 0, 0, 0), + (2, RW.Read, RWTableTag.Stack, 1, 1023, 0, offset, 0, 0, 0), + (3, RW.Read, RWTableTag.CallContext, 1, CallContextFieldTag.TxId, 0, 1, 0, 0, 0), + (4, RW.Write, RWTableTag.Stack, 1, 1023, 0, expected_stack_top, 0, 0, 0), + ] + ), + ) + + verify_steps( + randomness=randomness, + tables=tables, + steps=[ + StepState( + execution_state=ExecutionState.PUSH, + rw_counter=1, + call_id=1, + is_root=True, + is_create=False, + code_source=bytecode_hash, + program_counter=0, + stack_pointer=1024, + gas_left=6, + ), + StepState( + execution_state=ExecutionState.CALLDATALOAD, + rw_counter=2, + call_id=1, + is_root=True, + is_create=False, + code_source=bytecode_hash, + program_counter=33, + stack_pointer=1023, + gas_left=3, + ), + StepState( + execution_state=ExecutionState.STOP, + rw_counter=5, + call_id=1, + is_root=True, + is_create=False, + code_source=bytecode_hash, + program_counter=34, + stack_pointer=1023, + gas_left=0, + ), + ], + ) diff --git a/tests/evm/test_calldatasize.py b/tests/evm/test_calldatasize.py index 156f7bce0..3f8a1879a 100644 --- a/tests/evm/test_calldatasize.py +++ b/tests/evm/test_calldatasize.py @@ -3,7 +3,6 @@ from zkevm_specs.evm import ( ExecutionState, StepState, - Opcode, verify_steps, Tables, RWTableTag, From 31b679cf1d466ea6dc158ae4a3f2453b580342fc Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Thu, 24 Feb 2022 10:35:35 +0800 Subject: [PATCH 02/12] fix: linting --- src/zkevm_specs/evm/execution/calldataload.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index a4f551379..8d6db6969 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -18,11 +18,17 @@ def calldataload(instruction: Instruction): expected_stack_top = instruction.rlc_to_le_bytes(instruction.stack_push()) - bytes_left = N_BYTES_WORD if calldata_size.n > calldata_end.n else calldata_size - calldata_start - buffer_reader = BufferReaderGadget(instruction, N_BYTES_WORD, calldata_start, calldata_end, bytes_left) + bytes_left = ( + N_BYTES_WORD if calldata_size.n > calldata_end.n else calldata_size - calldata_start + ) + buffer_reader = BufferReaderGadget( + instruction, N_BYTES_WORD, calldata_start, calldata_end, bytes_left + ) for idx in range(32): if buffer_reader.read_flag(idx): - buffer_reader.constrain_byte(idx, instruction.tx_calldata_lookup(tx_id, calldata_start + idx)) + buffer_reader.constrain_byte( + idx, instruction.tx_calldata_lookup(tx_id, calldata_start + idx) + ) else: buffer_reader.constrain_byte(idx, 0) From 48481f6e2693e156e705ae3b62a24f14626031c4 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Tue, 1 Mar 2022 13:30:35 +0800 Subject: [PATCH 03/12] feat: account for internal calls --- src/zkevm_specs/evm/execution/calldataload.py | 22 +++++-- src/zkevm_specs/evm/instruction.py | 7 +- tests/evm/test_calldataload.py | 64 +++++++++++++------ 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index 8d6db6969..dd1e2458d 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -14,23 +14,33 @@ def calldataload(instruction: Instruction): calldata_end = calldata_start + N_BYTES_WORD tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId, RW.Read) - calldata_size = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CallDataLength) - - expected_stack_top = instruction.rlc_to_le_bytes(instruction.stack_push()) + calldata_size = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CallDataLength) bytes_left = ( N_BYTES_WORD if calldata_size.n > calldata_end.n else calldata_size - calldata_start ) buffer_reader = BufferReaderGadget( instruction, N_BYTES_WORD, calldata_start, calldata_end, bytes_left ) + + calldata_word = [] for idx in range(32): if buffer_reader.read_flag(idx): - buffer_reader.constrain_byte( - idx, instruction.tx_calldata_lookup(tx_id, calldata_start + idx) - ) + if instruction.curr.is_root: + tx_byte = instruction.tx_calldata_lookup(tx_id, calldata_start + idx) + buffer_reader.constrain_byte(idx, tx_byte) + calldata_word.append(int(tx_byte)) + else: + mem_byte = instruction.memory_lookup(RW.Read, calldata_start + idx) + buffer_reader.constrain_byte(idx, mem_byte) + calldata_word.append(int(mem_byte)) else: buffer_reader.constrain_byte(idx, 0) + calldata_word.append(0) + + calldata_word = bytes(calldata_word) + expected_stack_top = instruction.stack_push() + instruction.constrain_equal(expected_stack_top, instruction.bytes_to_rlc(calldata_word)) instruction.step_state_transition_in_same_context( opcode, diff --git a/src/zkevm_specs/evm/instruction.py b/src/zkevm_specs/evm/instruction.py index d37b1c4a8..254020f95 100644 --- a/src/zkevm_specs/evm/instruction.py +++ b/src/zkevm_specs/evm/instruction.py @@ -230,8 +230,8 @@ def constant_divmod( def compare(self, lhs: FQ, rhs: FQ, n_bytes: int) -> Tuple[bool, bool]: assert n_bytes <= MAX_N_BYTES, "Too many bytes to composite an integer in field" - assert lhs.n < 256**n_bytes, f"lhs {lhs} exceeds the range of {n_bytes} bytes" - assert rhs.n < 256**n_bytes, f"rhs {rhs} exceeds the range of {n_bytes} bytes" + assert lhs.n < 256 ** n_bytes, f"lhs {lhs} exceeds the range of {n_bytes} bytes" + assert rhs.n < 256 ** n_bytes, f"rhs {rhs} exceeds the range of {n_bytes} bytes" return lhs.n < rhs.n, lhs.n == rhs.n def min(self, lhs: FQ, rhs: FQ, n_bytes: int) -> FQ: @@ -302,6 +302,9 @@ def word_to_lo_hi(self, word: RLC) -> Tuple[FQ, FQ]: def int_to_rlc(self, value: int, n_bytes: int) -> RLC: return RLC(value, self.randomness, n_bytes) + def bytes_to_rlc(self, value: bytes) -> RLC: + return RLC(value, self.randomness, len(value)) + def bytes_to_int(self, value: bytes) -> int: assert len(value) <= MAX_N_BYTES, "Too many bytes to composite an integer in field" return int.from_bytes(value, "little") diff --git a/tests/evm/test_calldataload.py b/tests/evm/test_calldataload.py index e2acbd8b4..93cc5ae79 100644 --- a/tests/evm/test_calldataload.py +++ b/tests/evm/test_calldataload.py @@ -18,47 +18,73 @@ bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), 0x00, bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + True, ), ( bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), 0x1F, bytes.fromhex("FF00000000000000000000000000000000000000000000000000000000000000"), + True, ), ( bytes.fromhex("a1bacf5488bfafc33bad736db41f06866eaeb35e1c1dd81dfc268357ec98563f"), 0x10, bytes.fromhex("6eaeb35e1c1dd81dfc268357ec98563f00000000000000000000000000000000"), + True, + ), + ( + bytes.fromhex("a1bacf5488bfafc33bad736db41f06866eaeb35e1c1dd81dfc268357ec98563f"), + 0x10, + bytes.fromhex("6eaeb35e1c1dd81dfc268357ec98563f00000000000000000000000000000000"), + False, ), ) -@pytest.mark.parametrize("call_data, offset, expected_stack_top", TESTING_DATA) -def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes): +@pytest.mark.parametrize("call_data, offset, expected_stack_top, is_root", TESTING_DATA) +def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, is_root: bool): randomness = rand_fp() - tx = Transaction( - id=1, - call_data=call_data, - ) + tx = Transaction(id=1, call_data=call_data) - offset = RLC(offset, randomness) + offset_rlc = RLC(offset, randomness) expected_stack_top = RLC(expected_stack_top, randomness) - bytecode = Bytecode().push(offset, n_bytes=32).calldataload().stop() + bytecode = Bytecode().push(offset_rlc, n_bytes=32).calldataload().stop() bytecode_hash = RLC(bytecode.hash(), randomness) + rws = set( + [ + (1, RW.Write, RWTableTag.Stack, 1, 1023, 0, offset_rlc, 0, 0, 0), + (2, RW.Read, RWTableTag.Stack, 1, 1023, 0, offset_rlc, 0, 0, 0), + (3, RW.Read, RWTableTag.CallContext, 1, CallContextFieldTag.TxId, 0, 1, 0, 0, 0), + ] + ) + if is_root: + rws.add((4, RW.Write, RWTableTag.Stack, 1, 1023, 0, expected_stack_top, 0, 0, 0)) + else: + for i in range(offset, len(call_data)): + rws.add((4 + i - offset, RW.Read, RWTableTag.Memory, 1, i, 0, call_data[i], 0, 0, 0)) + rws.add( + ( + 4 + len(call_data) - offset, + RW.Write, + RWTableTag.Stack, + 1, + 1023, + 0, + expected_stack_top, + 0, + 0, + 0, + ) + ) + tables = Tables( block_table=set(), tx_table=set(tx.table_assignments(randomness)), bytecode_table=set(bytecode.table_assignments(randomness)), - rw_table=set( - [ - (1, RW.Write, RWTableTag.Stack, 1, 1023, 0, offset, 0, 0, 0), - (2, RW.Read, RWTableTag.Stack, 1, 1023, 0, offset, 0, 0, 0), - (3, RW.Read, RWTableTag.CallContext, 1, CallContextFieldTag.TxId, 0, 1, 0, 0, 0), - (4, RW.Write, RWTableTag.Stack, 1, 1023, 0, expected_stack_top, 0, 0, 0), - ] - ), + rw_table=rws, ) verify_steps( @@ -69,7 +95,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes): execution_state=ExecutionState.PUSH, rw_counter=1, call_id=1, - is_root=True, + is_root=is_root, is_create=False, code_source=bytecode_hash, program_counter=0, @@ -80,7 +106,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes): execution_state=ExecutionState.CALLDATALOAD, rw_counter=2, call_id=1, - is_root=True, + is_root=is_root, is_create=False, code_source=bytecode_hash, program_counter=33, @@ -91,7 +117,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes): execution_state=ExecutionState.STOP, rw_counter=5, call_id=1, - is_root=True, + is_root=is_root, is_create=False, code_source=bytecode_hash, program_counter=34, From 691c871a299f8a2dba73b788f14c7594d7f72e24 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Tue, 1 Mar 2022 14:17:43 +0800 Subject: [PATCH 04/12] fix: linting issue after upgrading black --- src/zkevm_specs/evm/instruction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zkevm_specs/evm/instruction.py b/src/zkevm_specs/evm/instruction.py index 254020f95..bf5e36a45 100644 --- a/src/zkevm_specs/evm/instruction.py +++ b/src/zkevm_specs/evm/instruction.py @@ -230,8 +230,8 @@ def constant_divmod( def compare(self, lhs: FQ, rhs: FQ, n_bytes: int) -> Tuple[bool, bool]: assert n_bytes <= MAX_N_BYTES, "Too many bytes to composite an integer in field" - assert lhs.n < 256 ** n_bytes, f"lhs {lhs} exceeds the range of {n_bytes} bytes" - assert rhs.n < 256 ** n_bytes, f"rhs {rhs} exceeds the range of {n_bytes} bytes" + assert lhs.n < 256**n_bytes, f"lhs {lhs} exceeds the range of {n_bytes} bytes" + assert rhs.n < 256**n_bytes, f"rhs {rhs} exceeds the range of {n_bytes} bytes" return lhs.n < rhs.n, lhs.n == rhs.n def min(self, lhs: FQ, rhs: FQ, n_bytes: int) -> FQ: From 5a20c5fb13bbd4e5b737051ceec23f11bed21b61 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Tue, 1 Mar 2022 19:12:49 +0800 Subject: [PATCH 05/12] fix: account for call data offset in the case of internal call --- src/zkevm_specs/evm/execution/calldataload.py | 28 ++++++++++------ tests/evm/test_calldataload.py | 32 +++++++++++++++++-- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index dd1e2458d..e178c8bd7 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -9,29 +9,37 @@ def calldataload(instruction: Instruction): opcode = instruction.opcode_lookup(True) instruction.constrain_equal(opcode, Opcode.CALLDATALOAD) - # callldata_start is the 64-bit offset to start reading 32-bytes from calldata. - calldata_start = instruction.rlc_to_fq_exact(instruction.stack_pop(), n_bytes=8) - calldata_end = calldata_start + N_BYTES_WORD + # offset is the 64-bit offset to start reading 32-bytes from start of calldata. + offset = instruction.rlc_to_fq_exact(instruction.stack_pop(), n_bytes=8) tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId, RW.Read) - calldata_size = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CallDataLength) - bytes_left = ( - N_BYTES_WORD if calldata_size.n > calldata_end.n else calldata_size - calldata_start - ) + if instruction.curr.is_root: + calldata_length = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CallDataLength) + calldata_offset = 0 + src_addr = offset + src_addr_end = calldata_length + else: + calldata_length = instruction.call_context_lookup(CallContextFieldTag.CallDataLength) + calldata_offset = instruction.call_context_lookup(CallContextFieldTag.CallDataOffset) + src_addr = offset + calldata_offset + src_addr_end = calldata_offset + calldata_length + + bytes_left = N_BYTES_WORD if calldata_length.n > src_addr_end.n else src_addr_end - src_addr + print("bytes left = ", bytes_left) buffer_reader = BufferReaderGadget( - instruction, N_BYTES_WORD, calldata_start, calldata_end, bytes_left + instruction, N_BYTES_WORD, src_addr, src_addr_end, bytes_left ) calldata_word = [] for idx in range(32): if buffer_reader.read_flag(idx): if instruction.curr.is_root: - tx_byte = instruction.tx_calldata_lookup(tx_id, calldata_start + idx) + tx_byte = instruction.tx_calldata_lookup(tx_id, offset + idx) buffer_reader.constrain_byte(idx, tx_byte) calldata_word.append(int(tx_byte)) else: - mem_byte = instruction.memory_lookup(RW.Read, calldata_start + idx) + mem_byte = instruction.memory_lookup(RW.Read, offset + idx) buffer_reader.constrain_byte(idx, mem_byte) calldata_word.append(int(mem_byte)) else: diff --git a/tests/evm/test_calldataload.py b/tests/evm/test_calldataload.py index 93cc5ae79..7b2fc640a 100644 --- a/tests/evm/test_calldataload.py +++ b/tests/evm/test_calldataload.py @@ -63,11 +63,39 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, if is_root: rws.add((4, RW.Write, RWTableTag.Stack, 1, 1023, 0, expected_stack_top, 0, 0, 0)) else: + rws.add( + ( + 4, + RW.Read, + RWTableTag.CallContext, + 1, + CallContextFieldTag.CallDataLength, + 0, + len(call_data), + 0, + 0, + 0, + ) + ) + rws.add( + ( + 5, + RW.Read, + RWTableTag.CallContext, + 1, + CallContextFieldTag.CallDataOffset, + 0, + 0, + 0, + 0, + 0, + ) + ) for i in range(offset, len(call_data)): - rws.add((4 + i - offset, RW.Read, RWTableTag.Memory, 1, i, 0, call_data[i], 0, 0, 0)) + rws.add((6 + i - offset, RW.Read, RWTableTag.Memory, 1, i, 0, call_data[i], 0, 0, 0)) rws.add( ( - 4 + len(call_data) - offset, + 6 + len(call_data) - offset, RW.Write, RWTableTag.Stack, 1, From 23718dbc06fef7b0429c5f40ba55af79af4a3e59 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Thu, 3 Mar 2022 10:23:57 +0800 Subject: [PATCH 06/12] fix: minor refactor and rw_counter for root vs internal --- src/zkevm_specs/evm/execution/calldataload.py | 13 +++++-------- tests/evm/test_calldataload.py | 4 +++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index e178c8bd7..d8f98d95f 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -17,18 +17,15 @@ def calldataload(instruction: Instruction): if instruction.curr.is_root: calldata_length = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CallDataLength) calldata_offset = 0 - src_addr = offset - src_addr_end = calldata_length else: calldata_length = instruction.call_context_lookup(CallContextFieldTag.CallDataLength) calldata_offset = instruction.call_context_lookup(CallContextFieldTag.CallDataOffset) - src_addr = offset + calldata_offset - src_addr_end = calldata_offset + calldata_length - bytes_left = N_BYTES_WORD if calldata_length.n > src_addr_end.n else src_addr_end - src_addr - print("bytes left = ", bytes_left) + src_addr = offset + calldata_offset + src_addr_end = calldata_length + calldata_offset + buffer_reader = BufferReaderGadget( - instruction, N_BYTES_WORD, src_addr, src_addr_end, bytes_left + instruction, N_BYTES_WORD, src_addr, src_addr_end, N_BYTES_WORD ) calldata_word = [] @@ -52,7 +49,7 @@ def calldataload(instruction: Instruction): instruction.step_state_transition_in_same_context( opcode, - rw_counter=Transition.delta(3), + rw_counter=Transition.delta(instruction.rw_counter_offset), program_counter=Transition.delta(1), stack_pointer=Transition.delta(0), ) diff --git a/tests/evm/test_calldataload.py b/tests/evm/test_calldataload.py index 7b2fc640a..4dd87a7df 100644 --- a/tests/evm/test_calldataload.py +++ b/tests/evm/test_calldataload.py @@ -62,6 +62,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, ) if is_root: rws.add((4, RW.Write, RWTableTag.Stack, 1, 1023, 0, expected_stack_top, 0, 0, 0)) + rw_counter_stop = 5 else: rws.add( ( @@ -107,6 +108,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, 0, ) ) + rw_counter_stop = 7 + len(call_data) - offset tables = Tables( block_table=set(), @@ -143,7 +145,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, ), StepState( execution_state=ExecutionState.STOP, - rw_counter=5, + rw_counter=rw_counter_stop, call_id=1, is_root=is_root, is_create=False, From 354a500e7d0a5403f085a8197fc2d47afcbb089a Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Thu, 3 Mar 2022 10:29:42 +0800 Subject: [PATCH 07/12] chore: refactoring --- src/zkevm_specs/evm/execution/calldataload.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index d8f98d95f..ce527c744 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -29,7 +29,7 @@ def calldataload(instruction: Instruction): ) calldata_word = [] - for idx in range(32): + for idx in range(N_BYTES_WORD): if buffer_reader.read_flag(idx): if instruction.curr.is_root: tx_byte = instruction.tx_calldata_lookup(tx_id, offset + idx) @@ -43,9 +43,10 @@ def calldataload(instruction: Instruction): buffer_reader.constrain_byte(idx, 0) calldata_word.append(0) - calldata_word = bytes(calldata_word) - expected_stack_top = instruction.stack_push() - instruction.constrain_equal(expected_stack_top, instruction.bytes_to_rlc(calldata_word)) + instruction.constrain_equal( + instruction.stack_push(), + instruction.bytes_to_rlc(bytes(calldata_word)), + ) instruction.step_state_transition_in_same_context( opcode, From dfb29210abd7ea0d32bb5f0ebf1d26399f733db1 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Fri, 4 Mar 2022 09:56:44 +0800 Subject: [PATCH 08/12] fix: update specs as per implementation --- specs/opcode/35CALLDATALOAD.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/specs/opcode/35CALLDATALOAD.md b/specs/opcode/35CALLDATALOAD.md index 403d1328a..bdd11fa46 100644 --- a/specs/opcode/35CALLDATALOAD.md +++ b/specs/opcode/35CALLDATALOAD.md @@ -8,25 +8,27 @@ The `CALLDATALOAD` opcode gets input data of current environment. Stack input is the byte offset to read call data from. Stack output is a 32-byte value starting from the given offset of the call data. All bytes after the end of the call data are set to `0`. -## Circuit Behaviour - -1. Do busmapping lookup for stack read operation. -2. Construct call context table in RW table (for `tx.id`). -3. Construct tx context table in RW table (for every single byte of `tx.calldata`). -4. Do busmapping lookup for call context `txid` read operation. -5. Do busmapping lookup for tx context `calldata` read operation (for each one of the 32 bytes of the stack output). -6. Do busmapping lookup for stack write operation. - ## Constraints 1. opId == 0x35 2. State Transition: - - gc + 3 (1 stack read, 1 call context read, 1 stack write) + - if is_root_call: + - rw_counter += 3 (1 stack read, 1 call context read, 1 stack write) + - if is_internal_call: + - rw_counter += rw_counter_offset ∈ {4, 5, ..., 35, 36} (1 stack read, 2 call context reads, i ∈ {0, 1, ..., 31, 32} memory reads, 1 stack write) - stack_pointer unchanged - pc + 1 - gas - 3 3. Lookups: - - for index `i in range(32)`: `stack_top[i]` is in the RW table {tx context, call data, i} + - `offset` is at the top of the stack + - `tx_id` is in the RW table (call context) + - if is_root_call (where `src_addr = offset`): + - `calldata_length` is in the TX table + - i ∈ {0, 1, ..., 31, 32} lookups for `i in range(32)`: if `buffer.read_flag(i)` then the i'th byte of the element on top of the stack `calldata_word[i]` is in the TX table {tx id, call data, src_addr + i} + - if is_internal_call (where `src_addr = offset + calldata_offset`): + - `calldata_length` is in the RW table (call context) + - `calldata_offset` is in the RW table (call context) + - i ∈ {0, 1, ..., 31, 32} lookups for `i in range(32)`: if `buffer.read_flag(i)` then the i'th byte of the element on top of the stack `calldata_word[i]` is in the RW table {memory, src_addr + i} ## Exceptions From e3b145fa923f0db4946f1d6fb3c43a4181a40405 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Fri, 4 Mar 2022 10:34:32 +0800 Subject: [PATCH 09/12] fix: use src_addr and test case with call data offset --- src/zkevm_specs/evm/execution/calldataload.py | 4 +- tests/evm/test_calldataload.py | 53 ++++++++++++++++--- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index ce527c744..745741925 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -32,11 +32,11 @@ def calldataload(instruction: Instruction): for idx in range(N_BYTES_WORD): if buffer_reader.read_flag(idx): if instruction.curr.is_root: - tx_byte = instruction.tx_calldata_lookup(tx_id, offset + idx) + tx_byte = instruction.tx_calldata_lookup(tx_id, src_addr + idx) buffer_reader.constrain_byte(idx, tx_byte) calldata_word.append(int(tx_byte)) else: - mem_byte = instruction.memory_lookup(RW.Read, offset + idx) + mem_byte = instruction.memory_lookup(RW.Read, src_addr + idx) buffer_reader.constrain_byte(idx, mem_byte) calldata_word.append(int(mem_byte)) else: diff --git a/tests/evm/test_calldataload.py b/tests/evm/test_calldataload.py index 4dd87a7df..f34e600aa 100644 --- a/tests/evm/test_calldataload.py +++ b/tests/evm/test_calldataload.py @@ -1,5 +1,6 @@ import pytest +from typing import Optional from zkevm_specs.evm import ( Bytecode, CallContextFieldTag, @@ -16,33 +17,59 @@ TESTING_DATA = ( ( bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0x20, 0x00, bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), True, + None, ), ( bytes.fromhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0x20, 0x1F, bytes.fromhex("FF00000000000000000000000000000000000000000000000000000000000000"), True, + None, ), ( bytes.fromhex("a1bacf5488bfafc33bad736db41f06866eaeb35e1c1dd81dfc268357ec98563f"), + 0x20, 0x10, bytes.fromhex("6eaeb35e1c1dd81dfc268357ec98563f00000000000000000000000000000000"), True, + None, ), ( bytes.fromhex("a1bacf5488bfafc33bad736db41f06866eaeb35e1c1dd81dfc268357ec98563f"), + 0x20, 0x10, bytes.fromhex("6eaeb35e1c1dd81dfc268357ec98563f00000000000000000000000000000000"), False, + 0x00, + ), + ( + bytes.fromhex("a1bacf5488bfafc33bad736db41f06866eaeb35e1c1dd81dfc268357ec98563fab"), + 0x20, + 0x10, + bytes.fromhex("aeb35e1c1dd81dfc268357ec98563fab00000000000000000000000000000000"), + False, + 0x01, ), ) -@pytest.mark.parametrize("call_data, offset, expected_stack_top, is_root", TESTING_DATA) -def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, is_root: bool): +@pytest.mark.parametrize( + "call_data, call_data_length, offset, expected_stack_top, is_root, call_data_offset", + TESTING_DATA, +) +def test_calldataload( + call_data: bytes, + call_data_length: U64, + offset: U64, + expected_stack_top: bytes, + is_root: bool, + call_data_offset: Optional[U64], +): randomness = rand_fp() tx = Transaction(id=1, call_data=call_data) @@ -64,6 +91,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, rws.add((4, RW.Write, RWTableTag.Stack, 1, 1023, 0, expected_stack_top, 0, 0, 0)) rw_counter_stop = 5 else: + # add to RW table call context, call data length (read) rws.add( ( 4, @@ -72,12 +100,13 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, 1, CallContextFieldTag.CallDataLength, 0, - len(call_data), + call_data_length, 0, 0, 0, ) ) + # add to RW table call context, call data offset (read) rws.add( ( 5, @@ -86,17 +115,25 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, 1, CallContextFieldTag.CallDataOffset, 0, - 0, + call_data_offset, 0, 0, 0, ) ) - for i in range(offset, len(call_data)): - rws.add((6 + i - offset, RW.Read, RWTableTag.Memory, 1, i, 0, call_data[i], 0, 0, 0)) + rw_counter = 6 + # add to RW table memory (read) + for i in range(0, len(call_data)): + idx = offset + call_data_offset + i + if idx < len(call_data): + rws.add( + (rw_counter, RW.Read, RWTableTag.Memory, 1, idx, 0, call_data[idx], 0, 0, 0) + ) + rw_counter += 1 + # add to RW table stack (write) rws.add( ( - 6 + len(call_data) - offset, + rw_counter, RW.Write, RWTableTag.Stack, 1, @@ -108,7 +145,7 @@ def test_calldataload(call_data: bytes, offset: U64, expected_stack_top: bytes, 0, ) ) - rw_counter_stop = 7 + len(call_data) - offset + rw_counter_stop = rw_counter + 1 tables = Tables( block_table=set(), From 566d5cf3ece3df4fa689e6d9a6e78aee23498b28 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Tue, 8 Mar 2022 10:34:49 +0800 Subject: [PATCH 10/12] fix: spec in case of stack pointer | minor refactor --- specs/opcode/35CALLDATALOAD.md | 2 +- src/zkevm_specs/evm/execution/calldataload.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/opcode/35CALLDATALOAD.md b/specs/opcode/35CALLDATALOAD.md index bdd11fa46..993ef7d6f 100644 --- a/specs/opcode/35CALLDATALOAD.md +++ b/specs/opcode/35CALLDATALOAD.md @@ -32,7 +32,7 @@ Stack input is the byte offset to read call data from. Stack output is a 32-byte ## Exceptions -1. Stack overflow: stack is full, stack pointer = 0 +1. Stack underflow: stack is empty, stack pointer = 1024 2. Out of gas: remaining gas is not enough for this opcode ## Code diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index 745741925..d6ea0253f 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -52,5 +52,5 @@ def calldataload(instruction: Instruction): opcode, rw_counter=Transition.delta(instruction.rw_counter_offset), program_counter=Transition.delta(1), - stack_pointer=Transition.delta(0), + stack_pointer=Transition.same(), ) From bc7ac5e735392d5304061acc6016f278e5b033aa Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Tue, 8 Mar 2022 21:28:00 +0800 Subject: [PATCH 11/12] fix: use caller id's memory in case of internal call --- specs/opcode/35CALLDATALOAD.md | 5 +- src/zkevm_specs/evm/execution/calldataload.py | 3 +- tests/evm/test_calldataload.py | 56 +++++++++++++++---- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/specs/opcode/35CALLDATALOAD.md b/specs/opcode/35CALLDATALOAD.md index 993ef7d6f..a27bc9d36 100644 --- a/specs/opcode/35CALLDATALOAD.md +++ b/specs/opcode/35CALLDATALOAD.md @@ -15,7 +15,7 @@ Stack input is the byte offset to read call data from. Stack output is a 32-byte - if is_root_call: - rw_counter += 3 (1 stack read, 1 call context read, 1 stack write) - if is_internal_call: - - rw_counter += rw_counter_offset ∈ {4, 5, ..., 35, 36} (1 stack read, 2 call context reads, i ∈ {0, 1, ..., 31, 32} memory reads, 1 stack write) + - rw_counter += rw_counter_offset ∈ {5, 6, ..., 36, 37} (1 stack read, 3 call context reads, i ∈ {0, 1, ..., 31, 32} memory reads, 1 stack write) - stack_pointer unchanged - pc + 1 - gas - 3 @@ -28,7 +28,8 @@ Stack input is the byte offset to read call data from. Stack output is a 32-byte - if is_internal_call (where `src_addr = offset + calldata_offset`): - `calldata_length` is in the RW table (call context) - `calldata_offset` is in the RW table (call context) - - i ∈ {0, 1, ..., 31, 32} lookups for `i in range(32)`: if `buffer.read_flag(i)` then the i'th byte of the element on top of the stack `calldata_word[i]` is in the RW table {memory, src_addr + i} + - `caller_id` is in the RW table (call context) + - i ∈ {0, 1, ..., 31, 32} lookups for `i in range(32)`: if `buffer.read_flag(i)` then the i'th byte of the element on top of the stack `calldata_word[i]` is in the RW table {memory, src_addr + i, caller_id} ## Exceptions diff --git a/src/zkevm_specs/evm/execution/calldataload.py b/src/zkevm_specs/evm/execution/calldataload.py index d6ea0253f..c2361cbc3 100644 --- a/src/zkevm_specs/evm/execution/calldataload.py +++ b/src/zkevm_specs/evm/execution/calldataload.py @@ -20,6 +20,7 @@ def calldataload(instruction: Instruction): else: calldata_length = instruction.call_context_lookup(CallContextFieldTag.CallDataLength) calldata_offset = instruction.call_context_lookup(CallContextFieldTag.CallDataOffset) + caller_id = instruction.call_context_lookup(CallContextFieldTag.CallerId) src_addr = offset + calldata_offset src_addr_end = calldata_length + calldata_offset @@ -36,7 +37,7 @@ def calldataload(instruction: Instruction): buffer_reader.constrain_byte(idx, tx_byte) calldata_word.append(int(tx_byte)) else: - mem_byte = instruction.memory_lookup(RW.Read, src_addr + idx) + mem_byte = instruction.memory_lookup(RW.Read, src_addr + idx, caller_id) buffer_reader.constrain_byte(idx, mem_byte) calldata_word.append(int(mem_byte)) else: diff --git a/tests/evm/test_calldataload.py b/tests/evm/test_calldataload.py index f34e600aa..2822fc2c7 100644 --- a/tests/evm/test_calldataload.py +++ b/tests/evm/test_calldataload.py @@ -80,15 +80,21 @@ def test_calldataload( bytecode = Bytecode().push(offset_rlc, n_bytes=32).calldataload().stop() bytecode_hash = RLC(bytecode.hash(), randomness) + if is_root: + call_id = 1 + else: + call_id = 2 + parent_call_id = 1 + rws = set( [ - (1, RW.Write, RWTableTag.Stack, 1, 1023, 0, offset_rlc, 0, 0, 0), - (2, RW.Read, RWTableTag.Stack, 1, 1023, 0, offset_rlc, 0, 0, 0), - (3, RW.Read, RWTableTag.CallContext, 1, CallContextFieldTag.TxId, 0, 1, 0, 0, 0), + (1, RW.Write, RWTableTag.Stack, call_id, 1023, 0, offset_rlc, 0, 0, 0), + (2, RW.Read, RWTableTag.Stack, call_id, 1023, 0, offset_rlc, 0, 0, 0), + (3, RW.Read, RWTableTag.CallContext, call_id, CallContextFieldTag.TxId, 0, 1, 0, 0, 0), ] ) if is_root: - rws.add((4, RW.Write, RWTableTag.Stack, 1, 1023, 0, expected_stack_top, 0, 0, 0)) + rws.add((4, RW.Write, RWTableTag.Stack, call_id, 1023, 0, expected_stack_top, 0, 0, 0)) rw_counter_stop = 5 else: # add to RW table call context, call data length (read) @@ -97,7 +103,7 @@ def test_calldataload( 4, RW.Read, RWTableTag.CallContext, - 1, + call_id, CallContextFieldTag.CallDataLength, 0, call_data_length, @@ -112,7 +118,7 @@ def test_calldataload( 5, RW.Read, RWTableTag.CallContext, - 1, + call_id, CallContextFieldTag.CallDataOffset, 0, call_data_offset, @@ -121,13 +127,39 @@ def test_calldataload( 0, ) ) - rw_counter = 6 + # add to RW table call context, caller'd ID (read) + rws.add( + ( + 6, + RW.Read, + RWTableTag.CallContext, + call_id, + CallContextFieldTag.CallerId, + 0, + parent_call_id, + 0, + 0, + 0, + ) + ) + rw_counter = 7 # add to RW table memory (read) for i in range(0, len(call_data)): idx = offset + call_data_offset + i if idx < len(call_data): rws.add( - (rw_counter, RW.Read, RWTableTag.Memory, 1, idx, 0, call_data[idx], 0, 0, 0) + ( + rw_counter, + RW.Read, + RWTableTag.Memory, + parent_call_id, + idx, + 0, + call_data[idx], + 0, + 0, + 0, + ) ) rw_counter += 1 # add to RW table stack (write) @@ -136,7 +168,7 @@ def test_calldataload( rw_counter, RW.Write, RWTableTag.Stack, - 1, + call_id, 1023, 0, expected_stack_top, @@ -161,7 +193,7 @@ def test_calldataload( StepState( execution_state=ExecutionState.PUSH, rw_counter=1, - call_id=1, + call_id=call_id, is_root=is_root, is_create=False, code_source=bytecode_hash, @@ -172,7 +204,7 @@ def test_calldataload( StepState( execution_state=ExecutionState.CALLDATALOAD, rw_counter=2, - call_id=1, + call_id=call_id, is_root=is_root, is_create=False, code_source=bytecode_hash, @@ -183,7 +215,7 @@ def test_calldataload( StepState( execution_state=ExecutionState.STOP, rw_counter=rw_counter_stop, - call_id=1, + call_id=call_id, is_root=is_root, is_create=False, code_source=bytecode_hash, From 77841e8d63f3bfad0c9c61ad5a0f5ef0d255d6a6 Mon Sep 17 00:00:00 2001 From: Rohit Narurkar Date: Thu, 10 Mar 2022 08:45:17 +0800 Subject: [PATCH 12/12] fix: remove calldata when not a root call --- tests/evm/test_calldataload.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/evm/test_calldataload.py b/tests/evm/test_calldataload.py index 2822fc2c7..a207217f4 100644 --- a/tests/evm/test_calldataload.py +++ b/tests/evm/test_calldataload.py @@ -72,7 +72,9 @@ def test_calldataload( ): randomness = rand_fp() - tx = Transaction(id=1, call_data=call_data) + tx = Transaction(id=1) + if is_root: + tx.call_data = call_data offset_rlc = RLC(offset, randomness) expected_stack_top = RLC(expected_stack_top, randomness)