Skip to main content

miden_multisig_client/
keystore.rs

1//! Signer re-exports and hex utilities used by the multisig client.
2
3use guardian_shared::SignatureScheme;
4use miden_protocol::Word;
5
6pub use guardian_client::{
7    EcdsaKeyStore as EcdsaGuardianKeyStore, FalconKeyStore, Signer as KeyManager,
8};
9
10/// Backward-compatible alias for the Falcon key store.
11pub type GuardianKeyStore = FalconKeyStore;
12
13/// Strips the "0x" prefix from a hex string if present.
14pub fn strip_hex_prefix(input: &str) -> &str {
15    input.strip_prefix("0x").unwrap_or(input)
16}
17
18/// Ensures the hex string has a "0x" prefix.
19pub 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
27/// Returns the public key hex needed for exported ECDSA signatures.
28pub 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
35/// Validates that a string is valid commitment hex (64 hex chars, optionally with 0x prefix).
36pub 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
48/// Parses a hex-encoded word string to a Word.
49pub 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
72/// Backward-compatible alias for parsing commitment hex.
73pub 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(&not_hex).unwrap_err();
161        assert!(err.contains("invalid hex"));
162    }
163}