Skip to content

Commit 982a735

Browse files
authored
Merge pull request #18 from lightclient/add-4788
Add 4788 beacon contract (converted from etk)
2 parents ee61e80 + 75e602a commit 982a735

File tree

7 files changed

+329
-8
lines changed

7 files changed

+329
-8
lines changed

build-wrapper

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@ set -euf -o pipefail
33

44
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
55

6-
WITHDRAWAL_BYTECODE="$(geas "src/withdrawals/main.eas")"
7-
CONSOLODATION_BYTECODE="$(geas "src/consolidations/main.eas")"
6+
BEACONROOT_BYTECODE="$(geas "src/beacon_root/main.eas")"
7+
WITHDRAWALS_BYTECODE="$(geas "src/withdrawals/main.eas")"
8+
CONSOLODATIONS_BYTECODE="$(geas "src/consolidations/main.eas")"
89
FAKE_EXPO_BYTECODE="$(geas "src/common/fake_expo_test.eas")"
910

1011
sed \
11-
-e "s/@bytecode@/$WITHDRAWAL_BYTECODE/" \
12-
-e "s/@bytecode2@/$FAKE_EXPO_BYTECODE/" \
12+
-e "s/@bytecode@/$BEACONROOT_BYTECODE/" \
13+
"$SCRIPT_DIR/test/BeaconRoot.t.sol.in" > "$SCRIPT_DIR/test/BeaconRoot.t.sol"
14+
15+
sed \
16+
-e "s/@bytecode@/$WITHDRAWALS_BYTECODE/" \
17+
-e "s/@bytecode_expo@/$FAKE_EXPO_BYTECODE/" \
1318
"$SCRIPT_DIR/test/Withdrawal.t.sol.in" > "$SCRIPT_DIR/test/Withdrawal.t.sol"
1419

1520
sed \
1621
-e "s/@bytecode@/$FAKE_EXPO_BYTECODE/" \
1722
"$SCRIPT_DIR/test/FakeExpo.t.sol.in" > "$SCRIPT_DIR/test/FakeExpo.t.sol"
1823

1924
sed \
20-
-e "s/@bytecode@/$CONSOLODATION_BYTECODE/" \
21-
-e "s/@bytecode2@/$FAKE_EXPO_BYTECODE/" \
25+
-e "s/@bytecode@/$CONSOLODATIONS_BYTECODE/" \
26+
-e "s/@bytecode_expo@/$FAKE_EXPO_BYTECODE/" \
2227
"$SCRIPT_DIR/test/Consolidation.t.sol.in" > "$SCRIPT_DIR/test/Consolidation.t.sol"
2328

2429
forge "$@" --evm-version shanghai

scripts/addr.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ default_score=5
3030
score=${2:-$default_score}
3131

3232
case $1 in
33+
beaconroot|b|4788)
34+
echo "searching for beacon root deployment data "
35+
nick search --score=$score --initcode="0x$(geas src/beacon_root/ctor.eas)" --prefix=0xbeac02 --suffix=0x0000
36+
;;
3337
withdrawals|wxs|7002)
3438
echo "searching for withdrawals deployment data "
3539
nick search --score=$score --initcode="0x$(geas src/withdrawals/ctor.eas)" --prefix=0x0000 --suffix=0xaaaa

src/beacon_root/ctor.eas

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
;; Copy and return code.
2+
push @.end - @.start
3+
dup1
4+
push @.start
5+
push0
6+
codecopy
7+
push0
8+
return
9+
10+
.start:
11+
#assemble "main.eas"
12+
.end:

