Skip to main content

guardian_shared/
lib.rs

1use base64::Engine;
2use miden_protocol::account::Account;
3use miden_protocol::account::auth::Signature as AccountSignature;
4use miden_protocol::crypto::dsa::ecdsa_k256_keccak;
5use miden_protocol::crypto::dsa::falcon512_rpo::Signature as FalconSignature;
6use miden_protocol::transaction::TransactionSummary;
7use miden_protocol::utils::serde::{Deserializable, Serializable};
8use miden_protocol::{Felt, Hasher, Word};
9use serde::{Deserialize, Serialize};
10
11pub mod auth;
12pub mod auth_request_message;
13pub mod auth_request_payload;
14pub mod hex;
15
16use crate::hex::FromHex;
17
18/// Supported signature schemes
19#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum SignatureScheme {
22    Falcon,
23    Ecdsa,
24}
25
26impl SignatureScheme {
27    pub fn from(ack_scheme: &str) -> Result<Self, String> {
28        match ack_scheme {
29            value if value.eq_ignore_ascii_case("falcon") => Ok(Self::Falcon),
30            value if value.eq_ignore_ascii_case("ecdsa") => Ok(Self::Ecdsa),
31            value => Err(format!("unsupported signature scheme: {}", value)),
32        }
33    }
34
35    pub const fn as_str(self) -> &'static str {
36        match self {
37            Self::Falcon => "falcon",
38            Self::Ecdsa => "ecdsa",
39        }
40    }
41
42    pub fn parse_signature_hex(self, signature_hex: &str) -> Result<AccountSignature, String> {
43        match self {
44            Self::Falcon => {
45                let signature = FalconSignature::from_hex(&ensure_hex_prefix(signature_hex))
46                    .map_err(|e| format!("failed to parse Falcon signature: {}", e))?;
47                Ok(AccountSignature::from(signature))
48            }
49            Self::Ecdsa => {
50                let signature_bytes = ::hex::decode(signature_hex.trim_start_matches("0x"))
51                    .map_err(|e| format!("invalid ECDSA signature hex: {}", e))?;
52                let signature = ecdsa_k256_keccak::Signature::read_from_bytes(&signature_bytes)
53                    .map_err(|e| format!("failed to parse ECDSA signature: {}", e))?;
54                Ok(AccountSignature::EcdsaK256Keccak(signature))
55            }
56        }
57    }
58
59    pub fn build_signature_advice_entry(
60        self,
61        pubkey_commitment: Word,
62        message: Word,
63        signature: &AccountSignature,
64        public_key_hex: Option<&str>,
65    ) -> Result<(Word, Vec<Felt>), String> {
66        let key = signature_advice_key(pubkey_commitment, message);
67
68        let values = match (self, signature) {
69            (Self::Falcon, AccountSignature::Falcon512Rpo(_)) => {
70                signature.to_prepared_signature(message)
71            }
72            (Self::Falcon, _) => {
73                return Err("expected Falcon signature for falcon scheme".to_string());
74            }
75            (Self::Ecdsa, AccountSignature::EcdsaK256Keccak(ecdsa_signature)) => {
76                let public_key_hex = public_key_hex.ok_or_else(|| {
77                    "ECDSA signature requires public key for advice preparation".to_string()
78                })?;
79                let public_key = parse_ecdsa_public_key_hex(public_key_hex)?;
80                let mut encoded = encode_ecdsa_signature_felts(&public_key, ecdsa_signature);
81                encoded.reverse();
82                encoded
83            }
84            (Self::Ecdsa, _) => {
85                return Err("expected ECDSA signature for ecdsa scheme".to_string());
86            }
87        };
88
89        Ok((key, values))
90    }
91}
92
93impl std::fmt::Display for SignatureScheme {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.write_str(self.as_str())
96    }
97}
98
99fn ensure_hex_prefix(hex: &str) -> String {
100    if hex.starts_with("0x") {
101        hex.to_string()
102    } else {
103        format!("0x{}", hex)
104    }
105}
106
107fn signature_advice_key(pubkey_commitment: Word, message: Word) -> Word {
108    let mut elements = Vec::with_capacity(8);
109    elements.extend_from_slice(pubkey_commitment.as_elements());
110    elements.extend_from_slice(message.as_elements());
111    Hasher::hash_elements(&elements)
112}
113
114fn parse_ecdsa_public_key_hex(
115    public_key_hex: &str,
116) -> Result<ecdsa_k256_keccak::PublicKey, String> {
117    let public_key_bytes = ::hex::decode(public_key_hex.trim_start_matches("0x"))
118        .map_err(|e| format!("invalid ECDSA public key hex: {}", e))?;
119    ecdsa_k256_keccak::PublicKey::read_from_bytes(&public_key_bytes)
120        .map_err(|e| format!("failed to deserialize ECDSA public key: {}", e))
121}
122
123fn bytes_to_packed_u32_felts(bytes: &[u8]) -> Vec<Felt> {
124    bytes
125        .chunks(4)
126        .map(|chunk| {
127            let mut packed = [0u8; 4];
128            packed[..chunk.len()].copy_from_slice(chunk);
129            Felt::from(u32::from_le_bytes(packed))
130        })
131        .collect()
132}
133
134fn encode_ecdsa_signature_felts(
135    public_key: &ecdsa_k256_keccak::PublicKey,
136    signature: &ecdsa_k256_keccak::Signature,
137) -> Vec<Felt> {
138    let mut out = Vec::new();
139    out.extend(bytes_to_packed_u32_felts(&public_key.to_bytes()));
140    out.extend(bytes_to_packed_u32_felts(&signature.to_bytes()));
141    out
142}
143
144/// Signature type for delta proposals
145#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
146#[serde(tag = "scheme", rename_all = "snake_case")]
147pub enum ProposalSignature {
148    Falcon {
149        /// Hex-encoded Falcon signature
150        signature: String,
151    },
152    Ecdsa {
153        /// Hex-encoded ECDSA secp256k1 signature
154        signature: String,
155        /// Hex-encoded ECDSA public key (required for signature preparation)
156        #[serde(default, skip_serializing_if = "Option::is_none")]
157        public_key: Option<String>,
158    },
159}
160
161impl ProposalSignature {
162    /// Creates a ProposalSignature from a scheme, hex-encoded signature, and optional public key.
163    pub fn from_scheme(
164        scheme: SignatureScheme,
165        signature: String,
166        public_key: Option<String>,
167    ) -> Self {
168        match scheme {
169            SignatureScheme::Falcon => ProposalSignature::Falcon { signature },
170            SignatureScheme::Ecdsa => ProposalSignature::Ecdsa {
171                signature,
172                public_key,
173            },
174        }
175    }
176
177    /// Returns the public key hex if this is an ECDSA signature with a public key.
178    pub fn public_key(&self) -> Option<&str> {
179        match self {
180            ProposalSignature::Ecdsa { public_key, .. } => public_key.as_deref(),
181            _ => None,
182        }
183    }
184}
185
186/// Delta payload structure containing transaction summary and signatures
187/// This is the standard format for delta_payload in proposals
188#[derive(Serialize, Deserialize, Clone, Debug)]
189pub struct DeltaPayload {
190    pub tx_summary: serde_json::Value,
191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
192    pub signatures: Vec<DeltaSignature>,
193}
194
195impl DeltaPayload {
196    pub fn new(tx_summary: serde_json::Value) -> Self {
197        Self {
198            tx_summary,
199            signatures: Vec::new(),
200        }
201    }
202
203    pub fn with_signature(mut self, signature: DeltaSignature) -> Self {
204        self.signatures.push(signature);
205        self
206    }
207
208    pub fn to_json(&self) -> serde_json::Value {
209        serde_json::to_value(self).expect("DeltaPayload should always serialize")
210    }
211}
212
213/// Signature entry in delta payload
214#[derive(Serialize, Deserialize, Clone, Debug)]
215pub struct DeltaSignature {
216    pub signer_id: String,
217    pub signature: ProposalSignature,
218}
219
220pub trait ToJson {
221    fn to_json(&self) -> serde_json::Value;
222}
223
224pub trait FromJson: Sized {
225    fn from_json(json: &serde_json::Value) -> Result<Self, String>;
226}
227
228impl ToJson for Account {
229    fn to_json(&self) -> serde_json::Value {
230        let bytes = self.to_bytes();
231        let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
232        serde_json::json!({
233          "data": encoded,
234          "account_id": self.id().to_hex(),
235        })
236    }
237}
238
239impl FromJson for Account {
240    fn from_json(json: &serde_json::Value) -> Result<Self, String> {
241        let encoded = json
242            .get("data")
243            .and_then(|v| v.as_str())
244            .ok_or("Missing or invalid 'data' field")?;
245
246        let bytes = base64::engine::general_purpose::STANDARD
247            .decode(encoded)
248            .map_err(|e| format!("Base64 decode error: {e}"))?;
249
250        Account::read_from_bytes(&bytes).map_err(|e| format!("Deserialization error: {e}"))
251    }
252}
253
254impl ToJson for TransactionSummary {
255    fn to_json(&self) -> serde_json::Value {
256        let bytes = self.to_bytes();
257        let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
258        serde_json::json!({
259          "data": encoded,
260        })
261    }
262}
263
264impl FromJson for TransactionSummary {
265    fn from_json(json: &serde_json::Value) -> Result<Self, String> {
266        let encoded = json
267            .get("data")
268            .and_then(|v| v.as_str())
269            .ok_or("Missing or invalid 'data' field in delta payload")?;
270
271        let bytes = base64::engine::general_purpose::STANDARD
272            .decode(encoded)
273            .map_err(|e| format!("Base64 decode error: {e}"))?;
274
275        TransactionSummary::read_from_bytes(&bytes)
276            .map_err(|e| format!("AccountDelta deserialization error: {e}"))
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use miden_protocol::{
284        account::auth::Signature as AccountSignature,
285        account::{AccountBuilder, auth::PublicKeyCommitment},
286        crypto::dsa::ecdsa_k256_keccak::SecretKey as EcdsaSecretKey,
287        crypto::dsa::falcon512_rpo::SecretKey,
288    };
289    use miden_standards::account::{auth::AuthFalcon512Rpo, wallets::BasicWallet};
290
291    #[test]
292    fn test_account_json_round_trip() {
293        // Create a test account
294        let secret_key = SecretKey::new();
295        let public_key_commitment =
296            PublicKeyCommitment::from(secret_key.public_key().to_commitment());
297        let account = AccountBuilder::new([0xff; 32])
298            .with_auth_component(AuthFalcon512Rpo::new(public_key_commitment))
299            .with_component(BasicWallet)
300            .build()
301            .unwrap();
302
303        // Serialize to JSON
304        let json = account.to_json();
305
306        // Deserialize from JSON
307        let deserialized_account =
308            Account::from_json(&json).expect("Failed to deserialize account");
309
310        // Verify round-trip
311        assert_eq!(account.id(), deserialized_account.id());
312        assert_eq!(account.nonce(), deserialized_account.nonce());
313        assert_eq!(account.commitment(), deserialized_account.commitment());
314        assert_eq!(
315            account.storage().to_commitment(),
316            deserialized_account.storage().to_commitment()
317        );
318        assert_eq!(
319            account.code().commitment(),
320            deserialized_account.code().commitment()
321        );
322    }
323
324    #[test]
325    fn signature_scheme_from_accepts_known_values_case_insensitively() {
326        assert_eq!(
327            SignatureScheme::from("falcon").unwrap(),
328            SignatureScheme::Falcon
329        );
330        assert_eq!(
331            SignatureScheme::from("ECDSA").unwrap(),
332            SignatureScheme::Ecdsa
333        );
334    }
335
336    #[test]
337    fn signature_scheme_from_rejects_unknown_values() {
338        let error = SignatureScheme::from("unknown").unwrap_err();
339
340        assert!(error.contains("unsupported signature scheme"));
341    }
342
343    #[test]
344    fn signature_scheme_parse_signature_hex_accepts_falcon_signatures() {
345        let secret_key = SecretKey::new();
346        let message = Word::from([1u32, 2, 3, 4]);
347        let signature = secret_key.sign(message);
348        let signature_hex = format!("0x{}", ::hex::encode(signature.to_bytes()));
349
350        let parsed = SignatureScheme::Falcon
351            .parse_signature_hex(&signature_hex)
352            .unwrap();
353
354        assert!(matches!(parsed, AccountSignature::Falcon512Rpo(_)));
355    }
356
357    #[test]
358    fn signature_scheme_parse_signature_hex_accepts_ecdsa_signatures() {
359        let secret_key = EcdsaSecretKey::new();
360        let message = Word::from([1u32, 2, 3, 4]);
361        let signature = secret_key.sign(message);
362        let signature_hex = format!("0x{}", ::hex::encode(signature.to_bytes()));
363
364        let parsed = SignatureScheme::Ecdsa
365            .parse_signature_hex(&signature_hex)
366            .unwrap();
367
368        assert!(matches!(parsed, AccountSignature::EcdsaK256Keccak(_)));
369    }
370
371    #[test]
372    fn signature_scheme_build_signature_advice_entry_accepts_falcon_signatures() {
373        let secret_key = SecretKey::new();
374        let message = Word::from([1u32, 2, 3, 4]);
375        let commitment = Word::from([5u32, 6, 7, 8]);
376        let signature = AccountSignature::from(secret_key.sign(message));
377
378        let (key, values) = SignatureScheme::Falcon
379            .build_signature_advice_entry(commitment, message, &signature, None)
380            .unwrap();
381
382        let mut elements = Vec::with_capacity(8);
383        elements.extend_from_slice(commitment.as_elements());
384        elements.extend_from_slice(message.as_elements());
385
386        assert_eq!(key, Hasher::hash_elements(&elements));
387        assert!(!values.is_empty());
388    }
389
390    #[test]
391    fn signature_scheme_build_signature_advice_entry_accepts_ecdsa_signatures() {
392        let secret_key = EcdsaSecretKey::new();
393        let public_key = secret_key.public_key();
394        let public_key_hex = format!("0x{}", ::hex::encode(public_key.to_bytes()));
395        let message = Word::from([1u32, 2, 3, 4]);
396        let commitment = public_key.to_commitment();
397        let signature = AccountSignature::EcdsaK256Keccak(secret_key.sign(message));
398
399        let (key, values) = SignatureScheme::Ecdsa
400            .build_signature_advice_entry(commitment, message, &signature, Some(&public_key_hex))
401            .unwrap();
402
403        let mut elements = Vec::with_capacity(8);
404        elements.extend_from_slice(commitment.as_elements());
405        elements.extend_from_slice(message.as_elements());
406
407        assert_eq!(key, Hasher::hash_elements(&elements));
408        assert!(!values.is_empty());
409    }
410
411    #[test]
412    fn signature_scheme_build_signature_advice_entry_requires_ecdsa_public_key() {
413        let secret_key = EcdsaSecretKey::new();
414        let message = Word::from([1u32, 2, 3, 4]);
415        let commitment = secret_key.public_key().to_commitment();
416        let signature = AccountSignature::EcdsaK256Keccak(secret_key.sign(message));
417
418        let error = SignatureScheme::Ecdsa
419            .build_signature_advice_entry(commitment, message, &signature, None)
420            .unwrap_err();
421
422        assert!(error.contains("requires public key"));
423    }
424}