Skip to content

Commit

Permalink
2935: improve impl and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lightclient committed Oct 27, 2024
1 parent 8a0274a commit 418a7dd
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 287 deletions.
2 changes: 1 addition & 1 deletion build-wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ sed \
"$SCRIPT_DIR/test/BeaconRoot.t.sol.in" > "$SCRIPT_DIR/test/BeaconRoot.t.sol"

sed \
-e "s/@bytecode@/$BEACONROOT_BYTECODE/" \
-e "s/@bytecode@/$EXECHASH_BYTECODE/" \
"$SCRIPT_DIR/test/ExecutionHash.t.sol.in" > "$SCRIPT_DIR/test/ExecutionHash.t.sol"

sed \
Expand Down
108 changes: 43 additions & 65 deletions src/execution_hash/main.eas
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,10 @@
;; ┏┛┗┫ ┫┗┓┏┓┏┏┳┓
;; ┗━┗┛┗┛┗┛┗┻┛┛┗┗
;;
;; 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.
;; This is an implementation of EIP-2935's predeploy contract.
;;
;; 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.

;; The contract implements a ring buffer to create bounded execution block
;; hash lookup.

;; ----------------------------------------------------------------------------
;; MACROS ---------------------------------------------------------------------
Expand Down Expand Up @@ -56,69 +39,64 @@
;; Fallthrough if addresses don't match -- this means the caller intends
;; to read a root.

;; Verify input is 32 bytes long.
push1 32 ;; [32]
calldatasize ;; [calldatasize, 32]
eq ;; [calldatasize == 32]

;; Check if input > 8 byte value and revert if this isn't the case the
;; check is performed by comparing the biggest 8 byte number with the
;; call data, which is a right-padded 32 byte number.
push 0xffffffffffffffff ;; [2^32-1]
push 0 ;; [0, 2^32-1]
calldataload ;; [input, 2^32-1]
gt ;; [input > 2^32-1]
jumpi @throw ;; []

;; Check if input > number-1 then return 0.
push 1 ;; [1]
number ;; [number, 1]
sub ;; [number-1]
push0 ;; [0, number-1]
calldataload ;; [input, number-1]
gt ;; [input > number-1]
jumpi @zeroreturn

;; Check if blocknumber > input + 8192 then return 0, no overflow
;; expected for input of < max 8 byte value
;; Jump to continue if length-check passed, otherwise revert.
jumpi @load ;; []
%do_revert() ;; []

load:
;; Check if input is requesting a block hash greater than current block
;; number.
push 0 ;; [0]
calldataload ;; [input]
push 8192 ;; [8192, input]
add ;; [input+8192]
number ;; [number, input+8192]
gt ;; [number > input+8192]
jumpi @zeroreturn ;; []

;; mod 8191 and sload
push 8191 ;; [8191]
push 0 ;; [0, 8191]
calldataload ;; [input, 8191]
and ;; [input % 8191]
push 1 ;; [1, input]
number ;; [number, 1, input]
sub ;; [number-1, input]
dup2 ;; [input, number-1, input]
gt ;; [input > number-1, input]
jumpi @throw ;; [input]

;; Check if the input is requesting a block hash before the earliest available
;; hash currently. Since we've verfied that input <= number - 1, it's safe to
;; check the following:
;; number - 1 - input <= BUFLEN, which also equals: number - input < BUFLEN
dup1 ;; [input, input]
number ;; [number, input, input]
sub ;; [number - input, input]
push BUFLEN+1 ;; [buflen, number - input, input]
lt ;; [buflen < number - input, input]
jumpi @throw ;; [input]

;; Load the hash.
push BUFLEN ;; [buflen, input]
swap1 ;; [input, buflen]
mod ;; [input % buflen]
sload ;; [hash]

;; load into mem and return 32 bytes
;; Load into memory and return.
push 0 ;; [0, hash]
mstore ;; []
push 32 ;; [32]
push 0 ;; [0, 32]
return ;; []

zeroreturn:
push 0 ;; [0]
push 0 ;; [0, 0]
mstore ;; []
push 32 ;; [32]
push 0 ;; [0, 32]
return ;; []

throw:
;; Reverts current execution with no return data.
pop
%do_revert()

