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

WEB3-166: Commit Chain Configuration to Journal #281

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 9 additions & 7 deletions contracts/src/steel/Steel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ pragma solidity ^0.8.9;
/// @notice This library provides a collection of utilities to work with Steel commitments in Solidity.
library Steel {
/// @notice Represents a commitment to a specific block in the blockchain.
/// @dev The `blockID` encodes both the block identifier (block number or timestamp) and the version.
/// @dev The `blockDigest` is the block hash or beacon block root, used for validation.
/// @dev The `id` combines the version and the actual identifier of the claim, such as the block number.
/// @dev The `claim` represents the data being committed to, e.g. the hash of the execution block.
/// @dev The `configID` is the cryptographic digest of the network configuration.
struct Commitment {
uint256 blockID;
bytes32 blockDigest;
uint256 id;
bytes32 claim;
bytes32 configID;
}

/// @notice The version of the Commitment is incorrect.
Expand All @@ -37,11 +39,11 @@ library Steel {
/// @param commitment The Commitment struct to validate.
/// @return True if the commitment's block hash matches the block hash of the block number, false otherwise.
function validateCommitment(Commitment memory commitment) internal view returns (bool) {
(uint240 blockID, uint16 version) = Encoding.decodeVersionedID(commitment.blockID);
(uint240 claimID, uint16 version) = Encoding.decodeVersionedID(commitment.id);
if (version == 0) {
return validateBlockCommitment(blockID, commitment.blockDigest);
return validateBlockCommitment(claimID, commitment.claim);
} else if (version == 1) {
return validateBeaconCommitment(blockID, commitment.blockDigest);
return validateBeaconCommitment(claimID, commitment.claim);
} else {
revert InvalidCommitmentVersion();
}
Expand Down
4 changes: 2 additions & 2 deletions examples/erc20-counter/contracts/test/Counter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ contract CounterTest is Test {

// mock the Journal
Counter.Journal memory journal = Counter.Journal({
commitment: Steel.Commitment(Encoding.encodeVersionedID(blockNumber, 0), blockHash),
commitment: Steel.Commitment(Encoding.encodeVersionedID(blockNumber, 0), blockHash, bytes32(0x0)),
tokenContract: address(token)
});
// create a mock proof
Expand All @@ -77,7 +77,7 @@ contract CounterTest is Test {

// mock the Journal
Counter.Journal memory journal = Counter.Journal({
commitment: Steel.Commitment(Encoding.encodeVersionedID(beaconTimestamp, 1), beaconRoot),
commitment: Steel.Commitment(Encoding.encodeVersionedID(beaconTimestamp, 1), beaconRoot, bytes32(0x0)),
tokenContract: address(token)
});
// create a mock proof
Expand Down
1 change: 1 addition & 0 deletions steel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.

### 🚨 Breaking Changes

- The Solidity `Commitment` now also contains a hash of the chain specification including chain ID, and fork configuration.
- Introduce the `ComposeInput` as a generalized type to represent different commitments. The `BeaconInput` is now a `ComposeInput`. This changes the binary input data, but does not require any code changes.

## [0.13.0](https://github.com/risc0/risc0-ethereum/releases/tag/steel-v0.13.0) - 2024-09-10
Expand Down
9 changes: 7 additions & 2 deletions steel/src/beacon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ impl BeaconCommit {

impl<H: EvmBlockHeader> BlockHeaderCommit<H> for BeaconCommit {
#[inline]
fn commit(self, header: &Sealed<H>) -> Commitment {
fn commit(self, header: &Sealed<H>, config_id: B256) -> Commitment {
let (timestamp, beacon_root) = self.into_commit(header.seal());
Commitment::new(CommitmentVersion::Beacon as u16, timestamp, beacon_root)
Commitment::new(
CommitmentVersion::Beacon as u16,
timestamp,
beacon_root,
config_id,
)
}
}

Expand Down
91 changes: 83 additions & 8 deletions steel/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,19 @@
//! Handling different blockchain specifications.
use std::collections::BTreeMap;

use alloy_primitives::{BlockNumber, ChainId};
use alloy_primitives::{b256, BlockNumber, BlockTimestamp, ChainId, B256};
use anyhow::bail;
use revm::primitives::SpecId;
use serde::{Deserialize, Serialize};
use sha2::{digest::Output, Digest, Sha256};

/// The condition at which a fork is activated.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ForkCondition {
/// The fork is activated with a certain block.
Block(BlockNumber),
/// The fork is activated with a specific timestamp.
Timestamp(u64),
/// The fork is never activated
#[default]
TBD,
Timestamp(BlockTimestamp),
}

impl ForkCondition {
Expand All @@ -39,7 +37,6 @@ impl ForkCondition {
match self {
ForkCondition::Block(block) => *block <= block_number,
ForkCondition::Timestamp(ts) => *ts <= timestamp,
ForkCondition::TBD => false,
}
}
}
Expand All @@ -53,7 +50,19 @@ pub struct ChainSpec {
pub forks: BTreeMap<SpecId, ForkCondition>,
}

impl Default for ChainSpec {
/// Defaults to Ethereum Chain ID using the latest specification.
#[inline]
fn default() -> Self {
Self::new_single(1, SpecId::LATEST)
}
}

impl ChainSpec {
/// Digest of the default configuration, i.e. `ChainSpec::default().digest()`.
pub const DEFAULT_DIGEST: B256 =
b256!("0e0fe3926625a8ffdd4123ad55bf3a419918885daa2e506df18c0e3d6b6c5009");

/// Creates a new configuration consisting of only one specification ID.
///
/// For example, this can be used to create a [ChainSpec] for an anvil instance:
Expand All @@ -75,6 +84,12 @@ impl ChainSpec {
self.chain_id
}

/// Returns the cryptographic digest of the entire network configuration.
#[inline]
pub fn digest(&self) -> B256 {
<[u8; 32]>::from(StructHash::digest::<Sha256>(self)).into()
}

/// Returns the [SpecId] for a given block number and timestamp or an error if not supported.
pub fn active_fork(&self, block_number: BlockNumber, timestamp: u64) -> anyhow::Result<SpecId> {
for (spec_id, fork) in self.forks.iter().rev() {
Expand All @@ -86,6 +101,52 @@ impl ChainSpec {
}
}

/// A simple structured hasher.
trait StructHash {
fn digest<D: Digest>(&self) -> Output<D>;
}

impl StructHash for (&SpecId, &ForkCondition) {
/// Computes the cryptographic digest of a fork.
/// The hash is H(SpecID || ForkCondition::name || ForkCondition::value )
fn digest<D: Digest>(&self) -> Output<D> {
let mut hasher = D::new();
hasher.update([*self.0 as u8]);
match self.1 {
ForkCondition::Block(n) => {
hasher.update(b"Block");
hasher.update(n.to_le_bytes());
}
ForkCondition::Timestamp(ts) => {
hasher.update(b"Timestamp");
hasher.update(ts.to_le_bytes());
}
}
hasher.finalize()
}
}

impl StructHash for ChainSpec {
/// Computes the cryptographic digest of a chain spec.
///
/// This is equivalent to the `tagged_struct` structural hashing routines used for RISC Zero
/// data structures:
/// `tagged_struct("ChainSpec(chain_id,forks)", forks.into_vec(), &[chain_id, chain_id >> 32])`
fn digest<D: Digest>(&self) -> Output<D> {
let tag_digest = D::digest(b"ChainSpec(chain_id,forks)");

let mut hasher = D::new();
hasher.update(tag_digest);
self.forks
.iter()
.for_each(|f| hasher.update(f.digest::<D>()));
hasher.update(self.chain_id.to_le_bytes());
hasher.update(u16::try_from(self.forks.len()).unwrap().to_le_bytes());

hasher.finalize()
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -97,7 +158,6 @@ mod tests {
forks: BTreeMap::from([
(SpecId::MERGE, ForkCondition::Block(2)),
(SpecId::CANCUN, ForkCondition::Timestamp(60)),
(SpecId::PRAGUE, ForkCondition::TBD),
]),
};

Expand All @@ -110,4 +170,19 @@ mod tests {
SpecId::CANCUN
);
}

#[test]
fn default_digest() {
let exp: [u8; 32] = {
let mut h = Sha256::new();
h.update(Sha256::digest(b"ChainSpec(chain_id,forks)"));
h.update((&SpecId::LATEST, &ForkCondition::Block(0)).digest::<Sha256>());
h.update((1u64 as u32).to_le_bytes());
h.update(((1u64 >> 32) as u32).to_le_bytes());
h.update(1u16.to_le_bytes());
h.finalize().into()
};
assert_eq!(ChainSpec::DEFAULT_DIGEST.0, exp);
assert_eq!(ChainSpec::default().digest().0, exp);
}
}
Loading
Loading