signet_zenith/
block.rs

1use crate::Zenith::BlockHeader as ZenithHeader;
2use alloy::{
3    consensus::TxEnvelope,
4    eips::eip2718::{Decodable2718, Encodable2718},
5    primitives::{keccak256, Address, B256},
6    rlp::Decodable,
7};
8use std::{marker::PhantomData, sync::OnceLock};
9
10/// Zenith processes normal Ethereum txns.
11pub type ZenithTransaction = TxEnvelope;
12
13/// Encode/Decode trait for inner tx type
14pub trait Coder {
15    /// The inner tx type.
16    type Tx: std::fmt::Debug + Clone + PartialEq + Eq;
17
18    /// Encode the tx.
19    fn encode(t: &Self::Tx) -> Vec<u8>;
20
21    /// Decode the tx.
22    fn decode(buf: &mut &[u8]) -> Option<Self::Tx>
23    where
24        Self: Sized;
25}
26
27/// Coder for [`encode_txns`] and [`decode_txns`] that operates on
28/// [`TxEnvelope`].
29#[derive(Copy, Clone, Debug)]
30pub struct Alloy2718Coder;
31
32impl Coder for Alloy2718Coder {
33    type Tx = ZenithTransaction;
34
35    fn encode(t: &ZenithTransaction) -> Vec<u8> {
36        t.encoded_2718()
37    }
38
39    fn decode(buf: &mut &[u8]) -> Option<ZenithTransaction>
40    where
41        Self: Sized,
42    {
43        ZenithTransaction::decode_2718(buf).ok().filter(|tx| !tx.is_eip4844())
44    }
45}
46
47/// A Zenith block is just a list of transactions.
48#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49#[serde(bound = "C::Tx: serde::Serialize + serde::de::DeserializeOwned")]
50pub struct ZenithBlock<C: Coder = Alloy2718Coder> {
51    /// The zenith block header, which may be extracted from a
52    /// [`crate::Zenith::BlockSubmitted`] event.
53    header: ZenithHeader,
54    /// The transactions in the block, which are extracted from the calldata or
55    /// blob data.
56    transactions: Vec<<C as Coder>::Tx>,
57
58    // The encoded transactions, memoized to avoid re-encoding.
59    #[serde(skip)]
60    encoded: OnceLock<Vec<u8>>,
61
62    /// The hash of the encoded transactions.
63    #[serde(with = "oncelock_as_opt")]
64    block_data_hash: OnceLock<B256>,
65
66    /// The coder
67    _pd: std::marker::PhantomData<C>,
68}
69
70mod oncelock_as_opt {
71    use serde::{Deserialize, Serialize};
72    use std::sync::OnceLock;
73
74    /// Serialize a `OnceLock` as an optional value.
75    pub(crate) fn serialize<S, T>(value: &OnceLock<T>, serializer: S) -> Result<S::Ok, S::Error>
76    where
77        S: serde::Serializer,
78        T: serde::Serialize,
79    {
80        value.get().serialize(serializer)
81    }
82
83    /// Deserialize a `OnceLock` from an optional value.
84    pub(crate) fn deserialize<'de, D, T>(deserializer: D) -> Result<OnceLock<T>, D::Error>
85    where
86        D: serde::Deserializer<'de>,
87        T: serde::Deserialize<'de>,
88    {
89        if let Some(item) = Option::<T>::deserialize(deserializer)? {
90            Ok(OnceLock::from(item))
91        } else {
92            Ok(OnceLock::new())
93        }
94    }
95}
96
97impl<C> ZenithBlock<C>
98where
99    C: Coder,
100{
101    /// Create a new zenith block.
102    pub const fn new(header: ZenithHeader, transactions: Vec<<C as Coder>::Tx>) -> Self {
103        ZenithBlock {
104            header,
105            transactions,
106            encoded: OnceLock::new(),
107            block_data_hash: OnceLock::new(),
108            _pd: PhantomData,
109        }
110    }
111
112    /// Decode tx data in the block.
113    ///
114    /// This will perform the following steps:
115    /// - Attempt to decode the data as an RLP list
116    ///     - On failure, discard all data, returning an empty tx list
117    /// - Attempt to decode each item in the list as a transaction
118    ///     - On failure, discard the item
119    /// - Return a list of succesfully decoded transactions
120    pub fn from_header_and_data(header: ZenithHeader, buf: impl AsRef<[u8]>) -> Self {
121        let b = buf.as_ref();
122        let transactions = decode_txns::<C>(b);
123        let h = keccak256(b);
124        ZenithBlock {
125            header,
126            transactions,
127            encoded: b.to_owned().into(),
128            block_data_hash: h.into(),
129            _pd: PhantomData,
130        }
131    }
132
133    /// Break the block into its parts.
134    pub fn into_parts(self) -> (ZenithHeader, Vec<C::Tx>) {
135        (self.header, self.transactions)
136    }
137
138    /// Encode the transactions in the block.
139    pub fn encoded_txns(&self) -> &[u8] {
140        self.seal();
141        self.encoded.get().unwrap().as_slice()
142    }
143
144    /// The hash of the encoded transactions.
145    pub fn block_data_hash(&self) -> B256 {
146        self.seal();
147        *self.block_data_hash.get().unwrap()
148    }
149
150    /// Push a transaction into the block.
151    pub fn push_transaction(&mut self, tx: C::Tx) {
152        self.unseal();
153        self.transactions.push(tx);
154    }
155
156    /// Access to the transactions.
157    #[allow(clippy::missing_const_for_fn)] // false positive
158    pub fn transactions(&self) -> &[C::Tx] {
159        &self.transactions
160    }
161
162    /// Mutable access to the transactions.
163    pub fn transactions_mut(&mut self) -> &mut Vec<C::Tx> {
164        self.unseal();
165        &mut self.transactions
166    }
167
168    /// Iterate over the transactions.
169    pub fn transactions_iter(&self) -> std::slice::Iter<'_, C::Tx> {
170        self.transactions.iter()
171    }
172
173    /// Iterate over mut transactions.
174    pub fn transactions_iter_mut(&mut self) -> std::slice::IterMut<'_, C::Tx> {
175        self.unseal();
176        self.transactions.iter_mut()
177    }
178
179    /// Access to the header.
180    pub const fn header(&self) -> &ZenithHeader {
181        &self.header
182    }
183
184    /// Mutable access to the header.
185    pub const fn header_mut(&mut self) -> &mut ZenithHeader {
186        &mut self.header
187    }
188
189    fn seal(&self) {
190        let encoded = self.encoded.get_or_init(|| encode_txns::<C>(&self.transactions));
191        self.block_data_hash.get_or_init(|| keccak256(encoded));
192    }
193
194    fn unseal(&mut self) {
195        self.encoded.take();
196        self.block_data_hash.take();
197    }
198
199    /// Get the chain ID of the block (discarding high bytes).
200    pub const fn chain_id(&self) -> u64 {
201        self.header.chain_id()
202    }
203
204    /// Gets the block height according to the header
205    pub const fn block_height(&self) -> u64 {
206        self.header.host_block_number()
207    }
208
209    /// Get the gas limit of the block (discarding high bytes).
210    pub const fn gas_limit(&self) -> u64 {
211        self.header.gas_limit()
212    }
213
214    /// Get the reward address of the block.
215    pub const fn reward_address(&self) -> Address {
216        self.header.reward_address()
217    }
218}
219
220/// Decode transactions.
221///
222/// A transaction is an RLP-encoded list of EIP-2718-encoded transaction
223/// envelopes.
224///
225/// The function is generic over the coder type, which is used to decode the
226/// transactions. This allows for different transaction types to be decoded
227/// using different coders.
228pub fn decode_txns<C>(block_data: impl AsRef<[u8]>) -> Vec<C::Tx>
229where
230    C: Coder,
231{
232    let mut bd = block_data.as_ref();
233
234    Vec::<Vec<u8>>::decode(&mut bd)
235        .map(|rlp| rlp.into_iter().flat_map(|buf| C::decode(&mut buf.as_slice())).collect())
236        .ok()
237        .unwrap_or_default()
238}
239
240/// Encode a set of transactions into a single RLP-encoded buffer.
241///
242/// The function is generic over the coder type, which is used to encode the
243/// transactions. This allows for different transaction types to be encoded
244/// using different encodings.
245pub fn encode_txns<'a, C>(transactions: impl IntoIterator<Item = &'a C::Tx>) -> Vec<u8>
246where
247    C: Coder,
248    C::Tx: 'a,
249{
250    let encoded_txns = transactions.into_iter().map(|tx| C::encode(tx)).collect::<Vec<Vec<u8>>>();
251
252    let mut buf = Vec::new();
253    alloy::rlp::Encodable::encode(&encoded_txns, &mut buf);
254    buf
255}
256
257#[cfg(test)]
258mod test {
259    use alloy::consensus::{Signed, TxEip1559};
260    use alloy::primitives::{b256, bytes, Address, Signature, U256};
261
262    use super::*;
263
264    #[test]
265    fn encode_decode() {
266        let sig = Signature::from_scalars_and_parity(
267            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
268            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
269            false,
270        );
271
272        let tx = ZenithTransaction::Eip1559(Signed::new_unchecked(
273            TxEip1559 {
274                chain_id: 1,
275                nonce: 2,
276                gas_limit: 3,
277                max_fee_per_gas: 4,
278                max_priority_fee_per_gas: 5,
279                to: Address::repeat_byte(6).into(),
280                value: U256::from(7),
281                access_list: Default::default(),
282                input: bytes!("08090a0b0c0d0e0f"),
283            },
284            sig,
285            b256!("87fdda4563f2f98ac9c3f076bca48a59309df94f13fb8abf8471b3b8b51a2816"),
286        ));
287
288        let mut txs = vec![tx.clone()];
289        let encoded = encode_txns::<Alloy2718Coder>(&txs);
290        let decoded = decode_txns::<Alloy2718Coder>(encoded);
291
292        assert_eq!(txs, decoded);
293
294        txs.push(tx.clone());
295        let encoded = encode_txns::<Alloy2718Coder>(&txs);
296        let decoded = decode_txns::<Alloy2718Coder>(encoded);
297
298        assert_eq!(txs, decoded);
299    }
300
301    #[test]
302    fn graceful_junk() {
303        let junk = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
304        let decoded = decode_txns::<Alloy2718Coder>(&junk);
305        assert!(decoded.is_empty());
306    }
307}