submit:
push 0 ;; [0]
calldataload ;; [in]
push 8191 ;; [8191, in]
push 1 ;; [1, 8191, in]
number ;; [number, 1, 8191, in]
sub ;; [number-1, 8191, in]
and ;; [number-1 % 8191, in]
push BUFLEN ;; [buflen, in]
push 1 ;; [1, buflen, in]
number ;; [number, 1, buflen, in]
sub ;; [number-1, buflen, in]
mod ;; [number-1 % buflen, in]
sstore

stop
102 changes: 44 additions & 58 deletions test/ExecutionHash.t.sol.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@ pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Contract.sol";

address constant addr = 0x000000000000000000000000000000000000000b;
address constant addr = 0x000000000000000000000000000000000000aaaa;
address constant sysaddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;
uint256 constant buflen = 8191;
bytes32 constant root = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6";
bytes32 constant hash = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6";

function timestamp() view returns (bytes32) {
return bytes32(uint256(block.timestamp));
function lastBlockNumber() view returns (bytes32) {
return bytes32(uint256(block.number)-1);
}

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));
function hash_idx() view returns (bytes32) {
return bytes32(uint256(lastBlockNumber()) % buflen);
}

contract ContractTest is Test {
Expand All @@ -29,24 +25,20 @@ contract ContractTest is Test {
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);
// testRead verifies the contract returns the expected execution hash.
function testExecRead() public {
// Store hash at expected indexes.
vm.store(unit, hash_idx(), hash);

// Read root associated with current timestamp.
(bool ret, bytes memory data) = unit.call(bytes.concat(timestamp()));
// Read hash associated with current timestamp.
(bool ret, bytes memory data) = unit.call(bytes.concat(lastBlockNumber()));
assertTrue(ret);
assertEq(data, bytes.concat(root));
assertEq(data, bytes.concat(hash));
}

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);
// Store hash at expected indexes.
vm.store(unit, hash_idx(), hash);

// Call with 0 byte arguement.
(bool ret, bytes memory data) = unit.call(hex"");
Expand All @@ -64,97 +56,91 @@ contract ContractTest is Test {
assertEq(data, hex"");
}

function testReadWrongTimestamp() public {
// Set reasonable timestamp.
vm.warp(1641070800);
uint256 time = block.timestamp;
function testReadBadBlockNumbers() public {
// Set reasonable block number.
vm.roll(21053500);
uint256 number = block.number-1;

// Store timestamp and root at expected indexes.
vm.store(unit, timestamp_idx(), bytes32(time));
vm.store(unit, root_idx(), root);
// Store hash at expected indexes.
vm.store(unit, hash_idx(), hash);

// Wrap around buflen once forward.
(bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(time+buflen)));
(bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(number+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)));
(ret, data) = unit.call(bytes.concat(bytes32(number-buflen-1)));
assertFalse(ret);
assertEq(data, hex"");

// Timestamp zero should fail.
// Block number 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.
// Simulate pre-block call to set hash.
vm.prank(sysaddr);
(bool ret, bytes memory data) = unit.call(bytes.concat(root));
(bool ret, bytes memory data) = unit.call(bytes.concat(hash));
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);
// Verify hash.
bytes32 got = vm.load(unit, hash_idx());
assertEq(got, hash);
}

// 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 {
// Set reasonable block number.
vm.roll(21053500);

for (uint256 i = 0; i < 10000; i += 1) {
bytes32 pbbr = bytes32(i*1337);

// Simulate pre-block call to set root.
// Simulate pre-block call to set hash.
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
// Call contract as normal account to get exeuction hash associated
// with current timestamp.
(ret, data) = unit.call(bytes.concat(timestamp()));
(ret, data) = unit.call(bytes.concat(lastBlockNumber()));
assertTrue(ret);
assertEq(data, bytes.concat(pbbr));

// Skip forward 12 seconds.
skip(12);
// Skip forward 1 block.
vm.roll(block.number+1);
}
}


// testHistoricalReads verifies that it is possible to read all previously
// saved values in the beacon root contract.
// saved values in the beacon hash contract.
function testHistoricalReads() public {
uint256 start = block.timestamp;
uint256 start = 1;
vm.roll(start);

// Saturate storage with fake roots.
// Saturate storage with fake hashs.
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);
vm.roll(block.number+1);
}

// 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));
bytes32 num = bytes32(uint256(start+i-1));
(bool ret, bytes memory got) = unit.call(bytes.concat(num));
assertTrue(ret);
assertEq(got, bytes.concat(bytes32(i*1337)));
}
Expand Down
Loading

0 comments on commit 418a7dd

Please sign in to comment.