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