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))); + } + } +}