amadeus_node/node/
protocol.rs

1use crate::Context;
2use crate::consensus::consensus::{self, Consensus};
3use crate::consensus::doms::attestation::EventAttestation;
4use crate::consensus::doms::entry::{Entry, EventEntry};
5use crate::consensus::doms::tx::TxU;
6use crate::consensus::doms::{Attestation, EntrySummary};
7use crate::consensus::fabric::Fabric;
8use crate::node::anr::Anr;
9use crate::node::peers::HandshakeStatus;
10use crate::node::{anr, peers};
11use crate::utils::Hash;
12use crate::utils::bls12_381;
13use crate::utils::misc::get_unix_millis_now;
14#[cfg(test)]
15use crate::utils::{PublicKey, Signature};
16use amadeus_utils::B3f4;
17use amadeus_utils::vecpak;
18use ambassador::{Delegate, delegatable_trait};
19use serde::{Deserialize, Serialize};
20use std::fmt::Debug;
21use std::io::Error as IoError;
22use std::net::{Ipv4Addr, SocketAddr};
23use tracing::instrument;
24
25#[derive(Delegate, Debug, Serialize, Deserialize)]
26#[delegate(Handle)]
27pub enum Protocol {
28    Ping(Ping),
29    PingReply(PingReply),
30    EventEntry(EventEntry),
31    EventTip(EventTip),
32    EventAttestation(EventAttestation),
33    EventTx(EventTx),
34    GetPeerAnrs(GetPeerAnrs),
35    GetPeerAnrsReply(GetPeerAnrsReply),
36    NewPhoneWhoDis(NewPhoneWhoDis),
37    NewPhoneWhoDisReply(NewPhoneWhoDisReply),
38    Catchup(Catchup),
39    CatchupReply(CatchupReply),
40    SpecialBusiness(SpecialBusiness),
41    SpecialBusinessReply(SpecialBusinessReply),
42}
43
44impl Typename for Protocol {
45    fn typename(&self) -> &'static str {
46        match self {
47            Protocol::Ping(_) => Ping::TYPENAME,
48            Protocol::PingReply(_) => PingReply::TYPENAME,
49            Protocol::EventEntry(_) => EventEntry::TYPENAME,
50            Protocol::EventTip(_) => EventTip::TYPENAME,
51            Protocol::EventAttestation(_) => EventAttestation::TYPENAME,
52            Protocol::EventTx(_) => EventTx::TYPENAME,
53            Protocol::GetPeerAnrs(_) => GetPeerAnrs::TYPENAME,
54            Protocol::GetPeerAnrsReply(_) => GetPeerAnrsReply::TYPENAME,
55            Protocol::NewPhoneWhoDis(_) => NewPhoneWhoDis::TYPENAME,
56            Protocol::NewPhoneWhoDisReply(_) => NewPhoneWhoDisReply::TYPENAME,
57            Protocol::Catchup(_) => Catchup::TYPENAME,
58            Protocol::CatchupReply(_) => CatchupReply::TYPENAME,
59            Protocol::SpecialBusiness(_) => SpecialBusiness::TYPENAME,
60            Protocol::SpecialBusinessReply(_) => SpecialBusinessReply::TYPENAME,
61        }
62    }
63}
64
65impl Protocol {
66    /// Requires ANR to be available due to e2e encryption
67    pub async fn send_to_with_metrics(&self, ctx: &Context, dst: Ipv4Addr) -> Result<(), Error> {
68        let dst_addr = SocketAddr::new(std::net::IpAddr::V4(dst), ctx.config.udp_port);
69        let dst_anr = ctx.anrs.get_by_ip4(dst).await.ok_or(Error::NoAnrForDestination(dst))?;
70
71        let shards = ctx.reassembler.build_shards(&ctx.config, &vecpak::to_vec(&self)?, &dst_anr.pk).await?;
72        for shard in &shards {
73            ctx.socket.send_to_with_metrics(shard, dst_addr, &ctx.metrics).await?;
74        }
75
76        ctx.metrics.add_outgoing_proto(self.typename());
77        Ok(())
78    }
79}
80
81/// Trait for types that can provide their type name as a static string
82#[delegatable_trait]
83pub trait Typename {
84    /// Get the type name for this instance
85    /// For enums, this can return different names based on the variant
86    fn typename(&self) -> &'static str;
87}
88
89/// Every object that has this trait must be convertible from a Vecpak
90/// Binary representation and must be able to handle itself as a message
91#[async_trait::async_trait]
92#[delegatable_trait]
93pub trait Handle: Typename + Debug + Send + Sync + Serialize {
94    /// Handle a message returning instructions for upper layers
95    async fn handle(&self, ctx: &Context, src: Ipv4Addr) -> Result<Vec<Instruction>, Error>;
96}
97
98#[derive(Debug, thiserror::Error, strum_macros::IntoStaticStr)]
99pub enum Error {
100    #[error(transparent)]
101    Io(#[from] IoError),
102    #[error(transparent)]
103    Bls(#[from] bls12_381::Error),
104    #[error(transparent)]
105    Tx(#[from] crate::consensus::doms::tx::Error),
106    #[error(transparent)]
107    Entry(#[from] crate::consensus::doms::entry::Error),
108    #[error(transparent)]
109    Archiver(#[from] crate::utils::archiver::Error),
110    #[error(transparent)]
111    Peers(#[from] peers::Error),
112    #[error(transparent)]
113    Consensus(#[from] consensus::Error),
114    #[error(transparent)]
115    Fabric(#[from] crate::consensus::fabric::Error),
116    #[error(transparent)]
117    Att(#[from] crate::consensus::doms::attestation::Error),
118    #[error(transparent)]
119    Reassembler(#[from] crate::node::reassembler::Error),
120    #[error(transparent)]
121    Anr(#[from] anr::Error),
122    #[error("parse error: {0}")]
123    ParseError(&'static str),
124    #[error("No ANR found for destination IP: {0}")]
125    NoAnrForDestination(Ipv4Addr),
126    #[error(transparent)]
127    Vecpak(#[from] vecpak::Error),
128    #[error("other error: {0}")]
129    Other(String),
130}
131
132impl Typename for Error {
133    fn typename(&self) -> &'static str {
134        self.into()
135    }
136}
137
138/// Result of handling an incoming message.
139#[derive(Debug, strum_macros::IntoStaticStr)]
140pub enum Instruction {
141    Noop { why: String },
142    SendNewPhoneWhoDisReply { dst: Ipv4Addr },
143    SendGetPeerAnrsReply { anrs: Vec<Anr>, dst: Ipv4Addr },
144    SendPingReply { ts_m: u64, dst: Ipv4Addr },
145
146    ValidTxs { txs: Vec<Vec<u8>> },
147    ReceivedEntry { entry: Entry },
148    ReceivedAttestation { attestation: Attestation },
149    ReceivedConsensus { consensus: Consensus },
150    SpecialBusiness { business: Vec<u8> },
151    SpecialBusinessReply { business: Vec<u8> },
152    SolicitEntry { hash: Vec<u8> },
153    SolicitEntry2,
154}
155
156impl Typename for Instruction {
157    fn typename(&self) -> &'static str {
158        self.into()
159    }
160}
161
162#[derive(Debug, serde::Serialize, serde::Deserialize)]
163pub struct EventTip {
164    pub temporal: EntrySummary,
165    pub rooted: EntrySummary,
166}
167
168impl Typename for EventTip {
169    fn typename(&self) -> &'static str {
170        Self::TYPENAME
171    }
172}
173
174#[async_trait::async_trait]
175impl Handle for EventTip {
176    #[instrument(skip(self, ctx), fields(src = %src), name = "EventTip::handle")]
177    async fn handle(&self, ctx: &Context, src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
178        // TODO: validate temporal and rooted entry signatures like in Elixir
179
180        let signer = self.temporal.header.signer.to_vec();
181        let is_trainer =
182            match ctx.fabric.trainers_for_height(ctx.fabric.get_temporal_height().ok().flatten().unwrap_or_default()) {
183                Some(trainers) => trainers.iter().any(|pk| pk.as_slice() == signer),
184                None => false,
185            };
186
187        if is_trainer || ctx.is_peer_handshaked(src).await {
188            ctx.peers.update_peer_from_tip(ctx, src, self).await;
189        }
190
191        Ok(vec![Instruction::Noop { why: "event_tip handling not implemented".to_string() }])
192    }
193}
194
195impl EventTip {
196    pub const TYPENAME: &'static str = "event_tip";
197
198    pub fn new(temporal: EntrySummary, rooted: EntrySummary) -> Self {
199        Self { temporal, rooted }
200    }
201
202    pub fn from_current_tips() -> Result<Self, Error> {
203        // Deprecated: use from_current_tips_db with an explicit DB handle
204        Err(Error::Consensus(consensus::Error::NotImplemented("from_current_tips requires DB context")))
205    }
206
207    /// Build EventTip from the current temporal/rooted tips in Fabric using the provided Fabric handle
208    pub fn from_current_tips_db(fab: &Fabric) -> Result<Self, Error> {
209        // Helper to load EntrySummary by tip hash, or return empty summary if missing
210        fn entry_summary_by_hash(fab: &Fabric, hash: &Hash) -> EntrySummary {
211            if let Some(entry) = fab.get_entry_by_hash(hash) { entry.into() } else { EntrySummary::empty() }
212        }
213
214        let temporal_summary = match fab.get_temporal_hash()? {
215            Some(h) => entry_summary_by_hash(fab, &Hash::from(h)),
216            None => EntrySummary::empty(),
217        };
218
219        let rooted_summary = match fab.get_rooted_hash()? {
220            Some(h) => entry_summary_by_hash(fab, &Hash::from(h)),
221            None => EntrySummary::empty(),
222        };
223
224        Ok(Self { temporal: temporal_summary, rooted: rooted_summary })
225    }
226}
227
228#[derive(Debug, serde::Serialize, serde::Deserialize)]
229pub struct Ping {
230    pub ts_m: u64,
231}
232
233#[derive(Debug, serde::Serialize, serde::Deserialize)]
234pub struct PingReply {
235    pub ts_m: u64,
236    #[serde(skip)]
237    pub seen_time: u64,
238}
239
240#[derive(Debug, serde::Serialize, serde::Deserialize)]
241pub struct Catchup {
242    pub height_flags: Vec<CatchupHeight>,
243}
244
245#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
246pub struct CatchupHeight {
247    pub height: u64,
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub c: Option<bool>,
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub e: Option<bool>,
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub a: Option<bool>,
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub hashes: Option<Vec<Vec<u8>>>,
256}
257
258impl Catchup {
259    pub const TYPENAME: &'static str = "catchup";
260}
261
262impl Typename for Catchup {
263    fn typename(&self) -> &'static str {
264        Self::TYPENAME
265    }
266}
267
268#[async_trait::async_trait]
269impl Handle for Catchup {
270    async fn handle(&self, _ctx: &Context, _src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
271        Ok(vec![Instruction::Noop { why: "catchup received".to_string() }])
272    }
273}
274
275#[derive(Debug, serde::Serialize, serde::Deserialize)]
276pub struct CatchupReply {
277    pub tries: Vec<CatchupHeightReply>,
278}
279
280#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
281pub struct CatchupHeightReply {
282    pub height: u64,
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub entries: Option<Vec<Entry>>,
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub attestations: Option<Vec<Attestation>>,
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub consensuses: Option<Vec<Consensus>>,
289}
290
291impl CatchupReply {
292    pub const TYPENAME: &'static str = "catchup_reply";
293}
294
295impl Typename for CatchupReply {
296    fn typename(&self) -> &'static str {
297        Self::TYPENAME
298    }
299}
300
301#[async_trait::async_trait]
302impl Handle for CatchupReply {
303    async fn handle(&self, ctx: &Context, _src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
304        let instructions = Vec::new();
305        let rooted_tip_height = ctx.fabric.get_rooted_height()?.unwrap_or(0);
306
307        for trie in &self.tries {
308            if let Some(ref entries) = trie.entries {
309                for entry in entries {
310                    if entry.header.height >= rooted_tip_height {
311                        let seen_time_ms = get_unix_millis_now();
312                        let entry_bin = entry.to_vecpak_bin();
313                        {
314                            let _ = ctx.fabric.insert_entry(
315                                &entry.hash,
316                                entry.header.height,
317                                entry.header.slot,
318                                &entry_bin,
319                                seen_time_ms,
320                            );
321                        }
322                    }
323                }
324            }
325
326            if let Some(ref attestations) = trie.attestations {
327                for _attestation in attestations {}
328            }
329
330            if let Some(ref consensuses) = trie.consensuses {
331                for consensus in consensuses {
332                    let _ = ctx.fabric.insert_consensus(&consensus);
333                }
334            }
335        }
336
337        Ok(instructions)
338    }
339}
340
341#[derive(Debug)]
342pub struct SolicitEntry {
343    pub hash: Vec<u8>,
344}
345
346#[derive(Debug)]
347pub struct SolicitEntry2;
348
349impl Typename for Ping {
350    fn typename(&self) -> &'static str {
351        Self::TYPENAME
352    }
353}
354
355#[async_trait::async_trait]
356impl Handle for Ping {
357    #[instrument(skip(self, ctx), fields(src = %src), name = "Ping::handle")]
358    async fn handle(&self, ctx: &Context, src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
359        ctx.peers.update_peer_ping_timestamp(src, self.ts_m).await;
360        Ok(vec![Instruction::SendPingReply { ts_m: self.ts_m, dst: src }])
361    }
362}
363
364impl Ping {
365    pub const TYPENAME: &'static str = "ping";
366
367    /// Create a new Ping with current timestamp (v1.1.7+ simplified format)
368    pub fn new() -> Self {
369        Self { ts_m: get_unix_millis_now() }
370    }
371
372    /// Create Ping with specific timestamp
373    pub fn with_timestamp(ts_m: u64) -> Self {
374        Self { ts_m }
375    }
376}
377
378impl Typename for PingReply {
379    fn typename(&self) -> &'static str {
380        Self::TYPENAME
381    }
382}
383
384#[async_trait::async_trait]
385impl Handle for PingReply {
386    #[instrument(skip(self, ctx), fields(src = %src), name = "PingReply::handle")]
387    async fn handle(&self, ctx: &Context, src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
388        ctx.peers.update_peer_from_pong(src, self).await;
389        Ok(vec![Instruction::Noop { why: "pong processed".to_string() }])
390    }
391}
392
393impl PingReply {
394    pub const TYPENAME: &'static str = "ping_reply";
395
396    pub fn new(ts_m: u64) -> Self {
397        Self { ts_m, seen_time: get_unix_millis_now() }
398    }
399}
400
401#[derive(Debug, serde::Serialize, serde::Deserialize)]
402pub struct EventTx {
403    #[serde(rename = "txus")]
404    pub txs: Vec<TxU>,
405}
406
407impl Typename for EventTx {
408    fn typename(&self) -> &'static str {
409        Self::TYPENAME
410    }
411}
412
413#[async_trait::async_trait]
414impl Handle for EventTx {
415    async fn handle(&self, _ctx: &Context, _src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
416        Ok(vec![Instruction::Noop { why: "event_tx handling not implemented".to_string() }])
417    }
418}
419
420impl EventTx {
421    pub const TYPENAME: &'static str = "event_tx";
422
423    pub fn new(txs: Vec<TxU>) -> Self {
424        Self { txs }
425    }
426}
427
428#[derive(Debug, serde::Serialize, serde::Deserialize)]
429#[allow(non_snake_case)]
430pub struct GetPeerAnrs {
431    pub hasPeersb3f4: Vec<B3f4>,
432}
433
434impl Typename for GetPeerAnrs {
435    fn typename(&self) -> &'static str {
436        Self::TYPENAME
437    }
438}
439
440#[async_trait::async_trait]
441impl Handle for GetPeerAnrs {
442    async fn handle(&self, ctx: &Context, src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
443        let anrs = ctx.anrs.get_all_excluding_b3f4(&self.hasPeersb3f4).await;
444
445        Ok(vec![Instruction::SendGetPeerAnrsReply { anrs, dst: src }])
446    }
447}
448
449impl GetPeerAnrs {
450    pub const TYPENAME: &'static str = "get_peer_anrs";
451
452    pub fn new(has_peers_b3f4: Vec<B3f4>) -> Self {
453        Self { hasPeersb3f4: has_peers_b3f4 }
454    }
455}
456
457#[derive(Debug, serde::Serialize, serde::Deserialize)]
458pub struct GetPeerAnrsReply {
459    pub anrs: Vec<Anr>,
460}
461
462impl Typename for GetPeerAnrsReply {
463    fn typename(&self) -> &'static str {
464        Self::TYPENAME
465    }
466}
467
468#[async_trait::async_trait]
469impl Handle for GetPeerAnrsReply {
470    async fn handle(&self, ctx: &Context, _src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
471        for anr in &self.anrs {
472            ctx.anrs.insert(anr.clone()).await;
473        }
474        Ok(vec![Instruction::Noop { why: format!("inserted {} anrs", self.anrs.len()) }])
475    }
476}
477
478impl GetPeerAnrsReply {
479    pub const TYPENAME: &'static str = "get_peer_anrs_reply";
480
481    pub fn new(anrs: Vec<Anr>) -> Self {
482        Self { anrs }
483    }
484}
485
486#[derive(Debug, serde::Serialize, serde::Deserialize)]
487pub struct NewPhoneWhoDis {}
488
489impl Typename for NewPhoneWhoDis {
490    fn typename(&self) -> &'static str {
491        Self::TYPENAME
492    }
493}
494
495#[async_trait::async_trait]
496impl Handle for NewPhoneWhoDis {
497    #[instrument(skip_all)]
498    async fn handle(&self, _ctx: &Context, src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
499        // v1.1.7+ simplified - respond with NewPhoneWhoDisReply
500        Ok(vec![Instruction::SendNewPhoneWhoDisReply { dst: src }])
501    }
502}
503
504impl NewPhoneWhoDis {
505    pub const TYPENAME: &'static str = "new_phone_who_dis";
506
507    /// Create new NewPhoneWhoDis message (v1.1.7+ simplified)
508    pub fn new() -> Self {
509        Self {}
510    }
511}
512
513#[derive(Debug, serde::Serialize, serde::Deserialize)]
514pub struct NewPhoneWhoDisReply {
515    pub anr: Anr,
516}
517
518impl Typename for NewPhoneWhoDisReply {
519    fn typename(&self) -> &'static str {
520        Self::TYPENAME
521    }
522}
523
524#[async_trait::async_trait]
525impl Handle for NewPhoneWhoDisReply {
526    #[instrument(skip(self, ctx), fields(src = %src), name = "NewPhoneWhoDisReply::handle")]
527    async fn handle(&self, ctx: &Context, src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
528        // SECURITY: ip address spoofing protection
529        if src != self.anr.ip4 {
530            return Err(Error::ParseError("anr_ip_mismatch"));
531        }
532
533        let now_s = crate::utils::misc::get_unix_secs_now();
534        let age_secs = now_s.saturating_sub(self.anr.ts);
535        if age_secs > 60 {
536            return Err(Error::ParseError("anr_too_old"));
537        }
538
539        ctx.anrs.insert(self.anr.clone()).await;
540        ctx.anrs.set_handshaked(self.anr.pk.as_ref()).await;
541        ctx.update_peer_from_anr(src, &self.anr.pk, &self.anr.version, Some(HandshakeStatus::Completed)).await;
542
543        Ok(vec![Instruction::Noop { why: "handshake completed".to_string() }])
544    }
545}
546
547impl NewPhoneWhoDisReply {
548    pub const TYPENAME: &'static str = "new_phone_who_dis_reply";
549
550    pub fn new(anr: Anr) -> Self {
551        Self { anr }
552    }
553}
554
555/// Application-specific payload for special business operations.
556/// Used for operations like slash_trainer_tx, slash_trainer_entry.
557#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
558pub struct SpecialBusiness {
559    #[serde(with = "serde_bytes")]
560    pub business: Vec<u8>,
561}
562
563impl SpecialBusiness {
564    pub const TYPENAME: &'static str = "special_business";
565
566    pub fn new(business: Vec<u8>) -> Self {
567        Self { business }
568    }
569}
570
571impl Typename for SpecialBusiness {
572    fn typename(&self) -> &'static str {
573        Self::TYPENAME
574    }
575}
576
577#[async_trait::async_trait]
578impl Handle for SpecialBusiness {
579    async fn handle(&self, _ctx: &Context, _src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
580        Ok(vec![Instruction::SpecialBusiness { business: self.business.clone() }])
581    }
582}
583
584/// Reply to a special business request.
585#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
586pub struct SpecialBusinessReply {
587    #[serde(with = "serde_bytes")]
588    pub business: Vec<u8>,
589}
590
591impl SpecialBusinessReply {
592    pub const TYPENAME: &'static str = "special_business_reply";
593
594    pub fn new(business: Vec<u8>) -> Self {
595        Self { business }
596    }
597}
598
599impl Typename for SpecialBusinessReply {
600    fn typename(&self) -> &'static str {
601        Self::TYPENAME
602    }
603}
604
605#[async_trait::async_trait]
606impl Handle for SpecialBusinessReply {
607    async fn handle(&self, _ctx: &Context, _src: Ipv4Addr) -> Result<Vec<Instruction>, Error> {
608        Ok(vec![Instruction::SpecialBusinessReply { business: self.business.clone() }])
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use crate::consensus::doms::entry::{Entry, EntryHeader, EntrySummary};
616    use crate::consensus::doms::tx::{EntryTx, EntryTxAction, EntryTxInner, Tx, TxAction, TxU};
617    use crate::node::anr::Anr;
618    use crate::utils::bls12_381;
619    use amadeus_utils::version::Ver;
620    use std::net::Ipv4Addr;
621
622    fn dummy_anr() -> Anr {
623        let sk = bls12_381::generate_sk();
624        let pk = bls12_381::get_public_key(&sk).unwrap();
625        let pop = bls12_381::sign(&sk, pk.as_ref(), crate::consensus::DST_POP).unwrap();
626        Anr::build(&sk, &pk, pop.as_ref(), Ipv4Addr::new(127, 0, 0, 1), Ver::new(1, 0, 0)).unwrap()
627    }
628
629    fn dummy_entry_summary() -> EntrySummary {
630        EntrySummary {
631            header: EntryHeader {
632                height: 1,
633                slot: 1,
634                prev_slot: 0,
635                prev_hash: Hash::new([0; 32]),
636                dr: Hash::new([1; 32]),
637                vr: Signature::new([2; 96]),
638                signer: PublicKey::new([3; 48]),
639                root_tx: Hash::new([4; 32]),
640                root_validator: Hash::new([5; 32]),
641            },
642            signature: Signature::new([6; 96]),
643            mask: None,
644        }
645    }
646
647    fn dummy_entry() -> Entry {
648        Entry {
649            hash: Hash::new([10; 32]),
650            header: EntryHeader {
651                height: 1,
652                slot: 1,
653                prev_slot: 0,
654                prev_hash: Hash::new([0; 32]),
655                dr: Hash::new([1; 32]),
656                vr: Signature::new([2; 96]),
657                signer: PublicKey::new([3; 48]),
658                root_tx: Hash::new([4; 32]),
659                root_validator: Hash::new([5; 32]),
660            },
661            signature: Signature::new([6; 96]),
662            mask: None,
663            txs: vec![EntryTx {
664                hash: Hash::new([20; 32]),
665                signature: Signature::new([21; 96]),
666                tx: EntryTxInner {
667                    action: EntryTxAction {
668                        args: vec![],
669                        contract: b"C".to_vec(),
670                        function: b"f".to_vec(),
671                        op: "call".into(),
672                        attached_symbol: None,
673                        attached_amount: None,
674                    },
675                    nonce: 1,
676                    signer: PublicKey::new([22; 48]),
677                },
678            }],
679        }
680    }
681
682    fn dummy_txu() -> TxU {
683        TxU {
684            hash: Hash::new([30; 32]),
685            signature: Signature::new([31; 96]),
686            tx: Tx {
687                action: TxAction {
688                    args: vec![],
689                    contract: b"C".to_vec(),
690                    function: b"f".to_vec(),
691                    op: "call".into(),
692                    attached_symbol: None,
693                    attached_amount: None,
694                },
695                nonce: 1,
696                signer: PublicKey::new([32; 48]),
697            },
698        }
699    }
700
701    fn dummy_attestation() -> Attestation {
702        Attestation {
703            entry_hash: Hash::new([40; 32]),
704            mutations_hash: Hash::new([41; 32]),
705            signer: PublicKey::new([42; 48]),
706            signature: Signature::new([43; 96]),
707        }
708    }
709
710    fn roundtrip<T: serde::Serialize + for<'de> serde::Deserialize<'de>>(msg: &T) -> T {
711        let bin = vecpak::to_vec(msg).expect("serialize");
712        vecpak::from_slice(&bin).expect("deserialize")
713    }
714
715    #[test]
716    fn ping_roundtrip() {
717        let msg = Protocol::Ping(Ping::with_timestamp(123456));
718        let rt = roundtrip(&msg);
719        assert_eq!(rt.typename(), "ping");
720    }
721
722    #[test]
723    fn ping_reply_roundtrip() {
724        let msg = Protocol::PingReply(PingReply { ts_m: 123456, seen_time: 0 });
725        let rt = roundtrip(&msg);
726        assert_eq!(rt.typename(), "ping_reply");
727    }
728
729    #[test]
730    fn event_entry_roundtrip() {
731        let msg = Protocol::EventEntry(EventEntry { entry_packed: dummy_entry() });
732        let rt = roundtrip(&msg);
733        assert_eq!(rt.typename(), "event_entry");
734    }
735
736    #[test]
737    fn event_tip_roundtrip() {
738        let msg = Protocol::EventTip(EventTip::new(dummy_entry_summary(), dummy_entry_summary()));
739        let rt = roundtrip(&msg);
740        assert_eq!(rt.typename(), "event_tip");
741    }
742
743    #[test]
744    fn event_attestation_roundtrip() {
745        let msg = Protocol::EventAttestation(EventAttestation::new(vec![dummy_attestation()]));
746        let rt = roundtrip(&msg);
747        assert_eq!(rt.typename(), "event_attestation");
748    }
749
750    #[test]
751    fn event_tx_roundtrip() {
752        let msg = Protocol::EventTx(EventTx::new(vec![dummy_txu()]));
753        let rt = roundtrip(&msg);
754        assert_eq!(rt.typename(), "event_tx");
755    }
756
757    #[test]
758    fn get_peer_anrs_roundtrip() {
759        let msg = Protocol::GetPeerAnrs(GetPeerAnrs::new(vec![[1, 2, 3, 4].into()]));
760        let rt = roundtrip(&msg);
761        assert_eq!(rt.typename(), "get_peer_anrs");
762    }
763
764    #[test]
765    fn get_peer_anrs_reply_roundtrip() {
766        let msg = Protocol::GetPeerAnrsReply(GetPeerAnrsReply::new(vec![dummy_anr()]));
767        let rt = roundtrip(&msg);
768        assert_eq!(rt.typename(), "get_peer_anrs_reply");
769    }
770
771    #[test]
772    fn new_phone_who_dis_roundtrip() {
773        let msg = Protocol::NewPhoneWhoDis(NewPhoneWhoDis::new());
774        let rt = roundtrip(&msg);
775        assert_eq!(rt.typename(), "new_phone_who_dis");
776    }
777
778    #[test]
779    fn new_phone_who_dis_reply_roundtrip() {
780        let msg = Protocol::NewPhoneWhoDisReply(NewPhoneWhoDisReply::new(dummy_anr()));
781        let rt = roundtrip(&msg);
782        assert_eq!(rt.typename(), "new_phone_who_dis_reply");
783    }
784
785    #[test]
786    fn special_business_roundtrip() {
787        let msg = Protocol::SpecialBusiness(SpecialBusiness::new(vec![1, 2, 3, 4, 5]));
788        let rt = roundtrip(&msg);
789        assert_eq!(rt.typename(), "special_business");
790        if let Protocol::SpecialBusiness(sb) = rt {
791            assert_eq!(sb.business, vec![1, 2, 3, 4, 5]);
792        }
793    }
794
795    #[test]
796    fn special_business_reply_roundtrip() {
797        let msg = Protocol::SpecialBusinessReply(SpecialBusinessReply::new(vec![6, 7, 8, 9]));
798        let rt = roundtrip(&msg);
799        assert_eq!(rt.typename(), "special_business_reply");
800        if let Protocol::SpecialBusinessReply(sbr) = rt {
801            assert_eq!(sbr.business, vec![6, 7, 8, 9]);
802        }
803    }
804
805    #[test]
806    fn parse_real_elixir_event_tx() {
807        let packet = hex::decode("0701020501026f700501086576656e745f74780501047478757306010107010305010274780701030501056e6f6e63650308187c02e8527c7cbf050106616374696f6e0701040501026f7005010463616c6c05010461726773060101050204f09e01000060f7e2fb1fbd6e6425d1fdb53a172e548a3369d041508952e0c27ee7bd548963904796524edd0037f341613e24a7bf2c1992044ea66cbc8d8f1ff91b3d61705de2780321cc74bb12c693913edf938ca4b6348ad0d56c1fb45c5d0c573ccc09771dcf89434cba91a977f5d8e7bcddd60220ca849cbf3babb55d515b14e8d248a40f6a515f91174b41fe9296f0cf027bc1d3cb1d8b2e4c5bfee88c3f0c914678ad45ac8fbf59b9cb0f535861ef54c474ad904796524edd0037f341613e24a7bf2c1992044ea66cbc8d8f1ff91b3d61705de2780321cc74bb12c693913edf938ca48566b77b83ee09c66333f2a34deba8ffa9f1aeff6205baffd891e5ffdb1fa8ff4190a6ffe96ef1ffd9cfc9ffb1bdb6ffe72ee5ff41e3eeffad23daff3c69b4ffb042cbff3d3e0d005e3696ffa7119dffd030bcffaaceaeff6f25caff76d5a3ffe40c9bff7291e4ff14fcd0ff2fe5cfffc2b7f5ffc869d2ffce69d6ffa7a0dbff24cfc7ff71ec09008862a3ff4b237dffe554d9ff9e84baff0adcd7ff8d9f9fff48e1b7ffce29daff0236a7ffad19bbffb065fbffe7cac0ffd941f2ff9e1d96ffad5ec7ffd4111300c0078bffd82db8ff8353bcff01dfd8fff139acff79c5afffa4bcb2ff617aeaffbaa4caff3a2ed4ff04730b00e66dc6ff6946cdff29e4abffac5ba3fff9571a008b19aeffde77aaff0987bcffe226b1ffb11cbbff88d1a6ff8ab1d6ff9b3ed8ff8460cdff6841e7ff9d16dbffebc1d3ffb76cfdffffd9c3ff9329c5fffa46f5ff649c99ff78fab1ffcf64bdffc65fc1ff7316c6ffed4eb2fff3a8a1ff8ec9cfff95d5d4ff8b66c0ffc1aff5ff889ac2ff87f3f5ff49fab0ff2a3db6ffebaf130008d88bffc1ab91ff5d00cdffd864a0ff06edceff66decbff8181c6ffda9dbcff2826ccffbcb2c0ff27c5e0ff294edaff5b8bd9ff07c998ff4571b6ff9fc8160069eeb5ffad93b2ff80a7bbffbae9c2ff65ecbdffa591a4ff2683c8ffe9c5d8ffc969b6ff1ba6c5ff19400e00830bc9ff5857bcff0cb89bffe8a4b4ffdeefffff547badfffd76b1ffeed4b0ffbc44cbff0c3ebfff0cb9b5ff08c9a8ff97b4caffc11de3ff2173a7fff744eaffdbd1f1ff3ab3dfffaefc96ffeca3abff8aba2100f8e084ff90cabeffdb0bbaffca18d1ffaff6c0ff45a38dffe05aaeff3db7cfff87c9bcffbabdc0ffb81be9ffeb1bcdff7741d5ff6ef9b2ffba33b6ff5b1a02006593a9ff4398b4ff666bb3ffe42fcbff7fceb9ffa179c3ffaaadd4ffa48efbff0578c6ffe541c9ff43dff8ff257498ff0d88dbff49758affb3c7c1ff8f7df9ff1ff587ff0842d6ffe954b9fff39fc8ffbd55a3ff2ea1a2ffa28bc5ff874bdcffa06ddcff9b74b6ff1976e6ffa2a8b8ff1d27d9ff31ffc3ff8603bbff760b270029349dff40c8a4ff27b8c5ff97faa1ff7087c9ff66cfa3ff46bdb2ffd404e8ff387dc5ff0512d7ffe955f5ffcd6bc6ffc777d9fff0439aff0034b0ffc1ad5100f9b4adff79379eff7e5eb9ff444096ffb8f0d2ff6f4cc5ffe4cbd7ff2abdf1ff8ccec0ff152cc7ffa7ccf4fff186ceff7a99d3ff608e94ffc9dfd4ff1a9b1c00a5f4bfff21dda5ff21b5ceff0d1ba6ff6388edff336fa7ffdd29b8ff41a8d5ffaccdc5ffbfdcc3ff64cff1ffdad7c2ff79afdfff699c9dfff1b2d1ff0014460022f399ffc803a5ff1a1396ff7df5ceff95eec6ff182eadff834ab1ff8de1d6ff8af2c1ffee38c9ff4a90eaff1b29c0ff6390cbff724aa8ffa417adffae762900ca90a2ff050108636f6e747261637405010545706f636805010866756e6374696f6e05010a7375626d69745f736f6c0501067369676e657205013098b033f3c88d92c3ed617e06b87ff452bebc505fcdcc094f5f673433817825801ba0eaa8319f05804ed4728c9809d3380501046861736805012092a51d6e44c6e8b662d2870c4a8a9efd99f026de98c6a3a67f1652a79fa2d6b20501097369676e6174757265050160ad01c361a5845bf5772540c3719bd7088a742c4905271bb89edbd3808accf0e2a2186d300370d42f7da17cddab2ddb310517521e67a74438d25cf549b46e51d385f32cafe2b8e8a4f3c71dd384f7456f41c0d83556205890ae5522cd2a18c45f").unwrap();
808        let parsed: Protocol = vecpak::from_slice(&packet).expect("should parse real elixir event_tx");
809        assert_eq!(parsed.typename(), "event_tx");
810        assert_eq!(packet, vecpak::to_vec(&parsed).expect("should encode real elixir event_tx"));
811    }
812
813    #[test]
814    fn handshake_compatibility() {
815        let p_hex = "0701010501026f700501116e65775f70686f6e655f77686f5f646973";
816        let p_bytes = hex::decode(p_hex).expect("valid hex");
817        let npwd: Protocol = amadeus_utils::vecpak::from_slice(&p_bytes).unwrap();
818        let rt_bytes = amadeus_utils::vecpak::to_vec(&npwd).unwrap();
819        assert_eq!(rt_bytes, p_bytes);
820
821        let p_hex = "0701020501026f700501176e65775f70686f6e655f77686f5f6469735f7265706c79050103616e72070107050102706b050130a9e81ed8c8eaaebd8dd53a889d8c5a8612ab7330275a5d39043e95200e7c1b66f0dc00c5307e867a55a9ad9e7ae4b9f005010274730304692634f205010369703405010c37322e392e3134342e313130050103706f70050160b62a96d62af0d2d7006ab560c64bde562df13ae642380a31d935276412c59f9944dceaa4060903e4ead197e97ad1654910be87ac556a5063e1d68df542aab1a3f75df3eab891a7cab572ba7170716c5487183ef28ef89f7c7555be2bb1d41218050104706f72740302906905010776657273696f6e050105312e332e300501097369676e6174757265050160b62d43994fa7614138d205ecefeb1677d4998574aac1db8fdd5673de4e1d2ae8391c4cf703007ce37778e20624650143068c59596b5838536ecfd05a0d0805b0baa04dcae97caf9f199232fbfff462ebb35bfc653576af43007ba9666a2952a7";
822        let p_bytes = hex::decode(p_hex).expect("valid hex");
823        let npwdr: Protocol = amadeus_utils::vecpak::from_slice(&p_bytes).unwrap();
824        let rt_bytes = amadeus_utils::vecpak::to_vec(&npwdr).unwrap();
825        assert_eq!(rt_bytes, p_bytes);
826    }
827}