Skip to content

Commit

Permalink
add 2935 as copy of 4788
Browse files Browse the repository at this point in the history
  • Loading branch information
lightclient committed Aug 26, 2024
1 parent 982a735 commit 73a0674
Show file tree
Hide file tree
Showing 5 changed files with 481 additions and 0 deletions.
5 changes: 5 additions & 0 deletions build-wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -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")"
Expand All @@ -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/" \
Expand Down
12 changes: 12 additions & 0 deletions src/execution_hash/ctor.eas
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
;; Copy and return code.
push @.end - @.start
dup1
push @.start
push0
codecopy
push0
return

.start:
#assemble "main.eas"
.end:
139 changes: 139 additions & 0 deletions src/execution_hash/main.eas
Original file line number Diff line number Diff line change
@@ -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 ;; []
163 changes: 163 additions & 0 deletions test/ExecutionHash.t.sol.in
Original file line number Diff line number Diff line change
@@ -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)));
}
}
}

Loading

0 comments on commit 73a0674

Please sign in to comment.