Skip to content

Commit

Permalink
Add block hash system contract (#229)
Browse files Browse the repository at this point in the history
* chore: forge init

* forge install: forge-std

v1.7.6

* Add blockhash contract

* Change license

* Add merkle root info

* Add initialize block number function and natspec

* Update foundry workflow

* update ci

* try running in parallel

* use single workflow

---------

Co-authored-by: eyusufatik <esadyusufatik@gmail.com>
  • Loading branch information
okkothejawa and eyusufatik authored Mar 13, 2024
1 parent 83a3cae commit b21a9e3
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 1 deletion.
33 changes: 32 additions & 1 deletion .github/workflows/rust.yml → .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Rust
name: Checks

# On Rust, GitHub Actions, and caching
# ===========
Expand Down Expand Up @@ -50,6 +50,8 @@ on:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
FOUNDRY_PROFILE: ci


# Automatically cancels a job if a new commit if pushed to the same PR, branch, or tag.
# Source: <https://stackoverflow.com/a/72408109/5148606>
Expand Down Expand Up @@ -170,3 +172,32 @@ jobs:
run: rustup show
- name: Run nextest
run: SKIP_GUEST_BUILD=1 make test

system-contracts:
strategy:
fail-fast: true

name: Foundry project
runs-on: ubicloud-standard-2
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run Forge build
run: |
cd module-system/module-implementations/sov-evm/src/evm/system_contracts
forge --version
forge build --sizes
id: build

- name: Run Forge tests
run: |
cd module-system/module-implementations/sov-evm/src/evm/system_contracts
forge test -vvv
id: test
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/forge-std"]
path = module-system/module-implementations/sov-evm/src/evm/system_contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## Foundry

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**

Foundry consists of:

- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.

## Documentation

https://book.getfoundry.sh/

## Usage

### Build

```shell
$ forge build
```

### Test

```shell
$ forge test
```

### Format

```shell
$ forge fmt
```

### Gas Snapshots

```shell
$ forge snapshot
```

### Anvil

```shell
$ anvil
```

### Deploy

```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```

### Cast

```shell
$ cast <subcommand>
```

### Help

```shell
$ forge --help
$ anvil --help
$ cast --help
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

abstract contract Ownable {
address public owner;
address public pendingOwner;

event OwnershipTransferred(address previousOwner, address newOwner);
event OwnershipTransferRequested(address previousOwner, address newOwner);

modifier onlyOwner() {
require(msg.sender == owner, "Caller is not owner");
_;
}

modifier onlyPendingOwner() {
require(msg.sender == pendingOwner, "Caller is not pending owner");
_;
}

constructor() {
owner = msg.sender;
}

function renounceOwnership() public onlyOwner {
owner = address(0);
emit OwnershipTransferred(owner, address(0));
}

function transferOwnership(address newOwner) public onlyOwner {
pendingOwner = newOwner;
emit OwnershipTransferRequested(owner, newOwner);
}

function acceptOwnership() public onlyPendingOwner {
address old_owner = owner;
owner = pendingOwner;
pendingOwner = address(0);
emit OwnershipTransferred(old_owner, pendingOwner);
}
}
Submodule forge-std added at ae570f
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "../lib/Ownable.sol";
import "./interfaces/IL1BlockHashList.sol";

/// @title A system contract that stores block hashes and merkle roots of L1 blocks
/// @author Citrea

contract L1BlockHashList is Ownable, IL1BlockHashList {
mapping(uint256 => bytes32) public blockHashes;
mapping(bytes32 => bytes32) public merkleRoots;
uint256 public blockNumber;

event BlockInfoAdded(uint256 blockNumber, bytes32 blockHash, bytes32 merkleRoot);
constructor() Ownable(){ }

/// @notice Sets the initial value for the block number, can only be called once
/// @param _blockNumber The L1 block number that is associated with the genesis block of Citrea
function initializeBlockNumber(uint256 _blockNumber) public onlyOwner {
require(blockNumber == 0, "Already initialized");
blockNumber = _blockNumber;
}

/// @notice Sets the block hash and merkle root for a given block
/// @notice Can only be called after the initial block number is set
/// @dev The block number is incremented by the contract as no block info should be overwritten or skipped
/// @param _blockHash The hash of the current L1 block
/// @param _merkleRoot The merkle root of the current L1 block
function setBlockInfo(bytes32 _blockHash, bytes32 _merkleRoot) public onlyOwner {
uint256 _blockNumber = blockNumber;
require(_blockNumber != 0, "Not initialized");
blockHashes[_blockNumber] = _blockHash;
blockNumber = _blockNumber + 1;
merkleRoots[_blockHash] = _merkleRoot;
emit BlockInfoAdded(blockNumber, _blockHash, _merkleRoot);
}

/// @param _blockNumber The number of the block to get the hash for
/// @return The block hash for the given block
function getBlockHash(uint256 _blockNumber) public view returns (bytes32) {
return blockHashes[_blockNumber];
}

/// @param _blockHash The block hash of the block to get the merkle root for
/// @return The merkle root for the given block
function getMerkleRootByHash(bytes32 _blockHash) public view returns (bytes32) {
return merkleRoots[_blockHash];
}

/// @param _blockNumber The block number of the block to get the merkle root for
/// @return The merkle root for the given block
function getMerkleRootByNumber(uint256 _blockNumber) public view returns (bytes32) {
return merkleRoots[blockHashes[_blockNumber]];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IL1BlockHashList {
function initializeBlockNumber(uint256) external;
function setBlockInfo(bytes32, bytes32) external;
function getBlockHash(uint256) external view returns (bytes32);
function getMerkleRootByHash(bytes32) external view returns (bytes32);
function getMerkleRootByNumber(uint256) external view returns (bytes32);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/L1BlockHashList.sol";

contract L1BlockHashListTest is Test {
L1BlockHashList l1BlockHashList;
bytes32 randomBlockHash = bytes32(keccak256("CITREA_TEST"));
bytes32 randomMerkleRoot = bytes32(keccak256("CITREA"));
uint256 constant INITIAL_BLOCK_NUMBER = 505050;

function setUp() public {
l1BlockHashList = new L1BlockHashList();
}

function testSetBlockInfo() public {
l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER);
l1BlockHashList.setBlockInfo(randomBlockHash, randomMerkleRoot);
assertEq(l1BlockHashList.getBlockHash(INITIAL_BLOCK_NUMBER), randomBlockHash);
assertEq(l1BlockHashList.getMerkleRootByHash(randomBlockHash), randomMerkleRoot);
assertEq(l1BlockHashList.getMerkleRootByNumber(INITIAL_BLOCK_NUMBER), randomMerkleRoot);
}

function testCannotReinitalize() public {
l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER);
vm.expectRevert("Already initialized");
l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER - 10);
}

function testNonOwnerCannotSetBlockInfo() public {
l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER);
vm.startPrank(address(0x1));
vm.expectRevert("Caller is not owner");
l1BlockHashList.setBlockInfo(randomBlockHash, randomMerkleRoot);
}

function testNonOwnerCannotInitializeBlockNumber() public {
vm.startPrank(address(0x1));
vm.expectRevert("Caller is not owner");
l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER);
}

function testCannotSetInfoWithoutInitialize() public {
vm.expectRevert("Not initialized");
l1BlockHashList.setBlockInfo(randomBlockHash, randomMerkleRoot);
}

function testBlockInfoAvailableAfterManyWrites() public {
l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER);
for (uint256 i = 0; i < 100; i++) {
bytes32 blockHash = keccak256(abi.encodePacked(i));
bytes32 root = keccak256(abi.encodePacked(blockHash));
l1BlockHashList.setBlockInfo(blockHash, root);
assertEq(l1BlockHashList.getBlockHash(i + INITIAL_BLOCK_NUMBER), blockHash);
assertEq(l1BlockHashList.getMerkleRootByHash(blockHash), root);
assertEq(l1BlockHashList.getMerkleRootByNumber(i + INITIAL_BLOCK_NUMBER), root);
}

bytes32 zeroth_hash = keccak256(abi.encodePacked(uint(0)));
bytes32 zeroth_root = keccak256(abi.encodePacked(zeroth_hash));
assertEq(l1BlockHashList.getBlockHash(INITIAL_BLOCK_NUMBER), zeroth_hash);
assertEq(l1BlockHashList.getMerkleRootByHash(zeroth_hash), zeroth_root);
assertEq(l1BlockHashList.getMerkleRootByNumber(INITIAL_BLOCK_NUMBER), zeroth_root);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "../lib/Ownable.sol";
import "forge-std/Test.sol";

contract OwnableHarness is Ownable {
constructor () Ownable() { }

function privilegedFunction() public onlyOwner {
}
}

contract OwnableTest is Test {
OwnableHarness ownable;

function setUp() public {
ownable = new OwnableHarness();
}

function testOnlyOwner() public {
ownable.privilegedFunction();
address non_owner = address(0x1);
vm.startPrank(non_owner);
vm.expectRevert("Caller is not owner");
ownable.privilegedFunction();
}

function testTransferOwnership() public {
ownable.transferOwnership(address(0x1));
assertEq(ownable.pendingOwner(), address(0x1));
}

function testAcceptOwnership() public {
address new_owner = address(0x1);
ownable.transferOwnership(new_owner);
vm.startPrank(new_owner);
ownable.acceptOwnership();
assertEq(ownable.owner(), new_owner);
}

function testRenounceOwnership() public {
ownable.renounceOwnership();
assertEq(ownable.owner(), address(0));
}
}

0 comments on commit b21a9e3

Please sign in to comment.