Skip to main content

tempo_primitives/
subblock.rs

1use crate::TempoTxEnvelope;
2use alloc::vec::Vec;
3use alloy_consensus::transaction::Recovered;
4use alloy_primitives::{Address, B256, Bytes, U256, keccak256, wrap_fixed_bytes};
5use alloy_rlp::{BufMut, Decodable, Encodable, RlpDecodable, RlpEncodable};
6
7/// Magic byte for the subblock signature hash.
8const SUBBLOCK_SIGNATURE_HASH_MAGIC_BYTE: u8 = 0x78;
9
10/// Nonce key prefix marking a subblock transaction.
11pub const TEMPO_SUBBLOCK_NONCE_KEY_PREFIX: u8 = 0x5b;
12
13/// Returns true if the given nonce key has the [`TEMPO_SUBBLOCK_NONCE_KEY_PREFIX`].
14#[inline]
15pub fn has_sub_block_nonce_key_prefix(nonce_key: &U256) -> bool {
16    nonce_key.byte(31) == TEMPO_SUBBLOCK_NONCE_KEY_PREFIX
17}
18
19wrap_fixed_bytes! {
20    /// Partial validator public key encoded inside the nonce key.
21    pub struct PartialValidatorKey<15>;
22}
23
24impl PartialValidatorKey {
25    /// Returns whether this partial public key matches the given validator public key.
26    pub fn matches(&self, validator: impl AsRef<[u8]>) -> bool {
27        validator.as_ref().starts_with(self.as_slice())
28    }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
33pub enum SubBlockVersion {
34    /// Subblock version 1.
35    V1 = 1,
36}
37
38impl From<SubBlockVersion> for u8 {
39    fn from(value: SubBlockVersion) -> Self {
40        value as Self
41    }
42}
43
44impl TryFrom<u8> for SubBlockVersion {
45    type Error = u8;
46
47    fn try_from(value: u8) -> Result<Self, Self::Error> {
48        match value {
49            1 => Ok(Self::V1),
50            _ => Err(value),
51        }
52    }
53}
54
55impl Encodable for SubBlockVersion {
56    fn encode(&self, out: &mut dyn BufMut) {
57        u8::from(*self).encode(out);
58    }
59
60    fn length(&self) -> usize {
61        u8::from(*self).length()
62    }
63}
64
65impl Decodable for SubBlockVersion {
66    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
67        u8::decode(buf)?
68            .try_into()
69            .map_err(|_| alloy_rlp::Error::Custom("invalid subblock version"))
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
75pub struct SubBlock {
76    /// Version of the subblock.
77    pub version: SubBlockVersion,
78    /// Hash of the parent block. This subblock can only be included as
79    /// part of the block building on top of the specified parent.
80    pub parent_hash: B256,
81    /// Recipient of the fees for the subblock.
82    pub fee_recipient: Address,
83    /// Transactions included in the subblock.
84    pub transactions: Vec<TempoTxEnvelope>,
85}
86
87impl SubBlock {
88    /// Returns the hash for the signature.
89    pub fn signature_hash(&self) -> B256 {
90        let mut buf = Vec::with_capacity(self.length() + 1);
91        buf.put_u8(SUBBLOCK_SIGNATURE_HASH_MAGIC_BYTE);
92        self.encode(&mut buf);
93        keccak256(&buf)
94    }
95
96    fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
97        self.version.encode(out);
98        self.parent_hash.encode(out);
99        self.fee_recipient.encode(out);
100        self.transactions.encode(out);
101    }
102
103    fn rlp_encoded_fields_length(&self) -> usize {
104        self.version.length()
105            + self.parent_hash.length()
106            + self.fee_recipient.length()
107            + self.transactions.length()
108    }
109
110    fn rlp_header(&self) -> alloy_rlp::Header {
111        alloy_rlp::Header {
112            list: true,
113            payload_length: self.rlp_encoded_fields_length(),
114        }
115    }
116
117    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
118        Ok(Self {
119            version: Decodable::decode(buf)?,
120            parent_hash: Decodable::decode(buf)?,
121            fee_recipient: Decodable::decode(buf)?,
122            transactions: Decodable::decode(buf)?,
123        })
124    }
125
126    /// Returns the total length of the transactions in the subblock.
127    pub fn total_tx_size(&self) -> usize {
128        self.transactions.iter().map(|tx| tx.length()).sum()
129    }
130}
131
132impl Encodable for SubBlock {
133    fn encode(&self, out: &mut dyn BufMut) {
134        self.rlp_header().encode(out);
135        self.rlp_encode_fields(out);
136    }
137}
138
139/// A subblock with a signature.
140#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut, PartialEq, Eq)]
141#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
142#[cfg_attr(test, reth_codecs::add_arbitrary_tests(rlp))]
143pub struct SignedSubBlock {
144    /// The subblock.
145    #[deref]
146    #[deref_mut]
147    pub inner: SubBlock,
148    /// The signature of the subblock.
149    pub signature: Bytes,
150}
151
152impl SignedSubBlock {
153    /// Attempts to recover the senders and convert the subblock into a [`RecoveredSubBlock`].
154    ///
155    /// Note that the validator is assumed to be pre-validated to match the submitted signature.
156    #[cfg(feature = "reth")]
157    pub fn try_into_recovered(
158        self,
159        validator: B256,
160    ) -> Result<RecoveredSubBlock, alloy_consensus::crypto::RecoveryError> {
161        let senders =
162            reth_primitives_traits::transaction::recover::recover_signers(&self.transactions)?;
163
164        Ok(RecoveredSubBlock {
165            inner: self,
166            senders,
167            validator,
168        })
169    }
170
171    fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
172        self.inner.rlp_encode_fields(out);
173        self.signature.encode(out);
174    }
175
176    fn rlp_encoded_fields_length(&self) -> usize {
177        self.inner.rlp_encoded_fields_length() + self.signature.length()
178    }
179
180    fn rlp_header(&self) -> alloy_rlp::Header {
181        alloy_rlp::Header {
182            list: true,
183            payload_length: self.rlp_encoded_fields_length(),
184        }
185    }
186
187    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
188        Ok(Self {
189            inner: SubBlock::rlp_decode_fields(buf)?,
190            signature: Decodable::decode(buf)?,
191        })
192    }
193}
194
195impl Encodable for SignedSubBlock {
196    fn encode(&self, out: &mut dyn BufMut) {
197        self.rlp_header().encode(out);
198        self.rlp_encode_fields(out);
199    }
200
201    fn length(&self) -> usize {
202        self.rlp_header().length_with_payload()
203    }
204}
205
206impl Decodable for SignedSubBlock {
207    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
208        let header = alloy_rlp::Header::decode(buf)?;
209        if !header.list {
210            return Err(alloy_rlp::Error::UnexpectedString);
211        }
212
213        let remaining = buf.len();
214
215        let this = Self::rlp_decode_fields(buf)?;
216
217        if buf.len() + header.payload_length != remaining {
218            return Err(alloy_rlp::Error::UnexpectedLength);
219        }
220
221        Ok(this)
222    }
223}
224
225/// A subblock with recovered senders.
226#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut)]
227pub struct RecoveredSubBlock {
228    /// Inner subblock.
229    #[deref]
230    #[deref_mut]
231    inner: SignedSubBlock,
232
233    /// The senders of the transactions.
234    senders: Vec<Address>,
235
236    /// The validator that submitted the subblock.
237    validator: B256,
238}
239
240impl RecoveredSubBlock {
241    /// Creates a new [`RecoveredSubBlock`] without validating the signatures.
242    pub fn new_unchecked(inner: SignedSubBlock, senders: Vec<Address>, validator: B256) -> Self {
243        Self {
244            inner,
245            senders,
246            validator,
247        }
248    }
249
250    /// Returns an iterator over `Recovered<&Transaction>`
251    #[inline]
252    pub fn transactions_recovered(&self) -> impl Iterator<Item = Recovered<&TempoTxEnvelope>> + '_ {
253        self.senders
254            .iter()
255            .zip(self.inner.transactions.iter())
256            .map(|(sender, tx)| Recovered::new_unchecked(tx, *sender))
257    }
258
259    /// Returns the validator that submitted the subblock.
260    pub fn validator(&self) -> B256 {
261        self.validator
262    }
263
264    /// Returns the metadata for the subblock.
265    pub fn metadata(&self) -> SubBlockMetadata {
266        SubBlockMetadata {
267            validator: self.validator,
268            fee_recipient: self.fee_recipient,
269            version: self.version,
270            signature: self.signature.clone(),
271        }
272    }
273}
274
275/// Metadata for an included subblock.
276#[derive(Debug, Clone, RlpEncodable, RlpDecodable)]
277pub struct SubBlockMetadata {
278    /// Version of the subblock.
279    pub version: SubBlockVersion,
280    /// Validator that submitted the subblock.
281    pub validator: B256,
282    /// Recipient of the fees for the subblock.
283    pub fee_recipient: Address,
284    /// Signature of the subblock.
285    pub signature: Bytes,
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_has_sub_block_nonce_key_prefix() {
294        // Valid prefix in MSB (byte 31)
295        let with_prefix = U256::from(TEMPO_SUBBLOCK_NONCE_KEY_PREFIX) << 248;
296        assert!(has_sub_block_nonce_key_prefix(&with_prefix));
297
298        // Zero has no prefix
299        assert!(!has_sub_block_nonce_key_prefix(&U256::ZERO));
300
301        // Max value has 0xff in MSB, not 0x5b
302        assert!(!has_sub_block_nonce_key_prefix(&U256::MAX));
303
304        // Prefix in LSB (byte 0), not MSB
305        assert!(!has_sub_block_nonce_key_prefix(&U256::from(
306            TEMPO_SUBBLOCK_NONCE_KEY_PREFIX
307        )));
308    }
309
310    #[test]
311    fn test_partial_validator_key_matches() {
312        // Create a 15-byte partial key
313        let partial =
314            PartialValidatorKey::from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
315
316        // Full key that starts with the partial
317        let matching_key = [
318            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
319        ];
320        assert!(
321            partial.matches(matching_key),
322            "Should match when validator starts with partial"
323        );
324
325        // Exactly the partial key length
326        let exact_match: [u8; 15] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
327        assert!(partial.matches(exact_match), "Should match exact length");
328
329        // Different first byte
330        let non_matching = [
331            0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
332        ];
333        assert!(
334            !partial.matches(non_matching),
335            "Should not match with different first byte"
336        );
337
338        // Different last byte of partial
339        let partial_mismatch = [
340            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 99, 16, 17, 18,
341        ];
342        assert!(
343            !partial.matches(partial_mismatch),
344            "Should not match with different byte in partial range"
345        );
346
347        // Shorter than partial (should not match)
348        let too_short: [u8; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
349        assert!(
350            !partial.matches(too_short),
351            "Should not match if validator is shorter than partial"
352        );
353
354        // Empty key
355        let empty: [u8; 0] = [];
356        assert!(!partial.matches(empty), "Should not match empty validator");
357
358        // Zero partial key matches any key starting with zeros
359        let zero_partial = PartialValidatorKey::ZERO;
360        let zeros = [0u8; 20];
361        assert!(
362            zero_partial.matches(zeros),
363            "Zero partial should match zeros"
364        );
365    }
366
367    #[test]
368    fn test_subblock_signature_and_recovery() {
369        let subblock = SubBlock {
370            version: SubBlockVersion::V1,
371            parent_hash: B256::random(),
372            fee_recipient: Address::random(),
373            transactions: vec![],
374        };
375
376        // Hash should be deterministic
377        let hash1 = subblock.signature_hash();
378        let hash2 = subblock.signature_hash();
379        assert_eq!(hash1, hash2, "signature_hash should be deterministic");
380        assert_ne!(hash1, B256::ZERO);
381
382        // Different subblocks produce different hashes
383        let subblock2 = SubBlock {
384            version: SubBlockVersion::V1,
385            parent_hash: B256::random(),
386            fee_recipient: Address::random(),
387            transactions: vec![],
388        };
389        assert_ne!(subblock.signature_hash(), subblock2.signature_hash());
390
391        // Verify hash includes magic byte prefix
392        let mut expected_buf = Vec::with_capacity(subblock.length() + 1);
393        expected_buf.put_u8(SUBBLOCK_SIGNATURE_HASH_MAGIC_BYTE);
394        subblock.encode(&mut expected_buf);
395        assert_eq!(hash1, keccak256(&expected_buf));
396
397        // SignedSubBlock
398        let signed = SignedSubBlock {
399            inner: subblock.clone(),
400            signature: Bytes::from(vec![1, 2, 3, 4]),
401        };
402
403        // SignedSubBlock RLP roundtrip
404        let mut buf = Vec::new();
405        signed.encode(&mut buf);
406        assert_eq!(buf.len(), signed.length());
407        let decoded = SignedSubBlock::decode(&mut buf.as_slice()).unwrap();
408        assert_eq!(decoded, signed);
409
410        // Deref to SubBlock works
411        assert_eq!(signed.version, SubBlockVersion::V1);
412        assert_eq!(signed.fee_recipient, subblock.fee_recipient);
413
414        // RecoveredSubBlock
415        let validator = B256::random();
416        let recovered = RecoveredSubBlock::new_unchecked(signed.clone(), vec![], validator);
417
418        // Accessors
419        assert_eq!(recovered.validator(), validator);
420        assert!(recovered.transactions_recovered().next().is_none()); // empty
421
422        // metadata()
423        let meta = recovered.metadata();
424        assert_eq!(meta.version, SubBlockVersion::V1);
425        assert_eq!(meta.validator, validator);
426        assert_eq!(meta.fee_recipient, subblock.fee_recipient);
427        assert_eq!(meta.signature, Bytes::from(vec![1, 2, 3, 4]));
428    }
429
430    #[test]
431    fn test_subblock_version_conversion() {
432        // Valid V1
433        assert_eq!(SubBlockVersion::try_from(1u8), Ok(SubBlockVersion::V1));
434        assert_eq!(u8::from(SubBlockVersion::V1), 1);
435
436        // Invalid versions
437        assert_eq!(SubBlockVersion::try_from(0u8), Err(0));
438        assert_eq!(SubBlockVersion::try_from(2u8), Err(2));
439        assert_eq!(SubBlockVersion::try_from(255u8), Err(255));
440
441        // RLP encode/decode
442        let mut buf = Vec::new();
443        SubBlockVersion::V1.encode(&mut buf);
444        assert_eq!(buf.len(), SubBlockVersion::V1.length());
445        let decoded = SubBlockVersion::decode(&mut buf.as_slice()).unwrap();
446        assert_eq!(decoded, SubBlockVersion::V1);
447
448        // Invalid version decode
449        let invalid_buf = [2u8];
450        assert!(SubBlockVersion::decode(&mut invalid_buf.as_slice()).is_err());
451    }
452}