miden_multisig_client/
keystore.rs1use guardian_shared::SignatureScheme;
4use miden_protocol::Word;
5
6pub use guardian_client::{
7 EcdsaKeyStore as EcdsaGuardianKeyStore, FalconKeyStore, Signer as KeyManager,
8};
9
10pub type GuardianKeyStore = FalconKeyStore;
12
13pub fn strip_hex_prefix(input: &str) -> &str {
15 input.strip_prefix("0x").unwrap_or(input)
16}
17
18pub fn ensure_hex_prefix(input: &str) -> String {
20 if input.starts_with("0x") {
21 input.to_string()
22 } else {
23 format!("0x{}", input)
24 }
25}
26
27pub fn proposal_public_key_hex(key_manager: &dyn KeyManager) -> Option<String> {
29 match key_manager.scheme() {
30 SignatureScheme::Falcon => None,
31 SignatureScheme::Ecdsa => Some(key_manager.public_key_hex()),
32 }
33}
34
35pub fn validate_commitment_hex(input: &str) -> Result<(), String> {
37 let stripped = strip_hex_prefix(input);
38 if stripped.len() != 64 {
39 return Err(format!(
40 "invalid commitment length: expected 64 hex chars, got {}",
41 stripped.len()
42 ));
43 }
44 hex::decode(stripped).map_err(|e| format!("invalid hex: {}", e))?;
45 Ok(())
46}
47
48pub fn word_from_hex(hex_str: &str) -> Result<Word, String> {
50 let trimmed = strip_hex_prefix(hex_str);
51 let bytes = hex::decode(trimmed).map_err(|e| format!("invalid hex: {}", e))?;
52
53 if bytes.len() != 32 {
54 return Err(format!(
55 "invalid word length: expected 32 bytes, got {}",
56 bytes.len()
57 ));
58 }
59
60 let mut felts = [miden_protocol::Felt::ZERO; 4];
61 #[allow(clippy::needless_range_loop)]
62 for (i, chunk) in bytes.chunks(8).enumerate() {
63 let mut arr = [0u8; 8];
64 arr.copy_from_slice(chunk);
65 felts[i] = miden_protocol::Felt::try_from(u64::from_le_bytes(arr))
66 .map_err(|e| format!("invalid field element in word '{}': {}", hex_str, e))?;
67 }
68
69 Ok(felts.into())
70}
71
72pub fn commitment_from_hex(hex_str: &str) -> Result<Word, String> {
74 word_from_hex(hex_str)
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use guardian_shared::hex::FromHex;
81 use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{
82 PublicKey as EcdsaPublicKey, Signature as EcdsaSignature,
83 };
84 use miden_protocol::crypto::dsa::falcon512_poseidon2::{
85 PublicKey as FalconPublicKey, Signature as FalconSignature,
86 };
87 use miden_protocol::utils::serde::Deserializable;
88
89 #[test]
90 fn falcon_signer_commitment_roundtrip_via_hex() {
91 let signer = FalconKeyStore::generate();
92 let hex = signer.commitment_hex();
93 let parsed = commitment_from_hex(&hex).unwrap();
94 assert_eq!(parsed, signer.commitment());
95 }
96
97 #[test]
98 fn ecdsa_signer_commitment_roundtrip_via_hex() {
99 let signer = EcdsaGuardianKeyStore::generate();
100 let hex = signer.commitment_hex();
101 let parsed = commitment_from_hex(&hex).unwrap();
102 assert_eq!(parsed, signer.commitment());
103 }
104
105 #[test]
106 fn falcon_signer_signature_hex_is_verifiable() {
107 let signer = FalconKeyStore::generate();
108 let message = Word::default();
109 let signature = FalconSignature::from_hex(&signer.sign_word_hex(message)).unwrap();
110 let public_key = FalconPublicKey::from_hex(&signer.public_key_hex()).unwrap();
111 assert!(public_key.verify(message, &signature));
112 }
113
114 #[test]
115 fn ecdsa_signer_signature_hex_is_verifiable() {
116 let signer = EcdsaGuardianKeyStore::generate();
117 let message = Word::default();
118 let signature_bytes =
119 hex::decode(signer.sign_word_hex(message).trim_start_matches("0x")).unwrap();
120 let signature = EcdsaSignature::read_from_bytes(&signature_bytes).unwrap();
121 let public_key_bytes =
122 hex::decode(signer.public_key_hex().trim_start_matches("0x")).unwrap();
123 let public_key = EcdsaPublicKey::read_from_bytes(&public_key_bytes).unwrap();
124 assert!(public_key.verify(message, &signature));
125 }
126
127 #[test]
128 fn proposal_public_key_hex_is_none_for_falcon() {
129 let signer = FalconKeyStore::generate();
130 assert_eq!(proposal_public_key_hex(&signer), None);
131 }
132
133 #[test]
134 fn proposal_public_key_hex_is_present_for_ecdsa() {
135 let signer = EcdsaGuardianKeyStore::generate();
136 assert_eq!(
137 proposal_public_key_hex(&signer),
138 Some(signer.public_key_hex())
139 );
140 }
141
142 #[test]
143 fn strip_hex_prefix_with_prefix() {
144 assert_eq!(strip_hex_prefix("0xabcd"), "abcd");
145 }
146
147 #[test]
148 fn strip_hex_prefix_without_prefix() {
149 assert_eq!(strip_hex_prefix("abcd"), "abcd");
150 }
151
152 #[test]
153 fn ensure_hex_prefix_adds_prefix() {
154 assert_eq!(ensure_hex_prefix("abcd"), "0xabcd");
155 }
156
157 #[test]
158 fn validate_commitment_hex_invalid_chars() {
159 let not_hex = "g".repeat(64);
160 let err = validate_commitment_hex(¬_hex).unwrap_err();
161 assert!(err.contains("invalid hex"));
162 }
163}