amadeus_node/consensus/doms/
attestation.rs

1use crate::Context;
2use crate::consensus::agg_sig::DST_ATT;
3use crate::node::protocol;
4use crate::node::protocol::Protocol;
5use crate::utils::bls12_381 as bls;
6use crate::utils::bls12_381::Error as BlsError;
7use crate::utils::misc::{TermExt, TermMap};
8use crate::utils::safe_etf::encode_safe;
9use eetf::DecodeError as EtfDecodeError;
10use eetf::EncodeError as EtfEncodeError;
11use eetf::{Atom, Binary, Term};
12use std::collections::HashMap;
13use std::fmt::Debug;
14use std::net::Ipv4Addr;
15use tracing::{instrument, warn};
16
17#[derive(Debug, thiserror::Error)]
18pub enum Error {
19    #[error("wrong type: {0}")]
20    WrongType(&'static str),
21    #[error("missing field: {0}")]
22    Missing(&'static str),
23    #[error("too large")]
24    TooLarge,
25    #[error("not deterministically encoded")]
26    NotDeterministic,
27    #[error("invalid length: {0}")]
28    InvalidLength(&'static str),
29    #[error(transparent)]
30    EtfDecode(#[from] EtfDecodeError),
31    #[error(transparent)]
32    EtfEncode(#[from] EtfEncodeError),
33    #[error(transparent)]
34    Bls(#[from] BlsError),
35}
36
37#[derive(Debug, Clone)]
38pub struct EventAttestation {
39    pub attestation: Attestation,
40}
41
42#[derive(Clone)]
43pub struct Attestation {
44    pub entry_hash: [u8; 32],
45    pub mutations_hash: [u8; 32],
46    pub signer: [u8; 48],
47    pub signature: [u8; 96],
48}
49
50impl Debug for Attestation {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("Attestation")
53            .field("entry_hash", &bs58::encode(self.entry_hash).into_string())
54            .field("mutations_hash", &bs58::encode(self.mutations_hash).into_string())
55            .field("signer", &bs58::encode(self.signer).into_string())
56            .finish()
57    }
58}
59
60impl crate::utils::misc::Typename for EventAttestation {
61    fn typename(&self) -> &'static str {
62        Self::TYPENAME
63    }
64}
65
66#[async_trait::async_trait]
67impl Protocol for EventAttestation {
68    #[instrument(skip(map), name = "EventAttestation::from_etf_map_validated")]
69    fn from_etf_map_validated(map: TermMap) -> Result<Self, protocol::Error> {
70        let bin = map.get_binary("attestation_packed").ok_or(Error::Missing("attestations_packed"))?;
71        let attestation = Attestation::from_etf_bin(bin)?;
72
73        Ok(Self { attestation })
74    }
75
76    fn to_etf_bin(&self) -> Result<Vec<u8>, protocol::Error> {
77        let attestation = self.attestation.to_etf_bin()?;
78        let mut m = HashMap::new();
79        m.insert(Term::Atom(Atom::from("op")), Term::Atom(Atom::from(Self::TYPENAME)));
80        m.insert(Term::Atom(Atom::from("attestation_packed")), Term::from(Binary { bytes: attestation }));
81        let term = Term::from(eetf::Map { map: m });
82        let etf_data = encode_safe(&term);
83        Ok(etf_data)
84    }
85
86    #[instrument(skip(self, _ctx), name = "EventAttestation::handle", err)]
87    async fn handle(&self, _ctx: &Context, _src: Ipv4Addr) -> Result<Vec<protocol::Instruction>, protocol::Error> {
88        // TODO: handle the event_attestation
89        Ok(vec![protocol::Instruction::Noop { why: "event_attestation handling not implemented".to_string() }])
90    }
91}
92
93impl EventAttestation {
94    pub const TYPENAME: &'static str = "event_attestation";
95}
96
97impl Attestation {
98    #[instrument(skip(bin), name = "Attestation::from_etf_bin", err)]
99    pub fn from_etf_bin(bin: &[u8]) -> Result<Self, Error> {
100        let term = Term::decode(bin)?;
101        let map = match term {
102            Term::Map(m) => m.map,
103            _ => return Err(Error::WrongType("attestation map")),
104        };
105        let entry_hash_v = map
106            .get(&Term::Atom(Atom::from("entry_hash")))
107            .and_then(|t| t.get_binary())
108            .map(|b| b.to_vec())
109            .ok_or(Error::Missing("entry_hash"))?;
110        let mutations_hash_v = map
111            .get(&Term::Atom(Atom::from("mutations_hash")))
112            .and_then(|t| t.get_binary())
113            .map(|b| b.to_vec())
114            .ok_or(Error::Missing("mutations_hash"))?;
115        let signer_v = map
116            .get(&Term::Atom(Atom::from("signer")))
117            .and_then(|t| t.get_binary())
118            .map(|b| b.to_vec())
119            .ok_or(Error::Missing("signer"))?;
120        let signature_v = map
121            .get(&Term::Atom(Atom::from("signature")))
122            .and_then(|t| t.get_binary())
123            .map(|b| b.to_vec())
124            .ok_or(Error::Missing("signature"))?;
125
126        Ok(Attestation {
127            entry_hash: entry_hash_v.try_into().map_err(|_| Error::InvalidLength("entry_hash"))?,
128            mutations_hash: mutations_hash_v.try_into().map_err(|_| Error::InvalidLength("mutations_hash"))?,
129            signer: signer_v.try_into().map_err(|_| Error::InvalidLength("signer"))?,
130            signature: signature_v.try_into().map_err(|_| Error::InvalidLength("signature"))?,
131        })
132    }
133    /// Encode into an ETF map with deterministic field set
134    #[instrument(skip(self), name = "Attestation::to_etf_bin", err)]
135    pub fn to_etf_bin(&self) -> Result<Vec<u8>, Error> {
136        let mut m = HashMap::new();
137        m.insert(Term::Atom(Atom::from("entry_hash")), Term::from(Binary { bytes: self.entry_hash.to_vec() }));
138        m.insert(Term::Atom(Atom::from("mutations_hash")), Term::from(Binary { bytes: self.mutations_hash.to_vec() }));
139        m.insert(Term::Atom(Atom::from("signer")), Term::from(Binary { bytes: self.signer.to_vec() }));
140        m.insert(Term::Atom(Atom::from("signature")), Term::from(Binary { bytes: self.signature.to_vec() }));
141        let term = Term::from(eetf::Map { map: m });
142        let out = encode_safe(&term);
143        Ok(out)
144    }
145
146    /// Validate sizes and signature with DST_ATT
147    #[instrument(skip(self), name = "Attestation::validate", err)]
148    pub fn validate(&self) -> Result<(), Error> {
149        let mut to_sign = [0u8; 64];
150        to_sign[..32].copy_from_slice(&self.entry_hash);
151        to_sign[32..].copy_from_slice(&self.mutations_hash);
152        bls::verify(&self.signer, &self.signature, &to_sign, DST_ATT)?;
153        Ok(())
154    }
155
156    /// Verify this attestation against an allowed set of trainers (public keys)
157    /// Returns Ok(()) only if signer is present in `trainers` and signature is valid
158    pub fn validate_vs_trainers<TPk>(&self, trainers: &[TPk]) -> Result<(), Error>
159    where
160        TPk: AsRef<[u8]>,
161    {
162        let is_allowed = trainers.iter().any(|pk| pk.as_ref() == self.signer);
163        if !is_allowed {
164            return Err(Error::WrongType("signer_not_trainer"));
165        }
166        self.validate()
167    }
168
169    /// Create an attestation from provided public/secret material
170    /// NOTE: we intentionally do not read global env here, caller supplies keys
171    pub fn sign_with(
172        pk_g1_48: &[u8],
173        trainer_sk: &[u8],
174        entry_hash: &[u8; 32],
175        mutations_hash: &[u8; 32],
176    ) -> Result<Self, Error> {
177        let mut msg = [0u8; 64];
178        msg[..32].copy_from_slice(entry_hash);
179        msg[32..].copy_from_slice(mutations_hash);
180        let signature = bls::sign(trainer_sk, &msg, DST_ATT)?;
181        let signer: [u8; 48] = pk_g1_48.try_into().map_err(|_| Error::InvalidLength("signer"))?;
182        let signature: [u8; 96] = signature.as_slice().try_into().map_err(|_| Error::InvalidLength("signature"))?;
183        Ok(Self { entry_hash: *entry_hash, mutations_hash: *mutations_hash, signer, signature })
184    }
185}