amadeus_node/consensus/doms/
entry.rs

1use crate::Context;
2use crate::consensus::doms::tx::EntryTx;
3use crate::consensus::fabric;
4use crate::node::protocol;
5use crate::node::protocol::{Handle, Typename};
6use crate::utils::bls12_381;
7use crate::utils::misc::{bin_to_bitvec, bitvec_to_bin, get_unix_millis_now};
8use crate::utils::{Hash, PublicKey, Signature};
9use crate::utils::{archiver, blake3};
10/// Entry is a consensus block in Amadeus
11use amadeus_utils::constants::DST_VRF;
12use amadeus_utils::vecpak::{Term, VecpakExt, decode, encode};
13use bitvec::prelude::*;
14//use eetf::{Atom, Binary, Map, Term};
15use amadeus_utils::vecpak;
16use std::fmt;
17use std::net::Ipv4Addr;
18
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
21    #[error(transparent)]
22    Io(#[from] std::io::Error),
23    #[error(transparent)]
24    EtfDecode(#[from] eetf::DecodeError),
25    #[error(transparent)]
26    EtfEncode(#[from] eetf::EncodeError),
27    #[error(transparent)]
28    BinDecode(#[from] bincode::error::DecodeError),
29    #[error(transparent)]
30    BinEncode(#[from] bincode::error::EncodeError),
31    #[error("bad format: {0}")]
32    BadFormat(&'static str),
33    #[error(transparent)]
34    Tx(#[from] super::tx::Error),
35    #[error(transparent)]
36    Bls(#[from] bls12_381::Error),
37    #[error(transparent)]
38    Fabric(#[from] fabric::Error),
39    #[error(transparent)]
40    Archiver(#[from] archiver::Error),
41    #[error(transparent)]
42    RocksDb(#[from] crate::utils::rocksdb::Error),
43}
44
45/// Shared summary of an entry's tip
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47pub struct EntrySummary {
48    pub header: EntryHeader,
49    pub signature: Signature,
50    #[serde(default, skip_serializing_if = "Option::is_none", with = "mask_serde")]
51    pub mask: Option<BitVec<u8, Msb0>>,
52}
53
54impl From<Entry> for EntrySummary {
55    fn from(entry: Entry) -> Self {
56        Self { header: entry.header, signature: entry.signature, mask: entry.mask }
57    }
58}
59
60impl EntrySummary {
61    /// Primary: Parse from vecpak PropListMap
62    pub fn from_vecpak_map(map: &amadeus_utils::vecpak::PropListMap) -> Result<Self, Error> {
63        let hmap = map
64            .get_by_key(b"header")
65            .ok_or(Error::BadFormat("entry.header"))?
66            .get_proplist_map()
67            .ok_or(Error::BadFormat("entry.header"))?;
68
69        let header = EntryHeader {
70            height: hmap.get_integer(b"height").ok_or(Error::BadFormat("entry.header.height"))?,
71            slot: hmap.get_integer(b"slot").ok_or(Error::BadFormat("entry.header.slot"))?,
72            prev_slot: hmap.get_integer(b"prev_slot").ok_or(Error::BadFormat("entry.header.prev_slot"))?,
73            prev_hash: hmap.get_binary(b"prev_hash").ok_or(Error::BadFormat("entry.header.prev_hash"))?,
74            dr: hmap.get_binary(b"dr").ok_or(Error::BadFormat("entry.header.dr"))?,
75            vr: hmap.get_binary(b"vr").ok_or(Error::BadFormat("entry.header.vr"))?,
76            signer: hmap.get_binary(b"signer").ok_or(Error::BadFormat("entry.header.signer"))?,
77            root_tx: hmap.get_binary(b"root_tx").ok_or(Error::BadFormat("entry.header.root_tx"))?,
78            root_validator: hmap
79                .get_binary(b"root_validator")
80                .ok_or(Error::BadFormat("entry.header.root_validator"))?,
81        };
82
83        let mask = map.get_binary::<Vec<u8>>(b"mask").map(bin_to_bitvec);
84        let signature: Signature = map.get_binary(b"signature").ok_or(Error::BadFormat("entry.signature"))?;
85
86        Ok(Self { header, signature, mask })
87    }
88
89    pub fn to_vecpak_term(&self) -> Term {
90        let mut props = vec![
91            (Term::Binary(b"header".to_vec()), self.header.to_vecpak_term()),
92            (Term::Binary(b"signature".to_vec()), Term::Binary(self.signature.to_vec())),
93        ];
94        if let Some(mask) = &self.mask {
95            props.push((Term::Binary(b"mask".to_vec()), Term::Binary(bitvec_to_bin(mask))));
96        }
97        Term::PropList(props)
98    }
99
100    /// Empty summary placeholder used when tips are missing
101    pub fn empty() -> Self {
102        let header = EntryHeader {
103            height: 0,
104            slot: 0,
105            prev_slot: 0,
106            prev_hash: Hash::from([0u8; 32]),
107            dr: Hash::from([0u8; 32]),
108            vr: Signature::from([0u8; 96]),
109            signer: PublicKey::from([0u8; 48]),
110            root_tx: Hash::from([0u8; 32]),
111            root_validator: Hash::from([0u8; 32]),
112        };
113        Self { header, signature: Signature::from([0u8; 96]), mask: None }
114    }
115}
116
117#[derive(Clone, serde::Serialize, serde::Deserialize)]
118pub struct EntryHeader {
119    pub height: u64,
120    pub slot: u64,
121    pub prev_slot: i64, // is negative 1 in genesis entry
122    pub prev_hash: Hash,
123    pub dr: Hash,      // deterministic random value
124    pub vr: Signature, // verifiable random value
125    pub signer: PublicKey,
126    #[serde(default = "zero_hash", skip_serializing_if = "is_zero_hash")]
127    pub root_tx: Hash,
128    #[serde(default = "zero_hash", skip_serializing_if = "is_zero_hash")]
129    pub root_validator: Hash,
130}
131
132fn zero_hash() -> Hash {
133    Hash::from([0u8; 32])
134}
135
136fn is_zero_hash(h: &Hash) -> bool {
137    *AsRef::<[u8; 32]>::as_ref(h) == [0u8; 32]
138}
139
140impl fmt::Debug for EntryHeader {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        f.debug_struct("EntryHeader")
143            .field("slot", &self.slot)
144            .field("dr", &bs58::encode(&self.dr).into_string())
145            .field("height", &self.height)
146            .field("prev_hash", &bs58::encode(&self.prev_hash).into_string())
147            .field("prev_slot", &self.prev_slot)
148            .field("signer", &bs58::encode(&self.signer).into_string())
149            .field("root_tx", &bs58::encode(&self.root_tx).into_string())
150            .field("root_validator", &bs58::encode(&self.root_validator).into_string())
151            .field("vr", &bs58::encode(&self.vr).into_string())
152            .finish()
153    }
154}
155
156impl EntryHeader {
157    pub fn from_vecpak_map(map: &amadeus_utils::vecpak::PropListMap) -> Result<Self, Error> {
158        Ok(EntryHeader {
159            height: map.get_integer(b"height").ok_or(Error::BadFormat("entry.header.height"))?,
160            slot: map.get_integer(b"slot").ok_or(Error::BadFormat("entry.header.slot"))?,
161            prev_slot: map.get_integer(b"prev_slot").ok_or(Error::BadFormat("entry.header.prev_slot"))?,
162            prev_hash: map.get_binary(b"prev_hash").ok_or(Error::BadFormat("entry.header.prev_hash"))?,
163            dr: map.get_binary(b"dr").ok_or(Error::BadFormat("entry.header.dr"))?,
164            vr: map.get_binary(b"vr").ok_or(Error::BadFormat("entry.header.vr"))?,
165            signer: map.get_binary(b"signer").ok_or(Error::BadFormat("entry.header.signer"))?,
166            root_tx: map.get_binary(b"root_tx").unwrap_or_else(zero_hash),
167            root_validator: map.get_binary(b"root_validator").unwrap_or_else(zero_hash),
168        })
169    }
170
171    pub fn to_vecpak_term(&self) -> Term {
172        let mut props = vec![
173            (Term::Binary(b"height".to_vec()), Term::VarInt(self.height as i128)),
174            (Term::Binary(b"slot".to_vec()), Term::VarInt(self.slot as i128)),
175            (Term::Binary(b"prev_slot".to_vec()), Term::VarInt(self.prev_slot as i128)),
176            (Term::Binary(b"prev_hash".to_vec()), Term::Binary(self.prev_hash.to_vec())),
177            (Term::Binary(b"dr".to_vec()), Term::Binary(self.dr.to_vec())),
178            (Term::Binary(b"vr".to_vec()), Term::Binary(self.vr.to_vec())),
179            (Term::Binary(b"signer".to_vec()), Term::Binary(self.signer.to_vec())),
180        ];
181        if !is_zero_hash(&self.root_tx) {
182            props.push((Term::Binary(b"root_tx".to_vec()), Term::Binary(self.root_tx.to_vec())));
183        }
184        if !is_zero_hash(&self.root_validator) {
185            props.push((Term::Binary(b"root_validator".to_vec()), Term::Binary(self.root_validator.to_vec())));
186        }
187        Term::PropList(props)
188    }
189
190    pub fn to_vecpak_bin(&self) -> Vec<u8> {
191        let term = self.to_vecpak_term();
192        encode(term)
193    }
194}
195
196mod mask_serde {
197    use super::{BitVec, Msb0, bin_to_bitvec, bitvec_to_bin};
198    use serde::{Deserialize, Deserializer, Serialize, Serializer};
199    pub fn serialize<S: Serializer>(mask: &Option<BitVec<u8, Msb0>>, ser: S) -> Result<S::Ok, S::Error> {
200        match mask {
201            Some(m) => serde_bytes::Bytes::new(&bitvec_to_bin(m)).serialize(ser),
202            None => ser.serialize_none(),
203        }
204    }
205    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Option<BitVec<u8, Msb0>>, D::Error> {
206        let v: Option<serde_bytes::ByteBuf> = Deserialize::deserialize(de)?;
207        Ok(v.map(|b| bin_to_bitvec(b.into_vec())))
208    }
209}
210
211/// Custom deserializer for txs that handles both binary blobs (from Elixir) and structured EntryTx
212mod txs_serde {
213    use super::EntryTx;
214    use amadeus_utils::vecpak;
215    use serde::de::{self, SeqAccess, Visitor};
216    use serde::{Deserialize, Deserializer, Serialize, Serializer};
217    use std::fmt;
218
219    pub fn serialize<S: Serializer>(txs: &Vec<EntryTx>, ser: S) -> Result<S::Ok, S::Error> {
220        txs.serialize(ser)
221    }
222
223    /// Visitor for individual tx items that handles both binary and structured
224    struct TxItemVisitor;
225
226    impl<'de> Visitor<'de> for TxItemVisitor {
227        type Value = Option<EntryTx>;
228
229        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
230            formatter.write_str("a binary blob or structured EntryTx")
231        }
232
233        fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
234            // Binary blob - decode as vecpak
235            Ok(vecpak::from_slice::<EntryTx>(v).ok())
236        }
237
238        fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
239            Ok(vecpak::from_slice::<EntryTx>(&v).ok())
240        }
241
242        fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
243            // Structured EntryTx - use normal deserialization
244            let de = de::value::MapAccessDeserializer::new(map);
245            EntryTx::deserialize(de).map(Some)
246        }
247    }
248
249    struct TxItemDeserializer;
250
251    impl<'de> de::DeserializeSeed<'de> for TxItemDeserializer {
252        type Value = Option<EntryTx>;
253
254        fn deserialize<D: Deserializer<'de>>(self, de: D) -> Result<Self::Value, D::Error> {
255            de.deserialize_any(TxItemVisitor)
256        }
257    }
258
259    struct TxsVisitor;
260
261    impl<'de> Visitor<'de> for TxsVisitor {
262        type Value = Vec<EntryTx>;
263
264        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
265            formatter.write_str("a list of transactions")
266        }
267
268        fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
269            let mut txs = Vec::with_capacity(seq.size_hint().unwrap_or(0));
270            while let Some(maybe_tx) = seq.next_element_seed(TxItemDeserializer)? {
271                if let Some(tx) = maybe_tx {
272                    txs.push(tx);
273                }
274            }
275            Ok(txs)
276        }
277    }
278
279    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Vec<EntryTx>, D::Error> {
280        de.deserialize_seq(TxsVisitor)
281    }
282}
283
284#[derive(Clone, serde::Serialize, serde::Deserialize)]
285pub struct Entry {
286    pub header: EntryHeader,
287    #[serde(with = "txs_serde")]
288    pub txs: Vec<EntryTx>,
289    pub hash: Hash,
290    pub signature: Signature,
291    #[serde(default, skip_serializing_if = "Option::is_none", with = "mask_serde")]
292    pub mask: Option<BitVec<u8, Msb0>>,
293}
294
295impl Entry {
296    pub fn from_vecpak_bin(bin: &[u8]) -> Result<Self, Error> {
297        let map = decode(bin)
298            .map_err(|_| Error::BadFormat("entry_packed"))?
299            .get_proplist_map()
300            .ok_or(Error::BadFormat("entry_packed"))?;
301        Self::from_vecpak_map(&map)
302    }
303
304    pub fn to_vecpak_bin(&self) -> Vec<u8> {
305        let term = self.to_vecpak_term();
306        encode(term)
307    }
308
309    pub fn from_vecpak_map(map: &amadeus_utils::vecpak::PropListMap) -> Result<Self, Error> {
310        let hash: Hash = map.get_binary(b"hash").ok_or(Error::BadFormat("entry.hash"))?;
311        let signature: Signature = map.get_binary(b"signature").ok_or(Error::BadFormat("entry.signature"))?;
312
313        let hmap = map
314            .get_by_key(b"header")
315            .ok_or(Error::BadFormat("entry.header"))?
316            .get_proplist_map()
317            .ok_or(Error::BadFormat("entry.header"))?;
318        let header = EntryHeader::from_vecpak_map(&hmap)?;
319
320        let mask = map.get_binary::<Vec<u8>>(b"mask").map(bin_to_bitvec);
321
322        // Parse txs as structured EntryTx objects
323        let txs = map
324            .get_list(b"txs")
325            .map(|list| {
326                list.iter()
327                    .filter_map(|t| {
328                        // Each tx is a PropList, encode it and deserialize as EntryTx
329                        let term_bin = encode(t.clone());
330                        vecpak::from_slice::<EntryTx>(&term_bin).ok()
331                    })
332                    .collect()
333            })
334            .unwrap_or_default();
335
336        Ok(Entry { hash, header, signature, mask, txs })
337    }
338
339    pub fn to_vecpak_term(&self) -> Term {
340        // Serialize each EntryTx to a Term
341        let txs_list = Term::List(
342            self.txs
343                .iter()
344                .filter_map(|tx| {
345                    let bin = vecpak::to_vec(tx).ok()?;
346                    decode(&bin).ok()
347                })
348                .collect(),
349        );
350        let mut props = vec![
351            (Term::Binary(b"header".to_vec()), self.header.to_vecpak_term()),
352            (Term::Binary(b"txs".to_vec()), txs_list),
353            (Term::Binary(b"hash".to_vec()), Term::Binary(self.hash.to_vec())),
354            (Term::Binary(b"signature".to_vec()), Term::Binary(self.signature.to_vec())),
355        ];
356        if let Some(mask) = &self.mask {
357            props.push((Term::Binary(b"mask".to_vec()), Term::Binary(bitvec_to_bin(mask))));
358        }
359        Term::PropList(props)
360    }
361}
362
363#[derive(Clone, serde::Serialize, serde::Deserialize)]
364pub struct EventEntry {
365    pub entry_packed: Entry,
366}
367
368impl EventEntry {
369    pub const TYPENAME: &'static str = "event_entry";
370}
371
372impl Typename for EventEntry {
373    fn typename(&self) -> &'static str {
374        Self::TYPENAME
375    }
376}
377
378impl fmt::Debug for EventEntry {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        f.debug_struct("EntryProto").field("entry_packed", &self.entry_packed).finish()
381    }
382}
383
384#[async_trait::async_trait]
385impl Handle for EventEntry {
386    async fn handle(&self, ctx: &Context, src: Ipv4Addr) -> Result<Vec<protocol::Instruction>, protocol::Error> {
387        self.entry_packed.handle(ctx, src).await
388    }
389}
390
391impl crate::utils::misc::Typename for Entry {
392    fn typename(&self) -> &'static str {
393        Self::TYPENAME
394    }
395}
396
397impl Entry {
398    async fn handle(&self, ctx: &Context, _src: Ipv4Addr) -> Result<Vec<protocol::Instruction>, protocol::Error> {
399        let height = self.header.height;
400
401        // compute rooted_tip_height if possible
402        let rooted_height = ctx
403            .fabric
404            .get_rooted_hash()
405            .ok()
406            .flatten()
407            .map(TryInto::try_into)
408            .and_then(|h| h.ok())
409            .and_then(|h| ctx.fabric.get_entry_by_hash(&h))
410            .map(|e| e.header.height)
411            .unwrap_or(0);
412
413        if height >= rooted_height {
414            let hash = self.hash;
415            let slot = self.header.slot;
416            let bin = self.to_vecpak_bin();
417
418            ctx.fabric.insert_entry(&hash, height, slot, &bin, get_unix_millis_now())?;
419
420            //let epoch = self.get_epoch();
421            //archiver::store(bin, format!("epoch-{}", epoch), format!("entry-{}", height)).await?;
422        }
423
424        Ok(vec![protocol::Instruction::Noop { why: "entry handling not implemented".to_string() }])
425    }
426}
427
428impl fmt::Debug for Entry {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        f.debug_struct("Entry")
431            .field("hash", &bs58::encode(&self.hash).into_string())
432            .field("header", &self.header)
433            .field("signature", &bs58::encode(&self.signature).into_string())
434            .field("txs", &self.txs.iter().map(|tx| bs58::encode(&tx.hash).into_string()).collect::<Vec<String>>())
435            .finish()
436    }
437}
438
439impl Entry {
440    pub const TYPENAME: &'static str = "event_entry";
441
442    /// Build next header skeleton similar to Entry.build_next/2.
443    /// This requires chain state (pk/sk), so we only provide a helper to derive next header fields given inputs.
444    pub fn build_next_header(&self, slot: u64, signer_pk: &PublicKey, signer_sk: &[u8]) -> Result<EntryHeader, Error> {
445        // dr' = blake3(dr)
446        let dr = blake3::hash(self.header.dr.as_ref());
447        // vr' = sign(sk, prev_vr, DST_VRF)
448        let vr = bls12_381::sign(signer_sk, self.header.vr.as_ref(), DST_VRF)?;
449
450        Ok(EntryHeader {
451            slot,
452            height: self.header.height + 1,
453            prev_slot: self.header.slot as i64,
454            prev_hash: self.hash,
455            dr: Hash::from(dr),
456            vr,
457            signer: *signer_pk,
458            root_tx: Hash::from([0u8; 32]),
459            root_validator: Hash::from([0u8; 32]),
460        })
461    }
462
463    pub fn get_epoch(&self) -> u64 {
464        self.header.height / 100_000
465    }
466
467    pub fn contains_tx(&self, tx_function: &str) -> bool {
468        self.txs.iter().any(|tx| tx.tx.action.function.as_slice() == tx_function.as_bytes())
469    }
470}
471
472/// Get archived entries as a list of (epoch, height, entry_size) tuples by parsing filenames
473pub async fn get_archived_entries() -> Result<Vec<(u64, u64, u64)>, Error> {
474    let filenames_with_sizes = archiver::get_archived_filenames().await?;
475    let mut entries = Vec::new();
476
477    for (filename, file_size) in filenames_with_sizes {
478        if let Some((epoch, height)) = parse_entry_filename(&filename) {
479            entries.push((epoch, height, file_size));
480        }
481    }
482
483    // Sort by epoch first, then by height
484    entries.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
485    entries.dedup(); // Remove any duplicates
486
487    Ok(entries)
488}
489
490/// Parse entry filename to extract epoch and height
491/// Expected format: "epoch-{epoch}/entry-{height}" or similar patterns
492fn parse_entry_filename(filename: &str) -> Option<(u64, u64)> {
493    // Split by '/' to get directory and filename parts
494    let parts: Vec<&str> = filename.split('/').collect();
495
496    let mut epoch = None;
497    let mut height = None;
498
499    // Look for epoch in directory part (e.g., "epoch-123")
500    for part in &parts {
501        if let Some(epoch_str) = part.strip_prefix("epoch-") {
502            if let Ok(e) = epoch_str.parse::<u64>() {
503                epoch = Some(e);
504            }
505        }
506    }
507
508    // Look for height in filename part (e.g., "entry-456")
509    if let Some(filename_part) = parts.last() {
510        if let Some(height_str) = filename_part.strip_prefix("entry-") {
511            if let Ok(h) = height_str.parse::<u64>() {
512                height = Some(h);
513            }
514        }
515    }
516
517    match (epoch, height) {
518        (Some(e), Some(h)) => Some((e, h)),
519        _ => None,
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use crate::consensus::doms::tx::{EntryTx, EntryTxAction, EntryTxInner};
527
528    fn make_test_tx(nonce: i128) -> EntryTx {
529        EntryTx {
530            hash: Hash::from([0xABu8; 32]),
531            signature: Signature::from([0xCDu8; 96]),
532            tx: EntryTxInner {
533                action: EntryTxAction {
534                    args: vec![vec![1, 2, 3]],
535                    contract: b"TestContract".to_vec(),
536                    function: b"test_func".to_vec(),
537                    op: "call".to_string(),
538                    attached_symbol: None,
539                    attached_amount: None,
540                },
541                nonce,
542                signer: PublicKey::from([0xEFu8; 48]),
543            },
544        }
545    }
546
547    #[test]
548    fn test_parse_entry_filename() {
549        // Test valid filenames
550        assert_eq!(parse_entry_filename("epoch-0/entry-12345"), Some((0, 12345)));
551        assert_eq!(parse_entry_filename("epoch-123/entry-456"), Some((123, 456)));
552        assert_eq!(parse_entry_filename("epoch-999/subdir/entry-789"), Some((999, 789)));
553
554        // Test invalid filenames
555        assert_eq!(parse_entry_filename("not-epoch/entry-123"), None);
556        assert_eq!(parse_entry_filename("epoch-123/not-entry"), None);
557        assert_eq!(parse_entry_filename("epoch-abc/entry-123"), None);
558        assert_eq!(parse_entry_filename("epoch-123/entry-def"), None);
559        assert_eq!(parse_entry_filename("random-file.txt"), None);
560        assert_eq!(parse_entry_filename(""), None);
561    }
562
563    #[tokio::test]
564    async fn test_get_archived_entries_empty() {
565        // This test will only work if the archiver is not initialized
566        // or the directory is empty, which is fine for testing the function structure
567        let result = get_archived_entries().await;
568        // We don't assert specific values since we don't know the state of the filesystem
569        // but we ensure the function doesn't panic and returns a proper Result
570        match result {
571            Ok(entries) => {
572                // Entries should be sorted and deduplicated
573                for i in 1..entries.len() {
574                    let prev = entries[i - 1];
575                    let curr = entries[i];
576                    assert!(prev.0 < curr.0 || (prev.0 == curr.0 && prev.1 <= curr.1));
577                    // Each entry should have a file size (third element)
578                    assert!(curr.2 > 0 || curr.2 == 0); // File size can be 0 for empty files
579                }
580            }
581            Err(_) => {
582                // It's okay if it fails due to archiver not being initialized
583            }
584        }
585    }
586
587    #[test]
588    fn test_entry_serde_vecpak_roundtrip() {
589        use amadeus_utils::vecpak;
590
591        let header = EntryHeader {
592            height: 12345,
593            slot: 67890,
594            prev_slot: 67889,
595            prev_hash: Hash::from([1u8; 32]),
596            dr: Hash::from([2u8; 32]),
597            vr: Signature::from([3u8; 96]),
598            signer: PublicKey::from([4u8; 48]),
599            root_tx: Hash::from([5u8; 32]),
600            root_validator: Hash::from([14u8; 32]),
601        };
602        let entry = Entry {
603            hash: Hash::from([6u8; 32]),
604            header,
605            signature: Signature::from([7u8; 96]),
606            mask: Some(bin_to_bitvec(vec![0xFF, 0x00, 0xAB])),
607            txs: vec![make_test_tx(1), make_test_tx(2)],
608        };
609
610        // to_vecpak_bin -> from_slice
611        let vecpak_bin = entry.to_vecpak_bin();
612        let decoded: Entry = vecpak::from_slice(&vecpak_bin).expect("from_slice");
613        assert_eq!(decoded.hash, entry.hash);
614        assert_eq!(decoded.header.height, entry.header.height);
615        assert_eq!(decoded.header.slot, entry.header.slot);
616        assert_eq!(decoded.txs.len(), entry.txs.len());
617        assert_eq!(decoded.mask, entry.mask);
618
619        // to_vec -> from_vecpak_bin
620        let serde_bin = vecpak::to_vec(&entry).expect("to_vec");
621        let decoded2 = Entry::from_vecpak_bin(&serde_bin).expect("from_vecpak_bin");
622        assert_eq!(decoded2.hash, entry.hash);
623        assert_eq!(decoded2.header.height, entry.header.height);
624        assert_eq!(decoded2.header.slot, entry.header.slot);
625        assert_eq!(decoded2.txs.len(), entry.txs.len());
626        assert_eq!(decoded2.mask, entry.mask);
627
628        // verify byte-for-byte compatibility
629        assert_eq!(vecpak_bin, serde_bin);
630
631        // test without mask
632        let entry_no_mask = Entry {
633            hash: Hash::from([8u8; 32]),
634            header: EntryHeader {
635                height: 1,
636                slot: 2,
637                prev_slot: -1,
638                prev_hash: Hash::from([9u8; 32]),
639                dr: Hash::from([10u8; 32]),
640                vr: Signature::from([11u8; 96]),
641                signer: PublicKey::from([12u8; 48]),
642                root_tx: Hash::from([13u8; 32]),
643                root_validator: Hash::from([15u8; 32]),
644            },
645            signature: Signature::from([14u8; 96]),
646            mask: None,
647            txs: vec![],
648        };
649        let vecpak_bin2 = entry_no_mask.to_vecpak_bin();
650        let serde_bin2 = vecpak::to_vec(&entry_no_mask).expect("to_vec");
651        assert_eq!(vecpak_bin2, serde_bin2);
652        let decoded3: Entry = vecpak::from_slice(&vecpak_bin2).expect("from_slice");
653        assert_eq!(decoded3.mask, None);
654        assert_eq!(decoded3.header.prev_slot, -1);
655    }
656
657    #[test]
658    fn test_entry_proto_roundtrip() {
659        use amadeus_utils::vecpak;
660
661        // Create a test EntryProto and verify it can roundtrip through serde
662        let entry_proto = EventEntry {
663            entry_packed: Entry {
664                hash: Hash::from([0x07u8; 32]),
665                header: EntryHeader {
666                    height: 41939338,
667                    slot: 41939338,
668                    prev_slot: 41939337,
669                    prev_hash: Hash::from([0xD9u8; 32]),
670                    dr: Hash::from([0x91u8; 32]),
671                    vr: Signature::from([0xB3u8; 96]),
672                    signer: PublicKey::from([0x95u8; 48]),
673                    root_tx: Hash::from([0x3Cu8; 32]),
674                    root_validator: Hash::from([0x28u8; 32]),
675                },
676                signature: Signature::from([0x90u8; 96]),
677                mask: None,
678                txs: vec![make_test_tx(1762402566835945439)],
679            },
680        };
681
682        // Serialize
683        let bin = vecpak::to_vec(&entry_proto).expect("should serialize");
684        println!("Serialized EntryProto: {} bytes", bin.len());
685        println!("Hex: {}", hex::encode(&bin));
686
687        // Deserialize
688        let decoded: EventEntry = vecpak::from_slice(&bin).expect("should deserialize");
689
690        // Verify
691        assert_eq!(decoded.entry_packed.header.height, 41939338);
692        assert_eq!(decoded.entry_packed.txs.len(), 1);
693        assert_eq!(decoded.entry_packed.txs[0].tx.action.function.as_slice(), b"test_func");
694
695        println!("Successfully roundtripped EntryProto!");
696    }
697}