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

Implement consolidation contract #14

Merged
merged 2 commits into from
Jul 5, 2024
Merged
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
cache/
out/
test/Contract.t.sol
test/*.t.sol
21 changes: 16 additions & 5 deletions build-wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ set -euf -o pipefail

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

BYTECODE="$(geas "src/main.eas")"
BYTECODE2="$(geas "src/fake_expo_test.eas")"
WITHDRAWAL_BYTECODE="$(geas "src/withdrawal.eas")"
FAKE_EXPO_BYTECODE="$(geas "src/fake_expo_test.eas")"

CONSOLODATION_BYTECODE="$(geas "src/consolidation.eas")"

sed \
-e "s/@bytecode@/$WITHDRAWAL_BYTECODE/" \
-e "s/@bytecode2@/$FAKE_EXPO_BYTECODE/" \
"$SCRIPT_DIR/test/Withdrawal.t.sol.in" > "$SCRIPT_DIR/test/Withdrawal.t.sol"

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

sed \
-e "s/@bytecode@/$BYTECODE/" \
-e "s/@bytecode2@/$BYTECODE2/" \
"$SCRIPT_DIR/test/Contract.t.sol.in" > "$SCRIPT_DIR/test/Contract.t.sol"
-e "s/@bytecode@/$CONSOLODATION_BYTECODE/" \
-e "s/@bytecode2@/$FAKE_EXPO_BYTECODE/" \
"$SCRIPT_DIR/test/Consolidation.t.sol.in" > "$SCRIPT_DIR/test/Consolidation.t.sol"

forge "$@" --evm-version shanghai
383 changes: 383 additions & 0 deletions src/consolidation.eas

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions src/consolidation_ctor.eas
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
;; Store 1181 as a temporary excess value as it creates a fee so large that no
;; request will be accepted in the queue until after 7002 is activated and
;; called by the system for the first time.
push 1181
push0
sstore

;; Copy and return code.
push @.end - @.start
dup1
push @.start
push0
codecopy
push0
return

.start:
#assemble "consolidation.eas"
.end:
File renamed without changes.
2 changes: 1 addition & 1 deletion src/ctor.eas → src/withdrawal_ctor.eas
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ push0
return

.start:
#assemble "main.eas"
#assemble "withdrawal.eas"
.end:
209 changes: 209 additions & 0 deletions test/Consolidation.t.sol.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "./TestHelper.sol";

uint256 constant target_per_block = 1;
uint256 constant max_per_block = 1;

contract ConsolidationTest is TestHelper {

function setUp() public {
vm.etch(addr, hex"@bytecode@");
vm.etch(fakeExpo, hex"@bytecode2@");
}

// testInvalidRequest checks that common invalid requests are rejected.
function testInvalidRequest() public {
// pubkeys are too small
(bool ret,) = addr.call{value: 1e18}(hex"1234");
assertEq(ret, false);

// pubkeys 95 bytes
(ret,) = addr.call{value: 1e18}(hex"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
assertEq(ret, false);

// fee too small
(ret,) = addr.call{value: 0}(hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
assertEq(ret, false);
}

// testRequest verifies a single request below the target request
// count is accepted and read successfully.
function testRequest() public {
bytes memory data = hex"111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222";
(bool ret,) = addr.call{value: 2}(data);
assertEq(ret, true);
assertStorage(count_slot, 1, "unexpected request count");
assertExcess(0);

bytes memory req = getRequests();
assertEq(req.length, 116);
assertEq(toFixed(req, 20, 52), toFixed(data, 0, 32));
assertEq(toFixed(req, 52, 84), toFixed(data, 32, 64));
assertEq(toFixed(req, 84, 116), toFixed(data, 64, 96));
assertStorage(count_slot, 0, "unexpected request count");
assertExcess(0);
}

// testQueueReset verifies that after a period of time where there are more
// request than can be read per block, the queue is eventually cleared and the
// head and tails are reset to zero.
function testQueueReset() public {
// Add more requests than the max per block (1) so that the queue is not
// immediately emptied.
for (uint256 i = 0; i < max_per_block+1; i++) {
addRequest(address(uint160(i)), makeRequest(i), 2);
}
assertStorage(count_slot, max_per_block+1, "unexpected request count");

// Simulate syscall, check that max requests per block are read.
checkRequests(0, max_per_block);
assertExcess(1);

// Add another batch of max requests per block (1) so the next read leaves a
// single request in the queue.
for (uint256 i = 2; i < 3; i++) {
addRequest(address(uint160(i)), makeRequest(i), 2);
}
assertStorage(count_slot, max_per_block, "unexpected request count");

// Simulate syscall. Verify first that max per block are read. Then
// verify only the single final requst is read.
checkRequests(1, max_per_block);
assertExcess(1);
checkRequests(2, 1);
assertExcess(0);

// Now ensure the queue is empty and has reset to zero.
assertStorage(queue_head_slot, 0, "expected queue head reset");
assertStorage(queue_tail_slot, 0, "expected queue tail reset");

// Add five (5) more requests to check that new requests can be added after the queue
// is reset.
for (uint256 i = 3; i < 8; i++) {
addRequest(address(uint160(i)), makeRequest(i), 4);
}
assertStorage(count_slot, 5, "unexpected request count");

// Simulate syscall, read only the max requests per block.
checkRequests(3, 1);
assertExcess(4);
}

// testFee adds many requests, and verifies the fee decreases correctly until
// it returns to 0.
function testFee() public {
uint256 idx = 0;
uint256 count = max_per_block*64;

// Add a bunch of requests.
for (; idx < count; idx++) {
addRequest(address(uint160(idx)), makeRequest(idx), 1);
}
assertStorage(count_slot, count, "unexpected request count");
checkRequests(0, max_per_block);

uint256 read = max_per_block;
uint256 excess = count - target_per_block;

// Attempt to add an invalid request with fee too low or a valid request.
// This should cause the excess requests counter to either decrease by 1
// or remain the same each iteration.
for (uint256 i = 0; i < count; i++) {
assertExcess(excess);

uint256 fee = computeFee(excess);
if (idx % 2 == 0) {
addRequest(address(uint160(idx)), makeRequest(idx), fee);
} else {
addFailedRequest(address(uint160(idx)), makeRequest(idx), fee-1);
}

uint256 expected = min(idx-read+1, max_per_block);
checkRequests(read, expected);

if (excess > 0 && idx % 2 != 0) {
excess--;
}
read += expected;
idx++;
}

}

// testInhibitorRest verifies that after the first system call the excess
// value is reset to 0.
function testInhibitorReset() public {
vm.store(addr, bytes32(0), bytes32(uint256(1181)));
vm.prank(sysaddr);
(bool ret, bytes memory data) = addr.call("");
assertStorage(excess_slot, 0, "expected excess requests to be reset");

vm.store(addr, bytes32(0), bytes32(uint256(1180)));
vm.prank(sysaddr);
(ret, data) = addr.call("");
assertStorage(excess_slot, 1180-target_per_block, "didn't expect excess to be reset");
}

// --------------------------------------------------------------------------
// helpers ------------------------------------------------------------------
// --------------------------------------------------------------------------

// addRequest will submit a request to the system contract with the given values.
function addRequest(address from, bytes memory req, uint256 value) internal {
// Load tail index before adding request.
uint256 requests = load(count_slot);
uint256 tail = load(queue_tail_slot);

// Send request from address.
vm.deal(from, value);
vm.prank(from);
(bool ret,) = addr.call{value: value}(req);
assertEq(ret, true, "expected call to succeed");

// Verify the queue data was updated correctly.
assertStorage(count_slot, requests+1, "unexpected request count");
assertStorage(queue_tail_slot, tail+1, "unexpected tail slot");

// Verify the request was written to the queue.
uint256 idx = queue_storage_offset+tail*4;
assertStorage(idx, uint256(uint160(from)), "addr not written to queue");
assertStorage(idx+1, toFixed(req, 0, 32), "source[0:32] not written to queue");
assertStorage(idx+2, toFixed(req, 32, 64), "source[32:48] ++ target[0:16] not written to queue");
assertStorage(idx+3, toFixed(req, 64, 96), "target[16:48] not written to queue");
}

// checkRequest will simulate a system call to the system contract and verify
// the expected requests are returned.
//
// It assumes that addresses are stored as uint256(index) and pubkeys are
// uint8(index), repeating.
function checkRequests(uint256 startIndex, uint256 count) internal returns (uint256) {
bytes memory requests = getRequests();
assertEq(requests.length, count*116);
for (uint256 i = 0; i < count; i++) {
uint256 offset = i*116;
assertEq(toFixed(requests, offset, offset+20) >> 96, uint256(startIndex+i), "unexpected request address returned");
assertEq(toFixed(requests, offset+20, offset+52), toFixed(makeRequest(startIndex+i), 0, 32), "unexpected source[0:32] returned");
assertEq(toFixed(requests, offset+52, offset+84), toFixed(makeRequest(startIndex+i), 32, 64), "unexpected source[32:48] ++ target[0:16] returned");
assertEq(toFixed(requests, offset+84, offset+116), toFixed(makeRequest(startIndex+i), 64, 96), "unexpected target[16:48] returned");
}
return count;
}

function makeRequest(uint256 x) internal pure returns (bytes memory) {
bytes memory out = new bytes(96);

// source
for (uint256 i = 0; i < 48; i++) {
out[i] = bytes1(uint8(x));
}
// target
for (uint256 i = 0; i < 48; i++) {
out[48 + i] = bytes1(uint8(x+1));
}

return out;
}
}
15 changes: 15 additions & 0 deletions test/FakeExpo.t.sol.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "./TestHelper.sol";

contract FakeExpoTest is TestHelper {
function setUp() public {
vm.etch(fakeExpo, hex"@bytecode@");
}

// testFakeExpo calls the fake exponentiation logic with specific values.
function testFakeExpo() public {
assertEq(callFakeExpo(1, 100, 17), 357);
}
}
72 changes: 72 additions & 0 deletions test/TestHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";

address constant fakeExpo = 0x000000000000000000000000000000000000BbBB;
address constant addr = 0x000000000000000000000000000000000000aaaa;
address constant sysaddr = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;

uint256 constant excess_slot = 0;
uint256 constant count_slot = 1;
uint256 constant queue_head_slot = 2;
uint256 constant queue_tail_slot = 3;
uint256 constant queue_storage_offset = 4;

abstract contract TestHelper is Test {

function min(uint256 x, uint256 y) internal pure returns (uint256) {
if (x < y) {
return x;
}
return y;
}

function addFailedRequest(address from, bytes memory req, uint256 value) internal {
vm.deal(from, value);
vm.prank(from);
(bool ret,) = addr.call{value: value}(req);
assertEq(ret, false, "expected request to fail");
}

// getRequests will simulate a system call to the system contract.
function getRequests() internal returns (bytes memory) {
vm.prank(sysaddr);
(bool ret, bytes memory data) = addr.call("");
assertEq(ret, true);
return data;
}

function load(uint256 slot) internal view returns (uint256) {
return uint256(vm.load(addr, bytes32(slot)));
}

function assertStorage(uint256 slot, uint256 value, string memory err) internal {
bytes32 got = vm.load(addr, bytes32(slot));
assertEq(got, bytes32(value), err);
}

function assertExcess(uint256 count) internal {
assertStorage(excess_slot, count, "unexpected excess requests");
(, bytes memory data) = addr.call("");
assertEq(toFixed(data, 0, 32), count, "unexpected excess requests");
}

function toFixed(bytes memory data, uint256 start, uint256 end) internal pure returns (uint256) {
require(end-start <= 32, "range cannot be larger than 32 bytes");
bytes memory out = new bytes(32);
for (uint256 i = start; i < end; i++) {
out[i-start] = data[i];
}
return uint256(bytes32(out));
}

function computeFee(uint256 excess) internal returns (uint256) {
return callFakeExpo(1, int(excess), 17);
}

function callFakeExpo(int factor, int numerator, int denominator) internal returns (uint256) {
(, bytes memory data) = fakeExpo.call(bytes.concat(bytes32(uint256(factor)), bytes32(uint256(numerator)), bytes32(uint256(denominator))));
return toFixed(data, 0, 32);
}
}
Loading