Skip to main content

signet_test_utils/specs/
host_spec.rs

1use crate::{
2    chain::Chain,
3    specs::{NotificationSpec, NotificationWithSidecars, RuBlockSpec},
4};
5use alloy::{
6    consensus::{
7        constants::GWEI_TO_WEI, BlobTransactionSidecar, Header, Receipt, ReceiptEnvelope,
8        TxEip1559, TxEip4844,
9    },
10    primitives::{Address, Bytes, Log, LogData, U256},
11    signers::Signature,
12};
13use signet_evm::ExecutionOutcome;
14use signet_extract::{Events, Extractable, Extracts};
15use signet_types::primitives::{
16    RecoveredBlock, SealedBlock, SignetHeaderV1, Transaction, TransactionSigned,
17};
18use signet_types::{
19    constants::{KnownChains, ParseChainError, SignetSystemConstants},
20    AggregateFills,
21};
22use signet_zenith::{Passage, RollupOrders, Transactor};
23use std::{
24    borrow::Borrow,
25    str::FromStr,
26    sync::atomic::{AtomicU64, Ordering},
27};
28
29/// A block spec for the host chain.
30///
31/// Typically this should be used as follows:
32///
33/// 1. Instantiate with a [`SignetSystemConstants`] object via [`Self::new`].
34/// 2. Add Signet events to the block with  [`Self::enter`],
35///    [`Self::enter_token`] [`Self::transact`], and [`Self::fill`].
36/// 3. (optional) Add a [`RuBlockSpec`] with [`Self::submit_block`].
37/// 4. Convert to a [`Chain`] with [`Self::to_chain`], or a
38///    [`NotificationSpec`] with [`Self::to_commit_notification_spec`].
39#[derive(Debug)]
40pub struct HostBlockSpec {
41    /// The system constants for the block.
42    pub constants: SignetSystemConstants,
43
44    /// The Zenith-event receipts in the block.
45    pub receipts: Vec<ReceiptEnvelope>,
46    /// The Ru block associated with this host block (if any).
47    pub ru_block: Option<RuBlockSpec>,
48    /// The sidecar associated with the Ru block (if any).
49    pub sidecar: Option<BlobTransactionSidecar>,
50    /// The receipt for the Ru block (if any).
51    pub ru_block_receipt: Option<ReceiptEnvelope>,
52    /// The block number. This will be overridden when making chains of blocks.
53    pub block_number: AtomicU64,
54
55    /// The events that were used to create this block.
56    pub events: Vec<Events>,
57}
58
59impl Clone for HostBlockSpec {
60    fn clone(&self) -> Self {
61        Self {
62            constants: self.constants.clone(),
63            receipts: self.receipts.clone(),
64            ru_block: self.ru_block.clone(),
65            sidecar: self.sidecar.clone(),
66            ru_block_receipt: self.ru_block_receipt.clone(),
67            block_number: self.block_number().into(),
68            events: self.events.clone(),
69        }
70    }
71}
72
73impl From<SignetSystemConstants> for HostBlockSpec {
74    fn from(constants: SignetSystemConstants) -> Self {
75        Self::new(constants)
76    }
77}
78
79impl HostBlockSpec {
80    /// Make a new block spec
81    pub const fn new(constants: SignetSystemConstants) -> Self {
82        Self {
83            constants,
84            receipts: vec![],
85            ru_block: None,
86            sidecar: None,
87            ru_block_receipt: None,
88            block_number: AtomicU64::new(0),
89            events: vec![],
90        }
91    }
92
93    /// Make a new block spec with Mainnet constants.
94    pub const fn mainnet() -> Self {
95        Self::new(SignetSystemConstants::mainnet())
96    }
97
98    /// Make a new block spec with Parmigiana constants.
99    pub const fn parmigiana() -> Self {
100        Self::new(SignetSystemConstants::parmigiana())
101    }
102
103    /// Make a new block spec with Gouda constants.
104    pub const fn gouda() -> Self {
105        Self::new(SignetSystemConstants::gouda())
106    }
107
108    /// Make a new block spec with Pecorino constants.
109    #[deprecated(note = "Pecorino is being deprecated in favor of Parmigiana")]
110    #[allow(deprecated)]
111    pub const fn pecorino() -> Self {
112        Self::new(SignetSystemConstants::pecorino())
113    }
114
115    /// Make a new block spec with test constants.
116    pub const fn test() -> Self {
117        Self::new(SignetSystemConstants::test())
118    }
119
120    /// Set the block number.
121    pub fn with_block_number(self, block_number: u64) -> Self {
122        self.block_number.store(block_number, Ordering::Relaxed);
123        self
124    }
125
126    /// Add an enter to the host block.
127    pub fn enter_token(mut self, recipient: Address, amount: usize, token: Address) -> Self {
128        let e = Passage::EnterToken {
129            rollupChainId: U256::from(self.constants.ru_chain_id()),
130            rollupRecipient: recipient,
131            amount: U256::from(amount),
132            token,
133        };
134        self.receipts.push(to_receipt(self.constants.host_passage(), &e));
135        self.events.push(e.into());
136
137        self
138    }
139
140    /// Add an ignored enter token to the host block
141    pub fn ingnored_enter_token(mut self, recipient: Address, amount: u64, token: Address) -> Self {
142        self.receipts.push(to_receipt(
143            self.constants.host_passage(),
144            &Passage::EnterToken {
145                rollupChainId: U256::ZERO,
146                rollupRecipient: recipient,
147                amount: U256::from(amount),
148                token,
149            },
150        ));
151        self
152    }
153
154    /// Add an iter of enter tokens to the host block
155    pub fn enter_tokens<'a, T>(mut self, enter_tokens: impl IntoIterator<Item = T>) -> Self
156    where
157        T: Borrow<(Address, usize, Address)> + 'a,
158    {
159        for item in enter_tokens {
160            let enter_token = item.borrow();
161            self = self.enter_token(enter_token.0, enter_token.1, enter_token.2);
162        }
163        self
164    }
165
166    /// Add an enter to the host block.
167    pub fn enter(mut self, recipient: Address, amount: usize) -> Self {
168        let e = Passage::Enter {
169            rollupChainId: U256::from(self.constants.ru_chain_id()),
170            rollupRecipient: recipient,
171            amount: U256::from(amount),
172        };
173        self.receipts.push(to_receipt(self.constants.host_passage(), &e));
174        self.events.push(e.into());
175
176        self
177    }
178
179    /// Add an enter to the host block that is ignored by the Ru chain because
180    /// it has a mismatched RU chain ID.
181    pub fn ignored_enter(mut self, recipient: Address, amount: u64) -> Self {
182        self.receipts.push(to_receipt(
183            self.constants.host_passage(),
184            &Passage::Enter {
185                rollupChainId: U256::ZERO,
186                rollupRecipient: recipient,
187                amount: U256::from(amount),
188            },
189        ));
190        self
191    }
192
193    /// Add several enters to the host block.
194    pub fn enters<'a, T>(mut self, enters: impl IntoIterator<Item = T>) -> Self
195    where
196        T: Borrow<(Address, usize)> + 'a,
197    {
198        for item in enters {
199            let enter = item.borrow();
200            self = self.enter(enter.0, enter.1);
201        }
202        self
203    }
204
205    /// Add a transact to the host block
206    pub fn transact(mut self, t: Transactor::Transact) -> Self {
207        self.receipts.push(to_receipt(self.constants.host_transactor(), &t));
208        self.events.push(t.into());
209        self
210    }
211
212    /// Add a simple transact to the host block.
213    pub fn simple_transact(
214        self,
215        sender: Address,
216        target: Address,
217        data: impl AsRef<[u8]>,
218        value: usize,
219    ) -> Self {
220        let transact = Transactor::Transact {
221            rollupChainId: U256::from(self.constants.ru_chain_id()),
222            sender,
223            to: target,
224            data: Bytes::copy_from_slice(data.as_ref()),
225            value: U256::from(value),
226            gas: U256::from(100_000),
227            maxFeePerGas: U256::from(GWEI_TO_WEI),
228        };
229        self.transact(transact)
230    }
231
232    /// Add a fill to the host block
233    pub fn fill(mut self, token: Address, recipient: Address, amount: u64) -> Self {
234        let e = RollupOrders::Filled {
235            outputs: vec![RollupOrders::Output {
236                chainId: self.constants.ru_chain_id() as u32,
237                token,
238                recipient,
239                amount: U256::from(amount),
240            }],
241        };
242        self.receipts.push(to_receipt(self.constants.host_orders(), &e));
243        self.events.push(e.into());
244        self
245    }
246
247    /// Add a fill to the host block that is ignored by the Ru chain because
248    /// it has a mismatched RU chain ID.
249    pub fn ignored_fill(mut self, token: Address, recipient: Address, amount: u64) -> Self {
250        self.receipts.push(to_receipt(
251            self.constants.host_orders(),
252            &RollupOrders::Filled {
253                outputs: vec![RollupOrders::Output {
254                    chainId: 0,
255                    token,
256                    recipient,
257                    amount: U256::from(amount),
258                }],
259            },
260        ));
261        self
262    }
263
264    /// Add a block submitted to the host block
265    pub fn submit_block(mut self, ru_block: RuBlockSpec) -> Self {
266        let (bs, sidecar) = ru_block.to_block_submitted();
267
268        self.ru_block = Some(ru_block);
269        self.ru_block_receipt = Some(to_receipt(self.constants.host_zenith(), &bs));
270        self.sidecar = Some(sidecar);
271        self
272    }
273
274    /// Make a blob txn
275    fn blob_txn(&self) -> Option<TransactionSigned> {
276        let sidecar = self.sidecar.as_ref()?;
277
278        Some(TransactionSigned::new_unhashed(
279            Transaction::Eip4844(TxEip4844 {
280                chain_id: self.constants.host_chain_id(),
281                nonce: 0,
282                gas_limit: 100_000,
283                max_fee_per_gas: 100_000,
284                max_priority_fee_per_gas: 10_000,
285                to: self.constants.host_zenith(),
286                value: U256::ZERO,
287                access_list: Default::default(),
288                blob_versioned_hashes: sidecar.versioned_hashes().collect(),
289                max_fee_per_blob_gas: 100_000,
290                input: Bytes::default(),
291            }),
292            Signature::test_signature(),
293        ))
294    }
295
296    /// Make dummy txns. The blob txn will always be at the end of the block
297    fn make_txns(&self) -> Vec<TransactionSigned> {
298        self.receipts
299            .iter()
300            .map(|_| {
301                Some(
302                    alloy::consensus::Signed::new_unhashed(
303                        TxEip1559::default(),
304                        Signature::test_signature(),
305                    )
306                    .into(),
307                )
308            })
309            .chain(std::iter::once(self.blob_txn()))
310            .flatten()
311            .collect()
312    }
313
314    /// Get the block number
315    pub fn block_number(&self) -> u64 {
316        self.block_number.load(Ordering::Relaxed)
317    }
318
319    /// Set the block number
320    pub fn set_block_number(&self, block_number: u64) {
321        self.block_number.store(block_number, Ordering::Relaxed);
322    }
323
324    /// Make a header
325    pub fn header(&self) -> SignetHeaderV1 {
326        let header =
327            Header { number: self.block_number(), timestamp: 1716555576, ..Default::default() };
328        SignetHeaderV1::try_from(header).expect("test header is valid V1")
329    }
330
331    /// Make a block
332    pub fn sealed_block(&self) -> SealedBlock {
333        let header = self.header();
334        SealedBlock::new(header, self.make_txns())
335    }
336
337    /// Make a block with senders.
338    pub fn recovered_block(&self) -> RecoveredBlock {
339        let block = self.sealed_block();
340        let senders = vec![Address::ZERO; block.transactions().len()];
341        block.recover_unchecked(senders)
342    }
343
344    /// Make an execution outcome
345    pub fn execution_outcome(&self) -> ExecutionOutcome {
346        let mut receipts = vec![self.receipts.clone()];
347        if let Some(receipt) = self.ru_block_receipt.clone() {
348            receipts.first_mut().unwrap().push(receipt);
349        }
350
351        ExecutionOutcome::new(Default::default(), receipts, self.block_number())
352    }
353
354    /// Make a chain
355    pub fn to_chain(&self) -> (Chain, Option<BlobTransactionSidecar>) {
356        let execution_outcome = self.execution_outcome();
357
358        let chain = Chain::from_block(self.recovered_block(), execution_outcome);
359
360        (chain, self.sidecar.clone())
361    }
362
363    /// Make a commit notification spec
364    pub fn to_commit_notification_spec(&self) -> NotificationSpec {
365        NotificationSpec { old: vec![], new: vec![self.clone()] }
366    }
367
368    /// Make a reorg notification with sidecars
369    pub fn to_notification_with_sidecar(&self) -> NotificationWithSidecars {
370        self.to_commit_notification_spec().to_exex_notification()
371    }
372
373    /// Make a revert notification spec
374    pub fn to_revert_notification_spec(&self) -> NotificationSpec {
375        NotificationSpec { old: vec![self.clone()], new: vec![] }
376    }
377
378    /// Assert that the block conforms to the spec
379    pub fn assert_conforms<C: Extractable>(&self, extracts: &Extracts<'_, C>) {
380        if let Some(ru_block) = &self.ru_block {
381            ru_block.assert_conforms(extracts);
382        }
383
384        let mut enters = extracts.enters();
385        let mut enter_tokens = extracts.enter_tokens();
386        let mut transacts = extracts.transacts();
387
388        let mut context = AggregateFills::new();
389
390        for event in self.events.iter() {
391            match event {
392                Events::Enter(e) => {
393                    assert_eq!(&(enters.next().expect("iter ended early")), e);
394                }
395                Events::EnterToken(e) => {
396                    assert_eq!(&(enter_tokens.next().expect("iter ended early")), e);
397                }
398                Events::Transact(e) => {
399                    assert_eq!(transacts.next().expect("iter ended early"), e);
400                }
401                Events::Filled(e) => {
402                    context.add_fill(self.constants.host_chain_id(), e);
403                }
404                Events::BlockSubmitted(_) => {}
405            }
406        }
407        assert!(enters.next().is_none());
408        assert!(enter_tokens.next().is_none());
409        assert!(transacts.next().is_none());
410        assert_eq!(extracts.aggregate_fills(), context);
411    }
412}
413
414impl TryFrom<KnownChains> for HostBlockSpec {
415    type Error = ParseChainError;
416
417    fn try_from(chain: KnownChains) -> Result<Self, Self::Error> {
418        match chain {
419            KnownChains::Mainnet => Ok(Self::mainnet()),
420            KnownChains::Parmigiana => Ok(Self::parmigiana()),
421            KnownChains::Gouda => Ok(Self::gouda()),
422            #[allow(deprecated)]
423            KnownChains::Pecorino => Ok(Self::pecorino()),
424            KnownChains::Test => Ok(Self::test()),
425        }
426    }
427}
428
429impl FromStr for HostBlockSpec {
430    type Err = ParseChainError;
431
432    fn from_str(s: &str) -> Result<Self, Self::Err> {
433        s.parse::<KnownChains>()?.try_into()
434    }
435}
436
437fn to_receipt<T>(address: Address, t: &T) -> ReceiptEnvelope
438where
439    for<'a> &'a T: Into<LogData>,
440{
441    let log = Log { address, data: t.into() };
442    ReceiptEnvelope::Eip1559(
443        Receipt { status: true.into(), cumulative_gas_used: 30_000, logs: vec![log] }.into(),
444    )
445}