diff --git a/build-wrapper b/build-wrapper index 08fcd13..52dcea7 100755 --- a/build-wrapper +++ b/build-wrapper @@ -3,13 +3,18 @@ set -euf -o pipefail SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; -WITHDRAWAL_BYTECODE="$(geas "src/withdrawals/main.eas")" -CONSOLODATION_BYTECODE="$(geas "src/consolidations/main.eas")" +BEACONROOT_BYTECODE="$(geas "src/beacon_root/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")" sed \ - -e "s/@bytecode@/$WITHDRAWAL_BYTECODE/" \ - -e "s/@bytecode2@/$FAKE_EXPO_BYTECODE/" \ + -e "s/@bytecode@/$BEACONROOT_BYTECODE/" \ + "$SCRIPT_DIR/test/BeaconRoot.t.sol.in" > "$SCRIPT_DIR/test/BeaconRoot.t.sol" + +sed \ + -e "s/@bytecode@/$WITHDRAWALS_BYTECODE/" \ + -e "s/@bytecode_expo@/$FAKE_EXPO_BYTECODE/" \ "$SCRIPT_DIR/test/Withdrawal.t.sol.in" > "$SCRIPT_DIR/test/Withdrawal.t.sol" sed \ @@ -17,8 +22,8 @@ sed \ "$SCRIPT_DIR/test/FakeExpo.t.sol.in" > "$SCRIPT_DIR/test/FakeExpo.t.sol" sed \ - -e "s/@bytecode@/$CONSOLODATION_BYTECODE/" \ - -e "s/@bytecode2@/$FAKE_EXPO_BYTECODE/" \ + -e "s/@bytecode@/$CONSOLODATIONS_BYTECODE/" \ + -e "s/@bytecode_expo@/$FAKE_EXPO_BYTECODE/" \ "$SCRIPT_DIR/test/Consolidation.t.sol.in" > "$SCRIPT_DIR/test/Consolidation.t.sol" forge "$@" --evm-version shanghai diff --git a/scripts/addr.sh b/scripts/addr.sh index afea941..243fb3e 100755 --- a/scripts/addr.sh +++ b/scripts/addr.sh @@ -30,6 +30,10 @@ default_score=5 score=${2:-$default_score} case $1 in + beaconroot|b|4788) + echo "searching for beacon root deployment data " + nick search --score=$score --initcode="0x$(geas src/beacon_root/ctor.eas)" --prefix=0xbeac02 --suffix=0x0000 + ;; withdrawals|wxs|7002) echo "searching for withdrawals deployment data " nick search --score=$score --initcode="0x$(geas src/withdrawals/ctor.eas)" --prefix=0x0000 --suffix=0xaaaa diff --git a/src/beacon_root/ctor.eas b/src/beacon_root/ctor.eas new file mode 100644 index 0000000..5c5a4a1 --- /dev/null +++ b/src/beacon_root/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/beacon_root/main.eas b/src/beacon_root/main.eas new file mode 100644 index 0000000..32063e6 --- /dev/null +++ b/src/beacon_root/main.eas @@ -0,0 +1,138 @@ +;; __ ___________ ____ +;; / // /__ ( __ )( __ )____ __________ ___ +;; / // /_ / / __ / __ / __ `/ ___/ __ `__ \ +;; /__ __// / /_/ / /_/ / /_/ (__ ) / / / / / +;; /_/ /_/\____/\____/\__,_/____/_/ /_/ /_/ +;; +;; This is an implementation of EIP-4788's predeploy contract. It implements +;; two ring buffers to create bounded beacon root lookup. The first ring +;; buffer is a timestamp % buflen -> timestamp mapping. This is used to ensure +;; timestamp argument actually matches the stored root and isn't different +;; dividend. The second ring buffer store the beacon root. It's also keyed by +;; timestamp % 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 +;; timestamps beacon roots +;; +;; To get the corresponding beacon root for a specific timestamp, simply add +;; buflen to the timestamp'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/BeaconRoot.t.sol.in b/test/BeaconRoot.t.sol.in new file mode 100644 index 0000000..330fbaf --- /dev/null +++ b/test/BeaconRoot.t.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))); + } + } +} diff --git a/test/Consolidation.t.sol.in b/test/Consolidation.t.sol.in index eb251f5..f779ea6 100644 --- a/test/Consolidation.t.sol.in +++ b/test/Consolidation.t.sol.in @@ -10,7 +10,7 @@ contract ConsolidationTest is Test { function setUp() public { vm.etch(addr, hex"@bytecode@"); - vm.etch(fakeExpo, hex"@bytecode2@"); + vm.etch(fakeExpo, hex"@bytecode_expo@"); } // testInvalidRequest checks that common invalid requests are rejected. diff --git a/test/Withdrawal.t.sol.in b/test/Withdrawal.t.sol.in index 1d65bfc..7d66895 100644 --- a/test/Withdrawal.t.sol.in +++ b/test/Withdrawal.t.sol.in @@ -11,7 +11,7 @@ contract WithdrawalsTest is Test { function setUp() public { vm.etch(addr, hex"@bytecode@"); - vm.etch(fakeExpo, hex"@bytecode2@"); + vm.etch(fakeExpo, hex"@bytecode_expo@"); unit = addr; }