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
7const SUBBLOCK_SIGNATURE_HASH_MAGIC_BYTE: u8 = 0x78;
9
10pub const TEMPO_SUBBLOCK_NONCE_KEY_PREFIX: u8 = 0x5b;
12
13#[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 pub struct PartialValidatorKey<15>;
22}
23
24impl PartialValidatorKey {
25 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 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 pub version: SubBlockVersion,
78 pub parent_hash: B256,
81 pub fee_recipient: Address,
83 pub transactions: Vec<TempoTxEnvelope>,
85}
86
87impl SubBlock {
88 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 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#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut, PartialEq, Eq)]
141#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
142pub struct SignedSubBlock {
143 #[deref]
145 #[deref_mut]
146 pub inner: SubBlock,
147 pub signature: Bytes,
149}
150
151impl SignedSubBlock {
152 fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
153 self.inner.rlp_encode_fields(out);
154 self.signature.encode(out);
155 }
156
157 fn rlp_encoded_fields_length(&self) -> usize {
158 self.inner.rlp_encoded_fields_length() + self.signature.length()
159 }
160
161 fn rlp_header(&self) -> alloy_rlp::Header {
162 alloy_rlp::Header {
163 list: true,
164 payload_length: self.rlp_encoded_fields_length(),
165 }
166 }
167
168 fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
169 Ok(Self {
170 inner: SubBlock::rlp_decode_fields(buf)?,
171 signature: Decodable::decode(buf)?,
172 })
173 }
174}
175
176impl Encodable for SignedSubBlock {
177 fn encode(&self, out: &mut dyn BufMut) {
178 self.rlp_header().encode(out);
179 self.rlp_encode_fields(out);
180 }
181
182 fn length(&self) -> usize {
183 self.rlp_header().length_with_payload()
184 }
185}
186
187impl Decodable for SignedSubBlock {
188 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
189 let header = alloy_rlp::Header::decode(buf)?;
190 if !header.list {
191 return Err(alloy_rlp::Error::UnexpectedString);
192 }
193
194 let remaining = buf.len();
195
196 let this = Self::rlp_decode_fields(buf)?;
197
198 if buf.len() + header.payload_length != remaining {
199 return Err(alloy_rlp::Error::UnexpectedLength);
200 }
201
202 Ok(this)
203 }
204}
205
206#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut)]
208pub struct RecoveredSubBlock {
209 #[deref]
211 #[deref_mut]
212 inner: SignedSubBlock,
213
214 senders: Vec<Address>,
216
217 validator: B256,
219}
220
221impl RecoveredSubBlock {
222 pub fn new_unchecked(inner: SignedSubBlock, senders: Vec<Address>, validator: B256) -> Self {
224 Self {
225 inner,
226 senders,
227 validator,
228 }
229 }
230
231 #[inline]
233 pub fn transactions_recovered(&self) -> impl Iterator<Item = Recovered<&TempoTxEnvelope>> + '_ {
234 self.senders
235 .iter()
236 .zip(self.inner.transactions.iter())
237 .map(|(sender, tx)| Recovered::new_unchecked(tx, *sender))
238 }
239
240 pub fn into_recovered_iter(self) -> impl Iterator<Item = Recovered<TempoTxEnvelope>> {
242 self.senders
243 .into_iter()
244 .zip(self.inner.inner.transactions)
245 .map(|(sender, tx)| Recovered::new_unchecked(tx, sender))
246 }
247
248 pub fn has_expired_transactions(&self, timestamp: u64) -> bool {
250 self.transactions.iter().any(|tx| {
251 tx.as_aa().is_some_and(|tx| {
252 tx.tx()
253 .valid_before
254 .is_some_and(|valid| valid.get() <= timestamp)
255 })
256 })
257 }
258
259 pub fn validator(&self) -> B256 {
261 self.validator
262 }
263
264 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#[derive(Debug, Clone, RlpEncodable, RlpDecodable)]
277pub struct SubBlockMetadata {
278 pub version: SubBlockVersion,
280 pub validator: B256,
282 pub fee_recipient: Address,
284 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 let with_prefix = U256::from(TEMPO_SUBBLOCK_NONCE_KEY_PREFIX) << 248;
296 assert!(has_sub_block_nonce_key_prefix(&with_prefix));
297
298 assert!(!has_sub_block_nonce_key_prefix(&U256::ZERO));
300
301 assert!(!has_sub_block_nonce_key_prefix(&U256::MAX));
303
304 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 let partial =
314 PartialValidatorKey::from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
315
316 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 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 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 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 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 let empty: [u8; 0] = [];
356 assert!(!partial.matches(empty), "Should not match empty validator");
357
358 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 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 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 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 let signed = SignedSubBlock {
399 inner: subblock.clone(),
400 signature: Bytes::from(vec![1, 2, 3, 4]),
401 };
402
403 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 assert_eq!(signed.version, SubBlockVersion::V1);
412 assert_eq!(signed.fee_recipient, subblock.fee_recipient);
413
414 let validator = B256::random();
416 let recovered = RecoveredSubBlock::new_unchecked(signed.clone(), vec![], validator);
417
418 assert_eq!(recovered.validator(), validator);
420 assert!(recovered.transactions_recovered().next().is_none()); 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 assert_eq!(SubBlockVersion::try_from(1u8), Ok(SubBlockVersion::V1));
434 assert_eq!(u8::from(SubBlockVersion::V1), 1);
435
436 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 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 let invalid_buf = [2u8];
450 assert!(SubBlockVersion::decode(&mut invalid_buf.as_slice()).is_err());
451 }
452}