risc0_steel/
config.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Handling different blockchain specifications.
16use alloy_primitives::{BlockNumber, BlockTimestamp, ChainId, B256};
17use anyhow::bail;
18use serde::{Deserialize, Serialize};
19use sha2::{digest::Output, Digest, Sha256};
20use std::collections::BTreeMap;
21
22/// The condition at which a fork is activated.
23#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
24pub enum ForkCondition {
25    /// The fork is activated with a certain block.
26    Block(BlockNumber),
27    /// The fork is activated with a specific timestamp.
28    Timestamp(BlockTimestamp),
29}
30
31impl ForkCondition {
32    /// Returns whether the condition has been met.
33    #[inline]
34    pub fn active(&self, block_number: BlockNumber, timestamp: u64) -> bool {
35        match self {
36            ForkCondition::Block(block) => *block <= block_number,
37            ForkCondition::Timestamp(ts) => *ts <= timestamp,
38        }
39    }
40}
41
42/// Specification of a specific chain.
43#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
44pub struct ChainSpec<S: Ord> {
45    /// Chain identifier.
46    pub chain_id: ChainId,
47    /// Map revm specification IDs to their respective activation condition.
48    pub forks: BTreeMap<S, ForkCondition>,
49}
50
51impl<S: Ord + Serialize> ChainSpec<S> {
52    /// Creates a new configuration consisting of only one specification ID.
53    ///
54    /// For example, this can be used to create a [ChainSpec] for an anvil instance:
55    /// ```rust
56    /// # use revm::primitives::hardfork::SpecId;
57    /// # use risc0_steel::config::ChainSpec;
58    /// let spec = ChainSpec::new_single(31337, SpecId::CANCUN);
59    /// ```
60    pub fn new_single(chain_id: ChainId, spec_id: S) -> Self {
61        ChainSpec {
62            chain_id,
63            forks: BTreeMap::from([(spec_id, ForkCondition::Block(0))]),
64        }
65    }
66
67    /// Returns the network chain ID.
68    #[inline]
69    pub fn chain_id(&self) -> ChainId {
70        self.chain_id
71    }
72
73    /// Returns the cryptographic digest of the entire network configuration.
74    #[inline]
75    pub fn digest(&self) -> B256 {
76        <[u8; 32]>::from(StructHash::digest::<Sha256>(self)).into()
77    }
78
79    /// Returns the spec for a given block number and timestamp or an error if not supported.
80    pub fn active_fork(&self, block_number: BlockNumber, timestamp: u64) -> anyhow::Result<&S> {
81        for (spec_id, fork) in self.forks.iter().rev() {
82            if fork.active(block_number, timestamp) {
83                return Ok(spec_id);
84            }
85        }
86        bail!("no supported fork for block {}", block_number)
87    }
88}
89
90// NOTE: We do not want to make this public, to avoid having multiple traits with the `digest`
91// function in the RISC Zero ecosystem of crates.
92/// A simple structured hasher.
93trait StructHash {
94    fn digest<D: Digest>(&self) -> Output<D>;
95}
96
97impl<S: Serialize> StructHash for (&S, &ForkCondition) {
98    /// Computes the cryptographic digest of a fork.
99    /// The hash is H(SpecID || ForkCondition::name || ForkCondition::value )
100    fn digest<D: Digest>(&self) -> Output<D> {
101        let mut hasher = D::new();
102        // for enums this is essentially equivalent to (self.0 as u32).to_le_bytes()
103        let s_bytes = bincode::serialize(&self.0).unwrap();
104        hasher.update(&s_bytes);
105        match self.1 {
106            ForkCondition::Block(n) => {
107                hasher.update(b"Block");
108                hasher.update(n.to_le_bytes());
109            }
110            ForkCondition::Timestamp(ts) => {
111                hasher.update(b"Timestamp");
112                hasher.update(ts.to_le_bytes());
113            }
114        }
115        hasher.finalize()
116    }
117}
118
119impl<S: Ord + Serialize> StructHash for ChainSpec<S> {
120    /// Computes the cryptographic digest of a chain spec.
121    ///
122    /// This is equivalent to the `tagged_struct` structural hashing routines used for RISC Zero
123    /// data structures:
124    /// `tagged_struct("ChainSpec(chain_id,forks)", forks.into_vec(), &[chain_id, chain_id >> 32])`
125    fn digest<D: Digest>(&self) -> Output<D> {
126        let tag_digest = D::digest(b"ChainSpec(chain_id,forks)");
127
128        let mut hasher = D::new();
129        hasher.update(tag_digest);
130        // down
131        self.forks
132            .iter()
133            .for_each(|fork| hasher.update(fork.digest::<D>()));
134        // data
135        hasher.update(self.chain_id.to_le_bytes());
136        // down.len() as u16
137        hasher.update(u16::try_from(self.forks.len()).unwrap().to_le_bytes());
138
139        hasher.finalize()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use revm::primitives::hardfork::SpecId;
147
148    #[test]
149    fn active_fork() {
150        let spec = ChainSpec {
151            chain_id: 1,
152            forks: BTreeMap::from([
153                (SpecId::MERGE, ForkCondition::Block(2)),
154                (SpecId::CANCUN, ForkCondition::Timestamp(60)),
155            ]),
156        };
157
158        assert!(spec.active_fork(0, 0).is_err());
159        assert_eq!(*spec.active_fork(2, 0).unwrap(), SpecId::MERGE);
160        assert_eq!(*spec.active_fork(u64::MAX, 59).unwrap(), SpecId::MERGE);
161        assert_eq!(*spec.active_fork(0, 60).unwrap(), SpecId::CANCUN);
162        assert_eq!(
163            *spec.active_fork(u64::MAX, u64::MAX).unwrap(),
164            SpecId::CANCUN
165        );
166    }
167
168    #[test]
169    fn digest() {
170        let chain_id = 0xF1E2D3C4B5A69788;
171        let forks = [
172            (SpecId::FRONTIER, ForkCondition::Block(0xF1E2D3C4B5A69788)),
173            (SpecId::PRAGUE, ForkCondition::Timestamp(0xF1E2D3C4B5A69788)),
174        ];
175        let chain_spec = ChainSpec {
176            chain_id,
177            forks: BTreeMap::from(forks.clone()),
178        };
179
180        let exp: [u8; 32] = {
181            let mut h = Sha256::new();
182            // tag digest
183            h.update(Sha256::digest(b"ChainSpec(chain_id,forks)"));
184            // fork digests
185            let fork_digest = {
186                let (spec, ForkCondition::Block(n)) = forks[0] else {
187                    unreachable!()
188                };
189                let mut h = Sha256::new();
190                h.update((spec as u32).to_le_bytes());
191                h.update(b"Block");
192                h.update(n.to_le_bytes());
193                h.finalize()
194            };
195            h.update(fork_digest);
196            let fork_digest = {
197                let (spec, ForkCondition::Timestamp(ts)) = forks[1] else {
198                    unreachable!()
199                };
200                let mut h = Sha256::new();
201                h.update((spec as u32).to_le_bytes());
202                h.update(b"Timestamp");
203                h.update(ts.to_le_bytes());
204                h.finalize()
205            };
206            h.update(fork_digest);
207            // chain_id
208            h.update((chain_id as u32).to_le_bytes());
209            h.update(((chain_id >> 32) as u32).to_le_bytes());
210            // len(forks)
211            h.update(2u16.to_le_bytes());
212
213            h.finalize().into()
214        };
215
216        assert_eq!(chain_spec.digest(), B256::from(exp));
217    }
218}