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#[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 pub const fn auth_scheme(self) -> AuthScheme {
50 match self {
51 Self::Falcon => AuthScheme::Falcon512Poseidon2,
52 Self::Ecdsa => AuthScheme::EcdsaK256Keccak,
53 }
54 }
55
56 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#[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#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, utoipa::ToSchema)]
170#[serde(tag = "scheme", rename_all = "snake_case")]
171pub enum ProposalSignature {
172 Falcon {
173 signature: String,
175 },
176 Ecdsa {
177 signature: String,
179 #[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 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 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#[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#[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 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 let json = account.to_json();
338
339 let deserialized_account =
341 Account::from_json(&json).expect("Failed to deserialize account");
342
343 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}