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