Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP-2935 system contract impl #19

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading