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