From cae81963105438027355e6ba4798a361f1cd94a1 Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 26 Aug 2024 15:54:13 -0600 Subject: [PATCH 1/4] add 2935 as copy of 4788 --- build-wrapper | 5 ++ src/execution_hash/ctor.eas | 12 +++ src/execution_hash/main.eas | 139 +++++++++++++++++++++++++++++ test/ExecutionHash.t.sol.in | 163 +++++++++++++++++++++++++++++++++++ test/ExecutionHash.to.sol.in | 162 ++++++++++++++++++++++++++++++++++ 5 files changed, 481 insertions(+) create mode 100644 src/execution_hash/ctor.eas create mode 100644 src/execution_hash/main.eas create mode 100644 test/ExecutionHash.t.sol.in create mode 100644 test/ExecutionHash.to.sol.in diff --git a/build-wrapper b/build-wrapper index 52dcea7..3621d90 100755 --- a/build-wrapper +++ b/build-wrapper @@ -4,6 +4,7 @@ set -euf -o pipefail SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; BEACONROOT_BYTECODE="$(geas "src/beacon_root/main.eas")" +EXECHASH_BYTECODE="$(geas "src/execution_hash/main.eas")" WITHDRAWALS_BYTECODE="$(geas "src/withdrawals/main.eas")" CONSOLODATIONS_BYTECODE="$(geas "src/consolidations/main.eas")" FAKE_EXPO_BYTECODE="$(geas "src/common/fake_expo_test.eas")" @@ -12,6 +13,10 @@ sed \ -e "s/@bytecode@/$BEACONROOT_BYTECODE/" \ "$SCRIPT_DIR/test/BeaconRoot.t.sol.in" > "$SCRIPT_DIR/test/BeaconRoot.t.sol" +sed \ + -e "s/@bytecode@/$BEACONROOT_BYTECODE/" \ + "$SCRIPT_DIR/test/ExecutionHash.t.sol.in" > "$SCRIPT_DIR/test/ExecutionHash.t.sol" + sed \ -e "s/@bytecode@/$WITHDRAWALS_BYTECODE/" \ -e "s/@bytecode_expo@/$FAKE_EXPO_BYTECODE/" \ diff --git a/src/execution_hash/ctor.eas b/src/execution_hash/ctor.eas new file mode 100644 index 0000000..5c5a4a1 --- /dev/null +++ b/src/execution_hash/ctor.eas @@ -0,0 +1,12 @@ +;; Copy and return code. +push @.end - @.start +dup1 +push @.start +push0 +codecopy +push0 +return + +.start: +#assemble "main.eas" +.end: diff --git a/src/execution_hash/main.eas b/src/execution_hash/main.eas new file mode 100644 index 0000000..48117dd --- /dev/null +++ b/src/execution_hash/main.eas @@ -0,0 +1,139 @@ +;; ┏┓┏┓┏┓┏━ +;; ┏┛┗┫ ┫┗┓┏┓┏┏┳┓ +;; ┗━┗┛┗┛┗┛┗┻┛┛┗┗ +;; +;; This is an implementation of EIP-2935's predeploy contract. It is a slightly +;; modified version of the EIP-4788 predeploy. + +;; The contract implements two ring buffers to create bounded execution block +;; hash lookup. The first ring buffer is a blocknum % buflen -> timestamp +;; mapping. This is used to ensure blocknum argument actually matches the +;; stored hash and isn't a different dividend. The second ring buffer store the +;; block hash. It's also keyed by blocknum % buflen and the shifted right by +;; buflen so the two don't overlap. +;; +;; The ring buffers can be visualized as follows: +;; +;; buflen = 10 +;; |--------------|--------------| +;; 0 10 20 +;; block nums block hash +;; +;; To get the corresponding block hash for a specific number, simply add +;; buflen to the number's index in the first ring buffer. The sum will be +;; the storage slot in the second ring buffer where it is stored. + + +;; ---------------------------------------------------------------------------- +;; MACROS --------------------------------------------------------------------- +;; ---------------------------------------------------------------------------- + +;; BUFLEN returns the HISTORY_BUFFER_LENGTH as defined in the EIP. +#define BUFLEN 8191 + +;; SYSADDR is the address which calls the contract to submit a new root. +#define SYSADDR 0xfffffffffffffffffffffffffffffffffffffffe + +;; do_revert sets up and then executes a revert(0,0) operation. +#define %do_revert() { + push0 ;; [0] + push0 ;; [0, 0] + revert ;; [] +} + +;; ---------------------------------------------------------------------------- +;; MACROS END ----------------------------------------------------------------- +;; ---------------------------------------------------------------------------- + +.start: + ;; Protect the submit routine by verifying the caller is equal to + ;; sysaddr(). + caller ;; [caller] + push20 SYSADDR ;; [sysaddr, caller] + eq ;; [sysaddr == caller] + push1 @submit ;; [submit_lbl, sysaddr == caller] + jumpi ;; [] + + ;; Fallthrough if addresses don't match -- this means the caller intends + ;; to read a root. + + ;; Check if calldata is equal to 32 bytes. + push1 32 ;; [32] + calldatasize ;; [calldatasize, 32] + eq ;; [calldatasize == 32] + + ;; Jump to continue if length-check passed, otherwise revert. + push1 @loadtime ;; [loadtime_lbl, calldatasize == 32] + jumpi ;; [] + %do_revert() ;; [] + +loadtime: + ;; Load input timestamp. + push0 ;; [0] + calldataload ;; [input_timestamp] + dup1 ;; [input_timestamp, input_timestamp] + + ;; Verify input timestamp is non-zero. + iszero ;; [input_timestamp == 0, input_timestamp] + push1 @throw ;; [throw_lbl, input_timestamp == 0, input_timestamp] + jumpi ;; [input_timestamp] + + ;; Compute the timestamp index and load from storage. + push3 BUFLEN ;; [buflen, input_timestamp] + dup2 ;; [input_timestamp, buflen, input_timestamp] + mod ;; [time_index, input_timestamp] + swap1 ;; [input_timestamp, time_index] + dup2 ;; [time_index, input_timestamp, time_index] + sload ;; [stored_timestamp, input_timestamp, time_index] + + ;; Verify stored timestamp matches input timestamp. It's possible these + ;; don't match if the slot has been overwritten by the ring buffer or if + ;; the timestamp input wasn't a valid previous timestamp. + eq ;; [stored_timestamp == input_timestamp, time_index] + push1 @loadroot ;; [loadroot_lbl, input == timestamp, time_index] + jumpi ;; [time_index] + %do_revert() ;; [] + +loadroot: + ;; Extend index to get root index. + push3 BUFLEN ;; [buflen, time_index] + add ;; [root_index] + sload ;; [root] + + ;; Write the retrieved root to memory so it can be returned. + push0 ;; [0, root] + mstore ;; [] + + ;; Return the root. + push1 32 ;; [size] + push0 ;; [offset, size] + return ;; [] + +throw: + ;; Reverts current execution with no return data. + %do_revert() + +submit: + ;; Calculate the index the timestamp should be stored at, e.g. + ;; time_index = (time % buflen). + push3 BUFLEN ;; [buflen] + timestamp ;; [time, buflen] + mod ;; [time % buflen] + + ;; Write timestamp into storage slot at time_index. + timestamp ;; [time, time_index] + dup2 ;; [time_index, time, time_index] + sstore ;; [time_index] + + ;; Get root from calldata and write into root_index. No validation is + ;; done on the input root. Becuase the routine is protected by a caller + ;; check against sysaddr(), it's okay to assume the value is correctly + ;; given. + push0 ;; [0, time_index] + calldataload ;; [root, time_index] + swap1 ;; [time_index, root] + push3 BUFLEN ;; [buflen, time_index, root] + add ;; [root_index, root] + sstore ;; [] + + stop ;; [] diff --git a/test/ExecutionHash.t.sol.in b/test/ExecutionHash.t.sol.in new file mode 100644 index 0000000..2714ea5 --- /dev/null +++ b/test/ExecutionHash.t.sol.in @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../src/Contract.sol"; + +address constant addr = 0x000000000000000000000000000000000000000b; +address constant sysaddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; +uint256 constant buflen = 8191; +bytes32 constant root = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6"; + +function timestamp() view returns (bytes32) { + return bytes32(uint256(block.timestamp)); +} + +function timestamp_idx() view returns (bytes32) { + return bytes32(uint256(block.timestamp % buflen)); +} + +function root_idx() view returns (bytes32) { + return bytes32(uint256(block.timestamp % buflen + buflen)); +} + +contract ContractTest is Test { + address unit; + + function setUp() public { + vm.etch(addr, hex"@bytecode@"); + unit = addr; + } + + // testRead verifies the contract returns the expected beacon root. + function testRead() public { + // Store timestamp and root at expected indexes. + vm.store(unit, timestamp_idx(), timestamp()); + vm.store(unit, root_idx(), root); + + // Read root associated with current timestamp. + (bool ret, bytes memory data) = unit.call(bytes.concat(timestamp())); + assertTrue(ret); + assertEq(data, bytes.concat(root)); + } + + function testReadBadCalldataSize() public { + uint256 time = block.timestamp; + + // Store timestamp and root at expected indexes. + vm.store(unit, timestamp_idx(), bytes32(time)); + vm.store(unit, root_idx(), root); + + // Call with 0 byte arguement. + (bool ret, bytes memory data) = unit.call(hex""); + assertFalse(ret); + assertEq(data, hex""); + + // Call with 31 byte arguement. + (ret, data) = unit.call(hex"00000000000000000000000000000000000000000000000000000000001337"); + assertFalse(ret); + assertEq(data, hex""); + + // Call with 33 byte arguement. + (ret, data) = unit.call(hex"000000000000000000000000000000000000000000000000000000000000001337"); + assertFalse(ret); + assertEq(data, hex""); + } + + function testReadWrongTimestamp() public { + // Set reasonable timestamp. + vm.warp(1641070800); + uint256 time = block.timestamp; + + // Store timestamp and root at expected indexes. + vm.store(unit, timestamp_idx(), bytes32(time)); + vm.store(unit, root_idx(), root); + + // Wrap around buflen once forward. + (bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(time+buflen))); + assertFalse(ret); + assertEq(data, hex""); + + // Wrap around buflen once backward. + (ret, data) = unit.call(bytes.concat(bytes32(time-buflen))); + assertFalse(ret); + assertEq(data, hex""); + + // Timestamp without any associated root. + (ret, data) = unit.call(bytes.concat(bytes32(time+1))); + assertFalse(ret); + assertEq(data, hex""); + + // Timestamp zero should fail. + (ret, data) = unit.call(bytes.concat(bytes32(0))); + assertFalse(ret); + assertEq(data, hex""); + } + + // testUpdate verifies the set functionality of the contract. + function testUpdate() public { + // Simulate pre-block call to set root. + vm.prank(sysaddr); + (bool ret, bytes memory data) = unit.call(bytes.concat(root)); + assertTrue(ret); + assertEq(data, hex""); + + // Verify timestamp. + bytes32 got = vm.load(unit, timestamp_idx()); + assertEq(got, timestamp()); + + // Verify root. + got = vm.load(unit, root_idx()); + assertEq(got, root); + } + + // testRingBuffers verifies the integrity of the ring buffer is maintained + // as the write indexes loop back to the start and begin overwriting + // values. + function testRingBuffers() public { + for (uint256 i = 0; i < 10000; i += 1) { + bytes32 pbbr = bytes32(i*1337); + + // Simulate pre-block call to set root. + vm.prank(sysaddr); + (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); + assertTrue(ret); + assertEq(data, hex""); + + // Call contract as normal account to get beacon root associated + // with current timestamp. + (ret, data) = unit.call(bytes.concat(timestamp())); + assertTrue(ret); + assertEq(data, bytes.concat(pbbr)); + + // Skip forward 12 seconds. + skip(12); + } + } + + + // testHistoricalReads verifies that it is possible to read all previously + // saved values in the beacon root contract. + function testHistoricalReads() public { + uint256 start = block.timestamp; + + // Saturate storage with fake roots. + for (uint256 i = 0; i < buflen; i += 1) { + bytes32 pbbr = bytes32(i*1337); + vm.prank(sysaddr); + (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); + assertTrue(ret); + assertEq(data, hex""); + skip(12); + } + + // Attempt to read all values in same block context. + for (uint256 i = 0; i < buflen; i += 1) { + bytes32 time = bytes32(uint256(start+i*12)); + (bool ret, bytes memory got) = unit.call(bytes.concat(time)); + assertTrue(ret); + assertEq(got, bytes.concat(bytes32(i*1337))); + } + } +} + diff --git a/test/ExecutionHash.to.sol.in b/test/ExecutionHash.to.sol.in new file mode 100644 index 0000000..330fbaf --- /dev/null +++ b/test/ExecutionHash.to.sol.in @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../src/Contract.sol"; + +address constant addr = 0x000000000000000000000000000000000000000b; +address constant sysaddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; +uint256 constant buflen = 8191; +bytes32 constant root = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6"; + +function timestamp() view returns (bytes32) { + return bytes32(uint256(block.timestamp)); +} + +function timestamp_idx() view returns (bytes32) { + return bytes32(uint256(block.timestamp % buflen)); +} + +function root_idx() view returns (bytes32) { + return bytes32(uint256(block.timestamp % buflen + buflen)); +} + +contract ContractTest is Test { + address unit; + + function setUp() public { + vm.etch(addr, hex"@bytecode@"); + unit = addr; + } + + // testRead verifies the contract returns the expected beacon root. + function testRead() public { + // Store timestamp and root at expected indexes. + vm.store(unit, timestamp_idx(), timestamp()); + vm.store(unit, root_idx(), root); + + // Read root associated with current timestamp. + (bool ret, bytes memory data) = unit.call(bytes.concat(timestamp())); + assertTrue(ret); + assertEq(data, bytes.concat(root)); + } + + function testReadBadCalldataSize() public { + uint256 time = block.timestamp; + + // Store timestamp and root at expected indexes. + vm.store(unit, timestamp_idx(), bytes32(time)); + vm.store(unit, root_idx(), root); + + // Call with 0 byte arguement. + (bool ret, bytes memory data) = unit.call(hex""); + assertFalse(ret); + assertEq(data, hex""); + + // Call with 31 byte arguement. + (ret, data) = unit.call(hex"00000000000000000000000000000000000000000000000000000000001337"); + assertFalse(ret); + assertEq(data, hex""); + + // Call with 33 byte arguement. + (ret, data) = unit.call(hex"000000000000000000000000000000000000000000000000000000000000001337"); + assertFalse(ret); + assertEq(data, hex""); + } + + function testReadWrongTimestamp() public { + // Set reasonable timestamp. + vm.warp(1641070800); + uint256 time = block.timestamp; + + // Store timestamp and root at expected indexes. + vm.store(unit, timestamp_idx(), bytes32(time)); + vm.store(unit, root_idx(), root); + + // Wrap around buflen once forward. + (bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(time+buflen))); + assertFalse(ret); + assertEq(data, hex""); + + // Wrap around buflen once backward. + (ret, data) = unit.call(bytes.concat(bytes32(time-buflen))); + assertFalse(ret); + assertEq(data, hex""); + + // Timestamp without any associated root. + (ret, data) = unit.call(bytes.concat(bytes32(time+1))); + assertFalse(ret); + assertEq(data, hex""); + + // Timestamp zero should fail. + (ret, data) = unit.call(bytes.concat(bytes32(0))); + assertFalse(ret); + assertEq(data, hex""); + } + + // testUpdate verifies the set functionality of the contract. + function testUpdate() public { + // Simulate pre-block call to set root. + vm.prank(sysaddr); + (bool ret, bytes memory data) = unit.call(bytes.concat(root)); + assertTrue(ret); + assertEq(data, hex""); + + // Verify timestamp. + bytes32 got = vm.load(unit, timestamp_idx()); + assertEq(got, timestamp()); + + // Verify root. + got = vm.load(unit, root_idx()); + assertEq(got, root); + } + + // testRingBuffers verifies the integrity of the ring buffer is maintained + // as the write indexes loop back to the start and begin overwriting + // values. + function testRingBuffers() public { + for (uint256 i = 0; i < 10000; i += 1) { + bytes32 pbbr = bytes32(i*1337); + + // Simulate pre-block call to set root. + vm.prank(sysaddr); + (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); + assertTrue(ret); + assertEq(data, hex""); + + // Call contract as normal account to get beacon root associated + // with current timestamp. + (ret, data) = unit.call(bytes.concat(timestamp())); + assertTrue(ret); + assertEq(data, bytes.concat(pbbr)); + + // Skip forward 12 seconds. + skip(12); + } + } + + + // testHistoricalReads verifies that it is possible to read all previously + // saved values in the beacon root contract. + function testHistoricalReads() public { + uint256 start = block.timestamp; + + // Saturate storage with fake roots. + for (uint256 i = 0; i < buflen; i += 1) { + bytes32 pbbr = bytes32(i*1337); + vm.prank(sysaddr); + (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); + assertTrue(ret); + assertEq(data, hex""); + skip(12); + } + + // Attempt to read all values in same block context. + for (uint256 i = 0; i < buflen; i += 1) { + bytes32 time = bytes32(uint256(start+i*12)); + (bool ret, bytes memory got) = unit.call(bytes.concat(time)); + assertTrue(ret); + assertEq(got, bytes.concat(bytes32(i*1337))); + } + } +} From 8a0274aeb41e3dafa4e2fb421fb4b38d6999370a Mon Sep 17 00:00:00 2001 From: lightclient Date: Fri, 25 Oct 2024 14:23:12 -0600 Subject: [PATCH 2/4] 2935: match current eip impl --- src/execution_hash/main.eas | 165 ++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 90 deletions(-) diff --git a/src/execution_hash/main.eas b/src/execution_hash/main.eas index 48117dd..49c0999 100644 --- a/src/execution_hash/main.eas +++ b/src/execution_hash/main.eas @@ -36,9 +36,9 @@ ;; do_revert sets up and then executes a revert(0,0) operation. #define %do_revert() { - push0 ;; [0] - push0 ;; [0, 0] - revert ;; [] + push0 ;; [0] + push0 ;; [0, 0] + revert ;; [] } ;; ---------------------------------------------------------------------------- @@ -46,94 +46,79 @@ ;; ---------------------------------------------------------------------------- .start: - ;; Protect the submit routine by verifying the caller is equal to - ;; sysaddr(). - caller ;; [caller] - push20 SYSADDR ;; [sysaddr, caller] - eq ;; [sysaddr == caller] - push1 @submit ;; [submit_lbl, sysaddr == caller] - jumpi ;; [] - - ;; Fallthrough if addresses don't match -- this means the caller intends - ;; to read a root. - - ;; Check if calldata is equal to 32 bytes. - push1 32 ;; [32] - calldatasize ;; [calldatasize, 32] - eq ;; [calldatasize == 32] - - ;; Jump to continue if length-check passed, otherwise revert. - push1 @loadtime ;; [loadtime_lbl, calldatasize == 32] - jumpi ;; [] - %do_revert() ;; [] - -loadtime: - ;; Load input timestamp. - push0 ;; [0] - calldataload ;; [input_timestamp] - dup1 ;; [input_timestamp, input_timestamp] - - ;; Verify input timestamp is non-zero. - iszero ;; [input_timestamp == 0, input_timestamp] - push1 @throw ;; [throw_lbl, input_timestamp == 0, input_timestamp] - jumpi ;; [input_timestamp] - - ;; Compute the timestamp index and load from storage. - push3 BUFLEN ;; [buflen, input_timestamp] - dup2 ;; [input_timestamp, buflen, input_timestamp] - mod ;; [time_index, input_timestamp] - swap1 ;; [input_timestamp, time_index] - dup2 ;; [time_index, input_timestamp, time_index] - sload ;; [stored_timestamp, input_timestamp, time_index] - - ;; Verify stored timestamp matches input timestamp. It's possible these - ;; don't match if the slot has been overwritten by the ring buffer or if - ;; the timestamp input wasn't a valid previous timestamp. - eq ;; [stored_timestamp == input_timestamp, time_index] - push1 @loadroot ;; [loadroot_lbl, input == timestamp, time_index] - jumpi ;; [time_index] - %do_revert() ;; [] - -loadroot: - ;; Extend index to get root index. - push3 BUFLEN ;; [buflen, time_index] - add ;; [root_index] - sload ;; [root] - - ;; Write the retrieved root to memory so it can be returned. - push0 ;; [0, root] - mstore ;; [] - - ;; Return the root. - push1 32 ;; [size] - push0 ;; [offset, size] - return ;; [] + ;; Protect the submit routine by verifying the caller is equal to + ;; sysaddr(). + caller ;; [caller] + push20 SYSADDR ;; [sysaddr, caller] + eq ;; [sysaddr == caller] + jumpi @submit ;; [] + + ;; Fallthrough if addresses don't match -- this means the caller intends + ;; to read a root. + + + ;; Check if input > 8 byte value and revert if this isn't the case the + ;; check is performed by comparing the biggest 8 byte number with the + ;; call data, which is a right-padded 32 byte number. + push 0xffffffffffffffff ;; [2^32-1] + push 0 ;; [0, 2^32-1] + calldataload ;; [input, 2^32-1] + gt ;; [input > 2^32-1] + jumpi @throw ;; [] + + ;; Check if input > number-1 then return 0. + push 1 ;; [1] + number ;; [number, 1] + sub ;; [number-1] + push0 ;; [0, number-1] + calldataload ;; [input, number-1] + gt ;; [input > number-1] + jumpi @zeroreturn + + ;; Check if blocknumber > input + 8192 then return 0, no overflow + ;; expected for input of < max 8 byte value + push 0 ;; [0] + calldataload ;; [input] + push 8192 ;; [8192, input] + add ;; [input+8192] + number ;; [number, input+8192] + gt ;; [number > input+8192] + jumpi @zeroreturn ;; [] + + ;; mod 8191 and sload + push 8191 ;; [8191] + push 0 ;; [0, 8191] + calldataload ;; [input, 8191] + and ;; [input % 8191] + sload ;; [hash] + + ;; load into mem and return 32 bytes + push 0 ;; [0, hash] + mstore ;; [] + push 32 ;; [32] + push 0 ;; [0, 32] + return ;; [] + +zeroreturn: + push 0 ;; [0] + push 0 ;; [0, 0] + mstore ;; [] + push 32 ;; [32] + push 0 ;; [0, 32] + return ;; [] throw: - ;; Reverts current execution with no return data. - %do_revert() + ;; Reverts current execution with no return data. + %do_revert() submit: - ;; Calculate the index the timestamp should be stored at, e.g. - ;; time_index = (time % buflen). - push3 BUFLEN ;; [buflen] - timestamp ;; [time, buflen] - mod ;; [time % buflen] - - ;; Write timestamp into storage slot at time_index. - timestamp ;; [time, time_index] - dup2 ;; [time_index, time, time_index] - sstore ;; [time_index] - - ;; Get root from calldata and write into root_index. No validation is - ;; done on the input root. Becuase the routine is protected by a caller - ;; check against sysaddr(), it's okay to assume the value is correctly - ;; given. - push0 ;; [0, time_index] - calldataload ;; [root, time_index] - swap1 ;; [time_index, root] - push3 BUFLEN ;; [buflen, time_index, root] - add ;; [root_index, root] - sstore ;; [] - - stop ;; [] + push 0 ;; [0] + calldataload ;; [in] + push 8191 ;; [8191, in] + push 1 ;; [1, 8191, in] + number ;; [number, 1, 8191, in] + sub ;; [number-1, 8191, in] + and ;; [number-1 % 8191, in] + sstore + + stop From 418a7dd4a7bd61f6324748cd18c8e077693796a0 Mon Sep 17 00:00:00 2001 From: lightclient Date: Sat, 26 Oct 2024 23:14:56 -0600 Subject: [PATCH 3/4] 2935: improve impl and add tests --- build-wrapper | 2 +- lib/forge-std | 2 +- src/execution_hash/main.eas | 108 ++++++++++------------- test/ExecutionHash.t.sol.in | 102 ++++++++++------------ test/ExecutionHash.to.sol.in | 162 ----------------------------------- 5 files changed, 89 insertions(+), 287 deletions(-) delete mode 100644 test/ExecutionHash.to.sol.in diff --git a/build-wrapper b/build-wrapper index 3621d90..cde572a 100755 --- a/build-wrapper +++ b/build-wrapper @@ -14,7 +14,7 @@ sed \ "$SCRIPT_DIR/test/BeaconRoot.t.sol.in" > "$SCRIPT_DIR/test/BeaconRoot.t.sol" sed \ - -e "s/@bytecode@/$BEACONROOT_BYTECODE/" \ + -e "s/@bytecode@/$EXECHASH_BYTECODE/" \ "$SCRIPT_DIR/test/ExecutionHash.t.sol.in" > "$SCRIPT_DIR/test/ExecutionHash.t.sol" sed \ diff --git a/lib/forge-std b/lib/forge-std index bf66061..1eea5ba 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/src/execution_hash/main.eas b/src/execution_hash/main.eas index 49c0999..73ec511 100644 --- a/src/execution_hash/main.eas +++ b/src/execution_hash/main.eas @@ -2,27 +2,10 @@ ;; ┏┛┗┫ ┫┗┓┏┓┏┏┳┓ ;; ┗━┗┛┗┛┗┛┗┻┛┛┗┗ ;; -;; This is an implementation of EIP-2935's predeploy contract. It is a slightly -;; modified version of the EIP-4788 predeploy. - -;; The contract implements two ring buffers to create bounded execution block -;; hash lookup. The first ring buffer is a blocknum % buflen -> timestamp -;; mapping. This is used to ensure blocknum argument actually matches the -;; stored hash and isn't a different dividend. The second ring buffer store the -;; block hash. It's also keyed by blocknum % buflen and the shifted right by -;; buflen so the two don't overlap. +;; This is an implementation of EIP-2935's predeploy contract. ;; -;; The ring buffers can be visualized as follows: -;; -;; buflen = 10 -;; |--------------|--------------| -;; 0 10 20 -;; block nums block hash -;; -;; To get the corresponding block hash for a specific number, simply add -;; buflen to the number's index in the first ring buffer. The sum will be -;; the storage slot in the second ring buffer where it is stored. - +;; The contract implements a ring buffer to create bounded execution block +;; hash lookup. ;; ---------------------------------------------------------------------------- ;; MACROS --------------------------------------------------------------------- @@ -56,69 +39,64 @@ ;; Fallthrough if addresses don't match -- this means the caller intends ;; to read a root. + ;; Verify input is 32 bytes long. + push1 32 ;; [32] + calldatasize ;; [calldatasize, 32] + eq ;; [calldatasize == 32] - ;; Check if input > 8 byte value and revert if this isn't the case the - ;; check is performed by comparing the biggest 8 byte number with the - ;; call data, which is a right-padded 32 byte number. - push 0xffffffffffffffff ;; [2^32-1] - push 0 ;; [0, 2^32-1] - calldataload ;; [input, 2^32-1] - gt ;; [input > 2^32-1] - jumpi @throw ;; [] - - ;; Check if input > number-1 then return 0. - push 1 ;; [1] - number ;; [number, 1] - sub ;; [number-1] - push0 ;; [0, number-1] - calldataload ;; [input, number-1] - gt ;; [input > number-1] - jumpi @zeroreturn - - ;; Check if blocknumber > input + 8192 then return 0, no overflow - ;; expected for input of < max 8 byte value + ;; Jump to continue if length-check passed, otherwise revert. + jumpi @load ;; [] + %do_revert() ;; [] + +load: + ;; Check if input is requesting a block hash greater than current block + ;; number. push 0 ;; [0] calldataload ;; [input] - push 8192 ;; [8192, input] - add ;; [input+8192] - number ;; [number, input+8192] - gt ;; [number > input+8192] - jumpi @zeroreturn ;; [] - - ;; mod 8191 and sload - push 8191 ;; [8191] - push 0 ;; [0, 8191] - calldataload ;; [input, 8191] - and ;; [input % 8191] + push 1 ;; [1, input] + number ;; [number, 1, input] + sub ;; [number-1, input] + dup2 ;; [input, number-1, input] + gt ;; [input > number-1, input] + jumpi @throw ;; [input] + + ;; Check if the input is requesting a block hash before the earliest available + ;; hash currently. Since we've verfied that input <= number - 1, it's safe to + ;; check the following: + ;; number - 1 - input <= BUFLEN, which also equals: number - input < BUFLEN + dup1 ;; [input, input] + number ;; [number, input, input] + sub ;; [number - input, input] + push BUFLEN+1 ;; [buflen, number - input, input] + lt ;; [buflen < number - input, input] + jumpi @throw ;; [input] + + ;; Load the hash. + push BUFLEN ;; [buflen, input] + swap1 ;; [input, buflen] + mod ;; [input % buflen] sload ;; [hash] - ;; load into mem and return 32 bytes + ;; Load into memory and return. push 0 ;; [0, hash] mstore ;; [] push 32 ;; [32] push 0 ;; [0, 32] return ;; [] -zeroreturn: - push 0 ;; [0] - push 0 ;; [0, 0] - mstore ;; [] - push 32 ;; [32] - push 0 ;; [0, 32] - return ;; [] - throw: ;; Reverts current execution with no return data. + pop %do_revert() submit: push 0 ;; [0] calldataload ;; [in] - push 8191 ;; [8191, in] - push 1 ;; [1, 8191, in] - number ;; [number, 1, 8191, in] - sub ;; [number-1, 8191, in] - and ;; [number-1 % 8191, in] + push BUFLEN ;; [buflen, in] + push 1 ;; [1, buflen, in] + number ;; [number, 1, buflen, in] + sub ;; [number-1, buflen, in] + mod ;; [number-1 % buflen, in] sstore stop diff --git a/test/ExecutionHash.t.sol.in b/test/ExecutionHash.t.sol.in index 2714ea5..910c814 100644 --- a/test/ExecutionHash.t.sol.in +++ b/test/ExecutionHash.t.sol.in @@ -4,21 +4,17 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../src/Contract.sol"; -address constant addr = 0x000000000000000000000000000000000000000b; +address constant addr = 0x000000000000000000000000000000000000aaaa; address constant sysaddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; uint256 constant buflen = 8191; -bytes32 constant root = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6"; +bytes32 constant hash = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6"; -function timestamp() view returns (bytes32) { - return bytes32(uint256(block.timestamp)); +function lastBlockNumber() view returns (bytes32) { + return bytes32(uint256(block.number)-1); } -function timestamp_idx() view returns (bytes32) { - return bytes32(uint256(block.timestamp % buflen)); -} - -function root_idx() view returns (bytes32) { - return bytes32(uint256(block.timestamp % buflen + buflen)); +function hash_idx() view returns (bytes32) { + return bytes32(uint256(lastBlockNumber()) % buflen); } contract ContractTest is Test { @@ -29,24 +25,20 @@ contract ContractTest is Test { unit = addr; } - // testRead verifies the contract returns the expected beacon root. - function testRead() public { - // Store timestamp and root at expected indexes. - vm.store(unit, timestamp_idx(), timestamp()); - vm.store(unit, root_idx(), root); + // testRead verifies the contract returns the expected execution hash. + function testExecRead() public { + // Store hash at expected indexes. + vm.store(unit, hash_idx(), hash); - // Read root associated with current timestamp. - (bool ret, bytes memory data) = unit.call(bytes.concat(timestamp())); + // Read hash associated with current timestamp. + (bool ret, bytes memory data) = unit.call(bytes.concat(lastBlockNumber())); assertTrue(ret); - assertEq(data, bytes.concat(root)); + assertEq(data, bytes.concat(hash)); } function testReadBadCalldataSize() public { - uint256 time = block.timestamp; - - // Store timestamp and root at expected indexes. - vm.store(unit, timestamp_idx(), bytes32(time)); - vm.store(unit, root_idx(), root); + // Store hash at expected indexes. + vm.store(unit, hash_idx(), hash); // Call with 0 byte arguement. (bool ret, bytes memory data) = unit.call(hex""); @@ -64,31 +56,25 @@ contract ContractTest is Test { assertEq(data, hex""); } - function testReadWrongTimestamp() public { - // Set reasonable timestamp. - vm.warp(1641070800); - uint256 time = block.timestamp; + function testReadBadBlockNumbers() public { + // Set reasonable block number. + vm.roll(21053500); + uint256 number = block.number-1; - // Store timestamp and root at expected indexes. - vm.store(unit, timestamp_idx(), bytes32(time)); - vm.store(unit, root_idx(), root); + // Store hash at expected indexes. + vm.store(unit, hash_idx(), hash); // Wrap around buflen once forward. - (bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(time+buflen))); + (bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(number+buflen))); assertFalse(ret); assertEq(data, hex""); // Wrap around buflen once backward. - (ret, data) = unit.call(bytes.concat(bytes32(time-buflen))); - assertFalse(ret); - assertEq(data, hex""); - - // Timestamp without any associated root. - (ret, data) = unit.call(bytes.concat(bytes32(time+1))); + (ret, data) = unit.call(bytes.concat(bytes32(number-buflen-1))); assertFalse(ret); assertEq(data, hex""); - // Timestamp zero should fail. + // Block number zero should fail. (ret, data) = unit.call(bytes.concat(bytes32(0))); assertFalse(ret); assertEq(data, hex""); @@ -96,65 +82,65 @@ contract ContractTest is Test { // testUpdate verifies the set functionality of the contract. function testUpdate() public { - // Simulate pre-block call to set root. + // Simulate pre-block call to set hash. vm.prank(sysaddr); - (bool ret, bytes memory data) = unit.call(bytes.concat(root)); + (bool ret, bytes memory data) = unit.call(bytes.concat(hash)); assertTrue(ret); assertEq(data, hex""); - // Verify timestamp. - bytes32 got = vm.load(unit, timestamp_idx()); - assertEq(got, timestamp()); - - // Verify root. - got = vm.load(unit, root_idx()); - assertEq(got, root); + // Verify hash. + bytes32 got = vm.load(unit, hash_idx()); + assertEq(got, hash); } // testRingBuffers verifies the integrity of the ring buffer is maintained // as the write indexes loop back to the start and begin overwriting // values. function testRingBuffers() public { + // Set reasonable block number. + vm.roll(21053500); + for (uint256 i = 0; i < 10000; i += 1) { bytes32 pbbr = bytes32(i*1337); - // Simulate pre-block call to set root. + // Simulate pre-block call to set hash. vm.prank(sysaddr); (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); assertTrue(ret); assertEq(data, hex""); - // Call contract as normal account to get beacon root associated + // Call contract as normal account to get exeuction hash associated // with current timestamp. - (ret, data) = unit.call(bytes.concat(timestamp())); + (ret, data) = unit.call(bytes.concat(lastBlockNumber())); assertTrue(ret); assertEq(data, bytes.concat(pbbr)); - // Skip forward 12 seconds. - skip(12); + // Skip forward 1 block. + vm.roll(block.number+1); } } // testHistoricalReads verifies that it is possible to read all previously - // saved values in the beacon root contract. + // saved values in the beacon hash contract. function testHistoricalReads() public { - uint256 start = block.timestamp; + uint256 start = 1; + vm.roll(start); - // Saturate storage with fake roots. + // Saturate storage with fake hashs. for (uint256 i = 0; i < buflen; i += 1) { bytes32 pbbr = bytes32(i*1337); vm.prank(sysaddr); (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); assertTrue(ret); assertEq(data, hex""); - skip(12); + vm.roll(block.number+1); } // Attempt to read all values in same block context. for (uint256 i = 0; i < buflen; i += 1) { - bytes32 time = bytes32(uint256(start+i*12)); - (bool ret, bytes memory got) = unit.call(bytes.concat(time)); + bytes32 num = bytes32(uint256(start+i-1)); + (bool ret, bytes memory got) = unit.call(bytes.concat(num)); assertTrue(ret); assertEq(got, bytes.concat(bytes32(i*1337))); } diff --git a/test/ExecutionHash.to.sol.in b/test/ExecutionHash.to.sol.in deleted file mode 100644 index 330fbaf..0000000 --- a/test/ExecutionHash.to.sol.in +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -import "../src/Contract.sol"; - -address constant addr = 0x000000000000000000000000000000000000000b; -address constant sysaddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; -uint256 constant buflen = 8191; -bytes32 constant root = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6"; - -function timestamp() view returns (bytes32) { - return bytes32(uint256(block.timestamp)); -} - -function timestamp_idx() view returns (bytes32) { - return bytes32(uint256(block.timestamp % buflen)); -} - -function root_idx() view returns (bytes32) { - return bytes32(uint256(block.timestamp % buflen + buflen)); -} - -contract ContractTest is Test { - address unit; - - function setUp() public { - vm.etch(addr, hex"@bytecode@"); - unit = addr; - } - - // testRead verifies the contract returns the expected beacon root. - function testRead() public { - // Store timestamp and root at expected indexes. - vm.store(unit, timestamp_idx(), timestamp()); - vm.store(unit, root_idx(), root); - - // Read root associated with current timestamp. - (bool ret, bytes memory data) = unit.call(bytes.concat(timestamp())); - assertTrue(ret); - assertEq(data, bytes.concat(root)); - } - - function testReadBadCalldataSize() public { - uint256 time = block.timestamp; - - // Store timestamp and root at expected indexes. - vm.store(unit, timestamp_idx(), bytes32(time)); - vm.store(unit, root_idx(), root); - - // Call with 0 byte arguement. - (bool ret, bytes memory data) = unit.call(hex""); - assertFalse(ret); - assertEq(data, hex""); - - // Call with 31 byte arguement. - (ret, data) = unit.call(hex"00000000000000000000000000000000000000000000000000000000001337"); - assertFalse(ret); - assertEq(data, hex""); - - // Call with 33 byte arguement. - (ret, data) = unit.call(hex"000000000000000000000000000000000000000000000000000000000000001337"); - assertFalse(ret); - assertEq(data, hex""); - } - - function testReadWrongTimestamp() public { - // Set reasonable timestamp. - vm.warp(1641070800); - uint256 time = block.timestamp; - - // Store timestamp and root at expected indexes. - vm.store(unit, timestamp_idx(), bytes32(time)); - vm.store(unit, root_idx(), root); - - // Wrap around buflen once forward. - (bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(time+buflen))); - assertFalse(ret); - assertEq(data, hex""); - - // Wrap around buflen once backward. - (ret, data) = unit.call(bytes.concat(bytes32(time-buflen))); - assertFalse(ret); - assertEq(data, hex""); - - // Timestamp without any associated root. - (ret, data) = unit.call(bytes.concat(bytes32(time+1))); - assertFalse(ret); - assertEq(data, hex""); - - // Timestamp zero should fail. - (ret, data) = unit.call(bytes.concat(bytes32(0))); - assertFalse(ret); - assertEq(data, hex""); - } - - // testUpdate verifies the set functionality of the contract. - function testUpdate() public { - // Simulate pre-block call to set root. - vm.prank(sysaddr); - (bool ret, bytes memory data) = unit.call(bytes.concat(root)); - assertTrue(ret); - assertEq(data, hex""); - - // Verify timestamp. - bytes32 got = vm.load(unit, timestamp_idx()); - assertEq(got, timestamp()); - - // Verify root. - got = vm.load(unit, root_idx()); - assertEq(got, root); - } - - // testRingBuffers verifies the integrity of the ring buffer is maintained - // as the write indexes loop back to the start and begin overwriting - // values. - function testRingBuffers() public { - for (uint256 i = 0; i < 10000; i += 1) { - bytes32 pbbr = bytes32(i*1337); - - // Simulate pre-block call to set root. - vm.prank(sysaddr); - (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); - assertTrue(ret); - assertEq(data, hex""); - - // Call contract as normal account to get beacon root associated - // with current timestamp. - (ret, data) = unit.call(bytes.concat(timestamp())); - assertTrue(ret); - assertEq(data, bytes.concat(pbbr)); - - // Skip forward 12 seconds. - skip(12); - } - } - - - // testHistoricalReads verifies that it is possible to read all previously - // saved values in the beacon root contract. - function testHistoricalReads() public { - uint256 start = block.timestamp; - - // Saturate storage with fake roots. - for (uint256 i = 0; i < buflen; i += 1) { - bytes32 pbbr = bytes32(i*1337); - vm.prank(sysaddr); - (bool ret, bytes memory data) = unit.call(bytes.concat(pbbr)); - assertTrue(ret); - assertEq(data, hex""); - skip(12); - } - - // Attempt to read all values in same block context. - for (uint256 i = 0; i < buflen; i += 1) { - bytes32 time = bytes32(uint256(start+i*12)); - (bool ret, bytes memory got) = unit.call(bytes.concat(time)); - assertTrue(ret); - assertEq(got, bytes.concat(bytes32(i*1337))); - } - } -} From d8dcf8db593742c17c90e5fb297dff1058d4b8a3 Mon Sep 17 00:00:00 2001 From: lightclient Date: Sat, 26 Oct 2024 23:25:59 -0600 Subject: [PATCH 4/4] scripts: add addr miner in script for 2935 --- scripts/addr.sh | 4 ++++ test/ExecutionHash.t.sol.in | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/addr.sh b/scripts/addr.sh index 243fb3e..f83b3d3 100755 --- a/scripts/addr.sh +++ b/scripts/addr.sh @@ -42,6 +42,10 @@ case $1 in echo "searching for consolidations deployment data " nick search --score=$score --initcode="0x$(geas src/consolidations/ctor.eas)" --prefix=0x0000 --suffix=0xbbbb ;; + exechash|e|2935) + echo "searching for execution hash deployment data " + nick search --score=$score --initcode="0x$(geas src/execution_hash/ctor.eas)" --prefix=0x0000 --suffix=0xcccc + ;; *) echo "Invalid option. Usage: $0 {withdrawals|consolidations}" ;; diff --git a/test/ExecutionHash.t.sol.in b/test/ExecutionHash.t.sol.in index 910c814..118244d 100644 --- a/test/ExecutionHash.t.sol.in +++ b/test/ExecutionHash.t.sol.in @@ -146,4 +146,3 @@ contract ContractTest is Test { } } } -