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