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 4 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
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);
}
}
87 changes: 55 additions & 32 deletions steel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use ::serde::{Deserialize, Serialize};
use alloy_primitives::{uint, BlockNumber, Sealable, Sealed, B256, U256};
use alloy_sol_types::SolValue;
use config::ChainSpec;
use revm::primitives::{BlockEnv, CfgEnvWithHandlerCfg, SpecId};

pub mod beacon;
Expand Down Expand Up @@ -64,7 +65,7 @@ impl<H: EvmBlockHeader> EvmInput<H> {
/// A trait linking the block header to a commitment.
pub trait BlockHeaderCommit<H: EvmBlockHeader> {
/// Creates a verifiable [Commitment] of the `header`.
fn commit(self, header: &Sealed<H>) -> Commitment;
fn commit(self, header: &Sealed<H>, config_id: B256) -> Commitment;
}

/// A generalized input type consisting of a block-based input and a commitment wrapper.
Expand Down Expand Up @@ -93,7 +94,7 @@ impl<H: EvmBlockHeader, C: BlockHeaderCommit<H>> ComposeInput<H, C> {
/// Converts the input into a [EvmEnv] for verifiable state access in the guest.
pub fn into_env(self) -> GuestEvmEnv<H> {
let mut env = self.input.into_env();
env.commitment = self.commit.commit(&env.header);
env.commitment = self.commit.commit(&env.header, env.commitment.configID);

env
}
Expand All @@ -116,7 +117,12 @@ impl<D, H: EvmBlockHeader> EvmEnv<D, H> {
/// It uses the default configuration for the latest specification.
pub(crate) fn new(db: D, header: Sealed<H>) -> Self {
let cfg_env = CfgEnvWithHandlerCfg::new_with_spec_id(Default::default(), SpecId::LATEST);
let commitment = Commitment::from_block_header(&header);
let commitment = Commitment::new(
CommitmentVersion::Block as u16,
header.number(),
header.seal(),
ChainSpec::DEFAULT_DIGEST,
);

Self {
db: Some(db),
Expand All @@ -129,11 +135,13 @@ impl<D, H: EvmBlockHeader> EvmEnv<D, H> {
/// Sets the chain ID and specification ID from the given chain spec.
///
/// This will panic when there is no valid specification ID for the current block.
pub fn with_chain_spec(mut self, chain_spec: &config::ChainSpec) -> Self {
pub fn with_chain_spec(mut self, chain_spec: &ChainSpec) -> Self {
self.cfg_env.chain_id = chain_spec.chain_id();
self.cfg_env.handler_cfg.spec_id = chain_spec
.active_fork(self.header.number(), self.header.timestamp())
.unwrap();
self.commitment.configID = chain_spec.digest();

self
}

Expand Down Expand Up @@ -185,45 +193,63 @@ pub trait EvmBlockHeader: Sealable {
// Keep everything in the Steel library private except the commitment.
mod private {
alloy_sol_types::sol! {
/// Solidity struct representing the Steel commitment used for validation.
/// A Solidity struct representing a commitment used for validation.
///
/// This struct is used to commit to a specific claim, such as the hash of an execution block
/// or a beacon chain state. It includes a version, an identifier, the claim itself, and a
/// configuration ID to ensure the commitment is valid for the intended network.
#[derive(Default, PartialEq, Eq, Hash)]
struct Commitment {
/// Encodes both the block identifier (block number or timestamp) and the version.
uint256 blockID;
/// The block hash or beacon block root, used for validation.
bytes32 blockDigest;
/// Commitment ID.
///
/// This ID combines the version and the actual identifier of the claim, such as the block number.
uint256 id;
/// The cryptographic claim.
///
/// This is the core of the commitment, representing the data being committed to,
/// e.g., the hash of the execution block.
bytes32 claim;
/// The cryptographic digest of the network configuration.
///
/// This ID ensures that the commitment is valid only for the specific network configuration
/// it was created for.
bytes32 configID;
}
}
}

pub use private::Commitment;

/// The different versions of a [Commitment].
/// Version of a [`Commitment`].
#[repr(u16)]
enum CommitmentVersion {
#[derive(Debug, PartialEq, Eq)]
pub enum CommitmentVersion {
/// Commitment to an execution block.
Block,
/// Commitment to a beacon chain state.
Beacon,
}

impl Commitment {
/// The size if bytes of the ABI encoded commitment.
pub const ABI_ENCODED_SIZE: usize = 64;
/// The size in bytes of the ABI-encoded commitment.
pub const ABI_ENCODED_SIZE: usize = 3 * 32;

/// Constructs a new commitment.
/// Creates a new commitment.
#[inline]
pub const fn new(version: u16, id: u64, digest: B256) -> Commitment {
pub const fn new(version: u16, id: u64, claim: B256, config_id: B256) -> Commitment {
Self {
blockID: Commitment::encode_id(id, version),
blockDigest: digest,
id: Commitment::encode_id(id, version),
claim,
configID: config_id,
}
}

/// Decodes the `blockID` field into the ID and the version.
/// Decodes the `id` field into the claim ID and the commitment version.
#[inline]
pub fn decode_id(&self) -> (U256, u16) {
let decoded = self.blockID
let decoded = self.id
& uint!(0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_U256);
let version = (self.blockID.as_limbs()[3] >> 48) as u16;
let version = (self.id.as_limbs()[3] >> 48) as u16;
(decoded, version)
}

Expand All @@ -233,15 +259,6 @@ impl Commitment {
SolValue::abi_encode(self)
}

/// Creates a new block hash commitment from the given `header`.
fn from_block_header<H: EvmBlockHeader>(header: &Sealed<H>) -> Commitment {
Self::new(
CommitmentVersion::Block as u16,
header.number(),
header.seal(),
)
}

/// Encodes an ID and version into a single [U256] value.
const fn encode_id(id: u64, version: u16) -> U256 {
U256::from_limbs([id, 0, 0, (version as u64) << 48])
Expand All @@ -254,7 +271,8 @@ impl std::fmt::Debug for Commitment {
f.debug_struct("Commitment")
.field("version", &version)
.field("id", &id)
.field("digest", &self.blockDigest)
.field("claim", &self.claim)
.field("configID", &self.configID)
.finish()
}
}
Expand All @@ -268,7 +286,12 @@ mod tests {
fn size() {
let tests = vec![
Commitment::default(),
Commitment::new(u16::MAX, u64::MAX, B256::repeat_byte(0xFF)),
Commitment::new(
u16::MAX,
u64::MAX,
B256::repeat_byte(0xFF),
B256::repeat_byte(0xFF),
),
];
for test in tests {
assert_eq!(test.abi_encode().len(), Commitment::ABI_ENCODED_SIZE);
Expand All @@ -279,7 +302,7 @@ mod tests {
fn versioned_id() {
let tests = vec![(u64::MAX, u16::MAX), (u64::MAX, 0), (0, u16::MAX), (0, 0)];
for test in tests {
let commit = Commitment::new(test.1, test.0, B256::default());
let commit = Commitment::new(test.1, test.0, B256::default(), B256::default());
let (id, version) = commit.decode_id();
assert_eq!((id.to(), version), test);
}
Expand Down
Loading
Loading