Skip to content

Commit

Permalink
Add config ID
Browse files Browse the repository at this point in the history
  • Loading branch information
Wollac committed Oct 4, 2024
1 parent ee1c455 commit ed1e514
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 59 deletions.
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
4 changes: 2 additions & 2 deletions steel/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ where
let env = input.into_env().with_chain_spec(&ANVIL_CHAIN_SPEC);

let commitment = env.commitment();
assert_eq!(commitment.blockDigest, block_hash, "invalid commitment");
assert_eq!(commitment.claim, block_hash, "invalid commitment");
assert_eq!(
commitment.blockID,
commitment.id,
U256::from(block_number),
"invalid commitment"
);
Expand Down
Loading

0 comments on commit ed1e514

Please sign in to comment.