src/beacon_root/main.eas

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
;; __ ___________ ____
2+
;; / // /__ ( __ )( __ )____ __________ ___
3+
;; / // /_ / / __ / __ / __ `/ ___/ __ `__ \
4+
;; /__ __// / /_/ / /_/ / /_/ (__ ) / / / / /
5+
;; /_/ /_/\____/\____/\__,_/____/_/ /_/ /_/
6+
;;
7+
;; This is an implementation of EIP-4788's predeploy contract. It implements
8+
;; two ring buffers to create bounded beacon root lookup. The first ring
9+
;; buffer is a timestamp % buflen -> timestamp mapping. This is used to ensure
10+
;; timestamp argument actually matches the stored root and isn't different
11+
;; dividend. The second ring buffer store the beacon root. It's also keyed by
12+
;; timestamp % buflen and the shifted right by buflen so the two don't overlap.
13+
;;
14+
;; The ring buffers can be visualized as follows:
15+
;;
16+
;; buflen = 10
17+
;; |--------------|--------------|
18+
;; 0 10 20
19+
;; timestamps beacon roots
20+
;;
21+
;; To get the corresponding beacon root for a specific timestamp, simply add
22+
;; buflen to the timestamp's index in the first ring buffer. The sum will be
23+
;; the storage slot in the second ring buffer where it is stored.
24+
25+
26+
;; ----------------------------------------------------------------------------
27+
;; MACROS ---------------------------------------------------------------------
28+
;; ----------------------------------------------------------------------------
29+
30+
;; BUFLEN returns the HISTORY_BUFFER_LENGTH as defined in the EIP.
31+
#define BUFLEN 8191
32+
33+
;; SYSADDR is the address which calls the contract to submit a new root.
34+
#define SYSADDR 0xfffffffffffffffffffffffffffffffffffffffe
35+
36+
;; do_revert sets up and then executes a revert(0,0) operation.
37+
#define %do_revert() {
38+
push0 ;; [0]
39+
push0 ;; [0, 0]
40+
revert ;; []
41+
}
42+
43+
;; ----------------------------------------------------------------------------
44+
;; MACROS END -----------------------------------------------------------------
45+
;; ----------------------------------------------------------------------------
46+
47+
.start:
48+
;; Protect the submit routine by verifying the caller is equal to
49+
;; sysaddr().
50+
caller ;; [caller]
51+
push20 SYSADDR ;; [sysaddr, caller]
52+
eq ;; [sysaddr == caller]
53+
push1 @submit ;; [submit_lbl, sysaddr == caller]
54+
jumpi ;; []
55+
56+
;; Fallthrough if addresses don't match -- this means the caller intends
57+
;; to read a root.
58+
59+
;; Check if calldata is equal to 32 bytes.
60+
push1 32 ;; [32]
61+
calldatasize ;; [calldatasize, 32]
62+
eq ;; [calldatasize == 32]
63+
64+
;; Jump to continue if length-check passed, otherwise revert.
65+
push1 @loadtime ;; [loadtime_lbl, calldatasize == 32]
66+
jumpi ;; []
67+
%do_revert() ;; []
68+
69+
loadtime:
70+
;; Load input timestamp.
71+
push0 ;; [0]
72+
calldataload ;; [input_timestamp]
73+
dup1 ;; [input_timestamp, input_timestamp]
74+
75+
;; Verify input timestamp is non-zero.
76+
iszero ;; [input_timestamp == 0, input_timestamp]
77+
push1 @throw ;; [throw_lbl, input_timestamp == 0, input_timestamp]
78+
jumpi ;; [input_timestamp]
79+
80+
;; Compute the timestamp index and load from storage.
81+
push3 BUFLEN ;; [buflen, input_timestamp]
82+
dup2 ;; [input_timestamp, buflen, input_timestamp]
83+
mod ;; [time_index, input_timestamp]
84+
swap1 ;; [input_timestamp, time_index]
85+
dup2 ;; [time_index, input_timestamp, time_index]
86+
sload ;; [stored_timestamp, input_timestamp, time_index]
87+
88+
;; Verify stored timestamp matches input timestamp. It's possible these
89+
;; don't match if the slot has been overwritten by the ring buffer or if
90+
;; the timestamp input wasn't a valid previous timestamp.
91+
eq ;; [stored_timestamp == input_timestamp, time_index]
92+
push1 @loadroot ;; [loadroot_lbl, input == timestamp, time_index]
93+
jumpi ;; [time_index]
94+
%do_revert() ;; []
95+
96+
loadroot:
97+
;; Extend index to get root index.
98+
push3 BUFLEN ;; [buflen, time_index]
99+
add ;; [root_index]
100+
sload ;; [root]
101+
102+
;; Write the retrieved root to memory so it can be returned.
103+
push0 ;; [0, root]
104+
mstore ;; []
105+
106+
;; Return the root.
107+
push1 32 ;; [size]
108+
push0 ;; [offset, size]
109+
return ;; []
110+
111+
throw:
112+
;; Reverts current execution with no return data.
113+
%do_revert()
114+
115+
submit:
116+
;; Calculate the index the timestamp should be stored at, e.g.
117+
;; time_index = (time % buflen).
118+
push3 BUFLEN ;; [buflen]
119+
timestamp ;; [time, buflen]
120+
mod ;; [time % buflen]
121+
122+
;; Write timestamp into storage slot at time_index.
123+
timestamp ;; [time, time_index]
124+
dup2 ;; [time_index, time, time_index]
125+
sstore ;; [time_index]
126+
127+
;; Get root from calldata and write into root_index. No validation is
128+
;; done on the input root. Becuase the routine is protected by a caller
129+
;; check against sysaddr(), it's okay to assume the value is correctly
130+
;; given.
131+
push0 ;; [0, time_index]
132+
calldataload ;; [root, time_index]
133+
swap1 ;; [time_index, root]
134+
push3 BUFLEN ;; [buflen, time_index, root]
135+
add ;; [root_index, root]
136+
sstore ;; []
137+
138+
stop ;; []

