-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
982a735
commit 73a0674
Showing
5 changed files
with
481 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ;; [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); | ||
} | ||
} | ||
} | ||
|
Oops, something went wrong.