Skip to main content

guardian_shared/
lib.rs

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