test/BeaconRoot.t.sol.in

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Test.sol";
5+
import "../src/Contract.sol";
6+
7+
address constant addr = 0x000000000000000000000000000000000000000b;
8+
address constant sysaddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;
9+
uint256 constant buflen = 8191;
10+
bytes32 constant root = hex"88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6";
11+
12+
function timestamp() view returns (bytes32) {
13+
return bytes32(uint256(block.timestamp));
14+
}
15+
16+
function timestamp_idx() view returns (bytes32) {
17+
return bytes32(uint256(block.timestamp % buflen));
18+
}
19+
20+
function root_idx() view returns (bytes32) {
21+
return bytes32(uint256(block.timestamp % buflen + buflen));
22+
}
23+
24+
contract ContractTest is Test {
25+
address unit;
26+
27+
function setUp() public {
28+
vm.etch(addr, hex"@bytecode@");
29+
unit = addr;
30+
}
31+
32+
// testRead verifies the contract returns the expected beacon root.
33+
function testRead() public {
34+
// Store timestamp and root at expected indexes.
35+
vm.store(unit, timestamp_idx(), timestamp());
36+
vm.store(unit, root_idx(), root);
37+
38+
// Read root associated with current timestamp.
39+
(bool ret, bytes memory data) = unit.call(bytes.concat(timestamp()));
40+
assertTrue(ret);
41+
assertEq(data, bytes.concat(root));
42+
}
43+
44+
function testReadBadCalldataSize() public {
45+
uint256 time = block.timestamp;
46+
47+
// Store timestamp and root at expected indexes.
48+
vm.store(unit, timestamp_idx(), bytes32(time));
49+
vm.store(unit, root_idx(), root);
50+
51+
// Call with 0 byte arguement.
52+
(bool ret, bytes memory data) = unit.call(hex"");
53+
assertFalse(ret);
54+
assertEq(data, hex"");
55+
56+
// Call with 31 byte arguement.
57+
(ret, data) = unit.call(hex"00000000000000000000000000000000000000000000000000000000001337");
58+
assertFalse(ret);
59+
assertEq(data, hex"");
60+
61+
// Call with 33 byte arguement.
62+
(ret, data) = unit.call(hex"000000000000000000000000000000000000000000000000000000000000001337");
63+
assertFalse(ret);
64+
assertEq(data, hex"");
65+
}
66+
67+
function testReadWrongTimestamp() public {
68+
// Set reasonable timestamp.
69+
vm.warp(1641070800);
70+
uint256 time = block.timestamp;
71+
72+
// Store timestamp and root at expected indexes.
73+
vm.store(unit, timestamp_idx(), bytes32(time));
74+
vm.store(unit, root_idx(), root);
75+
76+
// Wrap around buflen once forward.
77+
(bool ret, bytes memory data) = unit.call(bytes.concat(bytes32(time+buflen)));
78+
assertFalse(ret);
79+
assertEq(data, hex"");
80+
81+
// Wrap around buflen once backward.
82+
(ret, data) = unit.call(bytes.concat(bytes32(time-buflen)));
83+
assertFalse(ret);
84+
assertEq(data, hex"");
85+
86+
// Timestamp without any associated root.
87+
(ret, data) = unit.call(bytes.concat(bytes32(time+1)));
88+
assertFalse(ret);
89+
assertEq(data, hex"");
90+
91+
// Timestamp zero should fail.
92+
(ret, data) = unit.call(bytes.concat(bytes32(0)));
93+
assertFalse(ret);
94+
assertEq(data, hex"");
95+
}
96+
97+
// testUpdate verifies the set functionality of the contract.
98+
function testUpdate() public {
99+
// Simulate pre-block call to set root.
100+
vm.prank(sysaddr);
101+
(bool ret, bytes memory data) = unit.call(bytes.concat(root));
102+
assertTrue(ret);
103+
assertEq(data, hex"");
104+
105+
// Verify timestamp.
106+
bytes32 got = vm.load(unit, timestamp_idx());
107+
assertEq(got, timestamp());
108+
109+
// Verify root.
110+
got = vm.load(unit, root_idx());
111+
assertEq(got, root);
112+
}
113+
114+
// testRingBuffers verifies the integrity of the ring buffer is maintained
115+
// as the write indexes loop back to the start and begin overwriting
116+
// values.
117+
function testRingBuffers() public {
118+
for (uint256 i = 0; i < 10000; i += 1) {
119+
bytes32 pbbr = bytes32(i*1337);
120+
121+
// Simulate pre-block call to set root.
122+
vm.prank(sysaddr);
123+
(bool ret, bytes memory data) = unit.call(bytes.concat(pbbr));
124+
assertTrue(ret);
125+
assertEq(data, hex"");
126+
127+
// Call contract as normal account to get beacon root associated
128+
// with current timestamp.
129+
(ret, data) = unit.call(bytes.concat(timestamp()));
130+
assertTrue(ret);
131+
assertEq(data, bytes.concat(pbbr));
132+
133+
// Skip forward 12 seconds.
134+
skip(12);
135+
}
136+
}
137+
138+
139+
// testHistoricalReads verifies that it is possible to read all previously
140+
// saved values in the beacon root contract.
141+
function testHistoricalReads() public {
142+
uint256 start = block.timestamp;
143+
144+
// Saturate storage with fake roots.
145+
for (uint256 i = 0; i < buflen; i += 1) {
146+
bytes32 pbbr = bytes32(i*1337);
147+
vm.prank(sysaddr);
148+
(bool ret, bytes memory data) = unit.call(bytes.concat(pbbr));
149+
assertTrue(ret);
150+
assertEq(data, hex"");
151+
skip(12);
152+
}
153+
154+
// Attempt to read all values in same block context.
155+
for (uint256 i = 0; i < buflen; i += 1) {
156+
bytes32 time = bytes32(uint256(start+i*12));
157+
(bool ret, bytes memory got) = unit.call(bytes.concat(time));
158+
assertTrue(ret);
159+
assertEq(got, bytes.concat(bytes32(i*1337)));
160+
}
161+
}
162+
}

test/Consolidation.t.sol.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ contract ConsolidationTest is Test {
1010

1111
function setUp() public {
1212
vm.etch(addr, hex"@bytecode@");
13-
vm.etch(fakeExpo, hex"@bytecode2@");
13+
vm.etch(fakeExpo, hex"@bytecode_expo@");
1414
}
1515

1616
// testInvalidRequest checks that common invalid requests are rejected.

test/Withdrawal.t.sol.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ contract WithdrawalsTest is Test {
1111

1212
function setUp() public {
1313
vm.etch(addr, hex"@bytecode@");
14-
vm.etch(fakeExpo, hex"@bytecode2@");
14+
vm.etch(fakeExpo, hex"@bytecode_expo@");
1515
unit = addr;
1616
}
1717

0 commit comments

Comments
 (0)