Skip to content
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
104 changes: 35 additions & 69 deletions src/Zenith.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ contract Zenith is HostPassage, AccessControlDefaultAdminRules {
/// @notice Emitted when a new rollup block is successfully submitted.
/// @param sequencer - the address of the sequencer that signed the block.
/// @param header - the block header information for the block.
/// @param blobIndices - the indices of the 4844 blob hashes for the block data.
event BlockSubmitted(address indexed sequencer, BlockHeader indexed header, uint32[] blobIndices);
/// @param blockDataHash - keccak256(blockData). the Node will discard the block if the hash doens't match.
event BlockSubmitted(address indexed sequencer, BlockHeader indexed header, bytes32 blockDataHash);

/// @notice Emit the entire block data for easy visibility
event BlockData(bytes blockData);

/// @notice Initializes the Admin role.
/// @dev See `AccessControlDefaultAdminRules` for information on contract administration.
Expand All @@ -54,94 +57,58 @@ contract Zenith is HostPassage, AccessControlDefaultAdminRules {
/// @param admin - the address that will be the initial admin.
constructor(address admin) AccessControlDefaultAdminRules(1 days, admin) {}

/// @notice Submit a rollup block with block data stored in 4844 blobs.
/// @notice Submit a rollup block with block data submitted via calldata.
/// @dev Blocks are submitted by Builders, with an attestation to the block data signed by a Sequencer.
/// @param header - the header information for the rollup block.
/// @param blobIndices - the indices of the 4844 blob hashes for the block data.
/// @param v - the v component of the Sequencer's ECSDA signature over the block commitment.
/// @param r - the r component of the Sequencer's ECSDA signature over the block commitment.
/// @param s - the s component of the Sequencer's ECSDA signature over the block commitment.
/// @param blockDataHash - keccak256(blockData). the Node will discard the block if the hash doens't match.
/// @dev including blockDataHash allows the sequencer to sign over finalized block data, without needing to calldatacopy the `blockData` param.
/// @param v - the v component of the Sequencer's ECSDA signature over the block header.
/// @param r - the r component of the Sequencer's ECSDA signature over the block header.
/// @param s - the s component of the Sequencer's ECSDA signature over the block header.
/// @param blockData - block data information. could be packed blob hashes, or direct rlp-encoded transctions. blockData is ignored by the contract logic.
/// @custom:reverts BadSequence if the sequence number is not the next block for the given rollup chainId.
/// @custom:reverts BlockExpired if the confirmBy time has passed.
/// @custom:reverts BadSignature if the signer is not a permissioned sequencer,
/// OR if the signature provided commits to different block data.
/// @custom:reverts BadSignature if the signer is not a permissioned sequencer,
/// OR if the signature provided commits to a different header.
/// @custom:reverts OneRollupBlockPerHostBlock if attempting to submit a second rollup block within one host block.
/// @custom:emits BlockSubmitted if the block is successfully submitted.
function submitBlock(BlockHeader memory header, uint32[] memory blobIndices, uint8 v, bytes32 r, bytes32 s)
external
{
/// @custom:emits BlockData to expose the block calldata; as a convenience until calldata tracing is implemented in the Node.
function submitBlock(
BlockHeader memory header,
bytes32 blockDataHash,
uint8 v,
bytes32 r,
bytes32 s,
bytes calldata blockData
) external {
_submitBlock(header, blockDataHash, v, r, s);
emit BlockData(blockData);
}

function _submitBlock(BlockHeader memory header, bytes32 blockDataHash, uint8 v, bytes32 r, bytes32 s) internal {
// assert that the sequence number is valid and increment it
uint256 _nextSequence = nextSequence[header.rollupChainId]++;
if (_nextSequence != header.sequence) revert BadSequence(_nextSequence);

// assert that confirmBy time has not passed
if (block.timestamp > header.confirmBy) revert BlockExpired();

// derive block commitment from sequence number and blobhashes
bytes32 blockCommit = blockCommitment(header, blobIndices);

// derive sequencer from signature
// derive sequencer from signature over block header
bytes32 blockCommit = blockCommitment(header, blockDataHash);
address sequencer = ecrecover(blockCommit, v, r, s);

// assert that signature is valid && sequencer is permissioned
if (!hasRole(SEQUENCER_ROLE, sequencer)) revert BadSignature(sequencer);

// emit event
emit BlockSubmitted(sequencer, header, blobIndices);
}

/// @notice Construct hash of the block data that the sequencer signs.
/// @dev See `getCommit` for hashed data encoding.
/// @dev Used to easily generate a correct commit hash off-chain for the sequencer to sign.
/// @param header - the header information for the rollup block.
/// @param blobHashes - the 4844 blob hashes for the block data.
/// @param commit - the hash of the encoded block details.
function blockCommitment(BlockHeader memory header, bytes32[] memory blobHashes)
external
view
returns (bytes32 commit)
{
commit = getCommit(header, packHashes(blobHashes));
}

/// @notice Encode the array of blob hashes into a bytes string.
/// @param blobHashes - the 4844 blob hashes for the block data.
/// @return encodedHashes - the encoded blob hashes.
function packHashes(bytes32[] memory blobHashes) public pure returns (bytes memory encodedHashes) {
for (uint32 i = 0; i < blobHashes.length; i++) {
encodedHashes = abi.encodePacked(encodedHashes, blobHashes[i]);
}
}

/// @notice Construct hash of block details that the sequencer signs.
/// @dev See `getCommit` for hash data encoding.
/// @dev Used within the transaction in which the block data is submitted as a 4844 blob.
/// Relies on blob indices, which are used to read blob hashes from the transaction.
/// @param header - the header information for the rollup block.
/// @param blobIndices - the indices of the 4844 blob hashes for the block data.
/// @param commit - the hash of the encoded block details.
function blockCommitment(BlockHeader memory header, uint32[] memory blobIndices)
internal
view
returns (bytes32 commit)
{
commit = getCommit(header, getHashes(blobIndices));
}

/// @notice Encode an array of blob hashes, given their indices in the transaction.
/// @param blobIndices - the indices of the 4844 blob hashes for the block data.
/// @return encodedHashes - the encoded blob hashes.
function getHashes(uint32[] memory blobIndices) internal view returns (bytes memory encodedHashes) {
for (uint32 i = 0; i < blobIndices.length; i++) {
encodedHashes = abi.encodePacked(encodedHashes, blobhash(blobIndices[i]));
}
// emit event
emit BlockSubmitted(sequencer, header, blockDataHash);
}

/// @notice Construct hash of block details that the sequencer signs.
/// @dev Hash is keccak256(abi.encodePacked("init4.sequencer.v0", hostChainId, rollupChainId, blockSequence, rollupGasLimit, confirmBy, rewardAddress, numBlobs, encodedBlobHashes))
/// @param header - the header information for the rollup block.
/// @param encodedHashes - the encoded blob hashes.
/// @return commit - the hash of the encoded block details.
function getCommit(BlockHeader memory header, bytes memory encodedHashes) internal view returns (bytes32 commit) {
function blockCommitment(BlockHeader memory header, bytes32 blockDataHash) public view returns (bytes32 commit) {
bytes memory encoded = abi.encodePacked(
"init4.sequencer.v0",
block.chainid,
Expand All @@ -150,8 +117,7 @@ contract Zenith is HostPassage, AccessControlDefaultAdminRules {
header.gasLimit,
header.confirmBy,
header.rewardAddress,
encodedHashes.length / 32,
encodedHashes
blockDataHash
);
commit = keccak256(encoded);
}
Expand Down
56 changes: 20 additions & 36 deletions test/Zenith.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ contract ZenithTest is Test {
Zenith public target;

Zenith.BlockHeader header;
bytes32[] blobHashes;
uint32[] blobIndices;
bytes32 commit;
/// @dev blockData is ignored by the contract. it's included for the purpose of DA for the node.
bytes blockData = "0x1234567890abcdef";
bytes32 blockDataHash;

uint256 sequencerKey = 123;
uint256 notSequencerKey = 300;

event BlockSubmitted(address indexed sequencer, Zenith.BlockHeader indexed header, uint32[] blobIndices);
event BlockSubmitted(address indexed sequencer, Zenith.BlockHeader indexed header, bytes32 blockDataHash);

function setUp() public {
target = new Zenith(address(this));
Expand All @@ -28,90 +29,73 @@ contract ZenithTest is Test {
header.gasLimit = 30_000_000;
header.rewardAddress = address(this);

// set default blob info
blobIndices.push(0);
blobHashes.push(bytes32("JUNK BLOBHASH"));
// TODO: vm.blobhashes(blobHashes);
blockDataHash = keccak256(blockData);

// derive block commitment from sequence number and blobhashes
commit = target.blockCommitment(header, blobHashes);
// derive block commitment from the header
commit = target.blockCommitment(header, blockDataHash);
}

// cannot submit block with incorrect sequence number
function test_badSequence() public {
// change to incorrect sequence number
header.sequence = 1;
commit = target.blockCommitment(header, blockDataHash);

// sign block commitmenet with sequencer key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerKey, commit);

vm.expectRevert(abi.encodeWithSelector(Zenith.BadSequence.selector, 0));
target.submitBlock(header, blobIndices, v, r, s);
target.submitBlock(header, blockDataHash, v, r, s, blockData);
}

// cannot submit block with expired confirmBy time
function test_blockExpired() public {
// change to expired confirmBy time
header.confirmBy = block.timestamp - 1;
commit = target.blockCommitment(header, blockDataHash);

// sign block commitmenet with sequencer key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerKey, commit);

vm.expectRevert(abi.encodeWithSelector(Zenith.BlockExpired.selector));
target.submitBlock(header, blobIndices, v, r, s);
target.submitBlock(header, blockDataHash, v, r, s, blockData);
}

// BLOCKED by PR supporting vm.blobhashes: https://github.com/foundry-rs/foundry/pull/7001
// can submit block successfully with acceptable data & correct signature provided
function BLOCKED_test_submitBlock() public {
// can submit block successfully with acceptable header & correct signature provided
function test_submitBlock() public {
// sign block commitmenet with correct sequencer key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerKey, commit);

// should emit BlockSubmitted event
vm.expectEmit();
emit BlockSubmitted(vm.addr(sequencerKey), header, blobIndices);
target.submitBlock(header, blobIndices, v, r, s);
emit BlockSubmitted(vm.addr(sequencerKey), header, blockDataHash);
target.submitBlock(header, blockDataHash, v, r, s, blockData);

// should increment sequence number
assertEq(target.nextSequence(header.rollupChainId), header.sequence + 1);
}

// cannot submit block with invalid sequencer signer from non-permissioned key
function BLOCKED_test_notSequencer() public {
function test_notSequencer() public {
// sign block commitmenet with NOT sequencer key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(notSequencerKey, commit);

vm.expectRevert(abi.encodeWithSelector(Zenith.BadSignature.selector, vm.addr(notSequencerKey)));
target.submitBlock(header, blobIndices, v, r, s);
target.submitBlock(header, blockDataHash, v, r, s, blockData);
}

// cannot submit block with sequencer signature over different block header data
function BLOCKED_test_badSignature_header() public {
function test_badSignature() public {
// sign original block commitmenet with sequencer key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerKey, commit);

// change header data from what was signed by sequencer
header.confirmBy = block.timestamp + 15 minutes;

bytes32 newCommit = target.blockCommitment(header, blobHashes);
bytes32 newCommit = target.blockCommitment(header, blockDataHash);
address derivedSigner = ecrecover(newCommit, v, r, s);

vm.expectRevert(abi.encodeWithSelector(Zenith.BadSignature.selector, derivedSigner));
target.submitBlock(header, blobIndices, v, r, s);
target.submitBlock(header, blockDataHash, v, r, s, blockData);
}

// cannot submit block with sequencer signature over different blob hashes
function BLOCKED_test_badSignature_blobs() public {
// sign original block commitmenet with sequencer key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(sequencerKey, commit);

blobHashes[0] = bytes32("DIFFERENT BLOBHASH");
// TODO: vm.blobhashes(blobHashes);

bytes32 newCommit = target.blockCommitment(header, blobHashes);
address derivedSigner = ecrecover(newCommit, v, r, s);

vm.expectRevert(abi.encodeWithSelector(Zenith.BadSignature.selector, derivedSigner));
target.submitBlock(header, blobIndices, v, r, s);
}
}