Skip to main content

cdk_signatory/
signatory.rs

1//! Signatory mod
2//!
3//! This module abstract all the key related operations, defining an interface for the necessary
4//! operations, to be implemented by the different signatory implementations.
5//!
6//! There is an in memory implementation, when the keys are stored in memory, in the same process,
7//! but it is isolated from the rest of the application, and they communicate through a channel with
8//! the defined API.
9use cdk_common::common::IssuerVersion;
10use cdk_common::error::Error;
11use cdk_common::mint::MintKeySetInfo;
12use cdk_common::nuts::nut02::KeySetVersion;
13use cdk_common::{
14    BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, Keys, MintKeySet, Proof, PublicKey,
15};
16
17/// RotateKeyArguments
18///
19/// This struct is used to pass the arguments to the rotate_keyset function
20#[derive(Debug, Clone)]
21pub struct RotateKeyArguments {
22    /// Unit
23    pub unit: CurrencyUnit,
24    /// List of amounts to support
25    pub amounts: Vec<u64>,
26    /// Input fee
27    pub input_fee_ppk: u64,
28    /// KeySet Version
29    pub keyset_id_type: KeySetVersion,
30    /// FinalExpiry
31    pub final_expiry: Option<u64>,
32}
33
34#[derive(Debug, Clone)]
35/// Signatory keysets
36pub struct SignatoryKeysets {
37    /// The public key
38    pub pubkey: PublicKey,
39    /// The list of keysets
40    pub keysets: Vec<SignatoryKeySet>,
41}
42
43#[derive(Debug, Clone)]
44/// SignatoryKeySet
45///
46/// This struct is used to represent a keyset and its info, pretty much all the information but the
47/// private key, that will never leave the signatory
48pub struct SignatoryKeySet {
49    /// The keyset Id
50    pub id: Id,
51    /// The Currency Unit
52    pub unit: CurrencyUnit,
53    /// Whether to set it as active or not
54    pub active: bool,
55    /// The list of public keys
56    pub keys: Keys,
57    /// Amounts supported by the keyset
58    pub amounts: Vec<u64>,
59    /// Input fee for the keyset (parts per thousand)
60    pub input_fee_ppk: u64,
61    /// Final expiry of the keyset (unix timestamp in the future)
62    pub final_expiry: Option<u64>,
63    /// Issuer Version
64    pub issuer_version: Option<IssuerVersion>,
65    /// Version is the derivation_path_index
66    pub version: u32,
67}
68
69impl SignatoryKeySet {
70    /// Returns true if `final_expiry` is set and strictly in the past.
71    pub fn is_expired(&self) -> bool {
72        self.final_expiry
73            .is_some_and(|expiry| expiry < cdk_common::util::unix_time())
74    }
75}
76
77impl From<&SignatoryKeySet> for KeySet {
78    fn from(val: &SignatoryKeySet) -> Self {
79        val.to_owned().into()
80    }
81}
82
83impl From<SignatoryKeySet> for KeySet {
84    fn from(val: SignatoryKeySet) -> Self {
85        KeySet {
86            id: val.id,
87            unit: val.unit,
88            active: Some(val.active),
89            keys: val.keys,
90            input_fee_ppk: val.input_fee_ppk,
91            final_expiry: val.final_expiry,
92        }
93    }
94}
95
96impl From<&SignatoryKeySet> for MintKeySetInfo {
97    fn from(val: &SignatoryKeySet) -> Self {
98        val.to_owned().into()
99    }
100}
101
102impl From<SignatoryKeySet> for MintKeySetInfo {
103    fn from(val: SignatoryKeySet) -> Self {
104        MintKeySetInfo {
105            id: val.id,
106            unit: val.unit,
107            active: val.active,
108            input_fee_ppk: val.input_fee_ppk,
109            derivation_path: Default::default(),
110            derivation_path_index: Default::default(),
111            amounts: val.amounts,
112            final_expiry: val.final_expiry,
113            issuer_version: val.issuer_version,
114            valid_from: 0,
115        }
116    }
117}
118
119/// The info is authoritative for every field but the key material: a reload
120/// reuses the already-derived [`MintKeySet`] while re-reading the info, so
121/// sourcing anything else from the key would publish a stale value.
122impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
123    fn from((info, key): &(MintKeySetInfo, MintKeySet)) -> Self {
124        Self {
125            id: info.id,
126            unit: info.unit.clone(),
127            active: info.active,
128            input_fee_ppk: info.input_fee_ppk,
129            amounts: info.amounts.clone(),
130            keys: key.keys.clone().into(),
131            version: info.derivation_path_index.unwrap_or(1),
132            final_expiry: info.final_expiry,
133            issuer_version: info.issuer_version.clone(),
134        }
135    }
136}
137
138#[async_trait::async_trait]
139/// Signatory trait
140pub trait Signatory {
141    /// The Signatory implementation name. This may be exposed, so being as discrete as possible is
142    /// advised.
143    fn name(&self) -> String;
144
145    /// Blind sign a message.
146    ///
147    /// The message can be for a coin or an auth token.
148    async fn blind_sign(
149        &self,
150        blinded_messages: Vec<BlindedMessage>,
151    ) -> Result<Vec<BlindSignature>, Error>;
152
153    /// Verify [`Proof`] meets conditions and is signed by the mint (ignores P2PK/HTLC signatures"
154    async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error>;
155
156    /// Retrieve the list of all mint keysets
157    async fn keysets(&self) -> Result<SignatoryKeysets, Error>;
158
159    /// Subscribe to keyset updates.
160    ///
161    /// The returned receiver holds the latest full set of keysets. The current
162    /// set is available immediately and the value is replaced on every
163    /// rotation. Consumers should treat each value as the complete current
164    /// state, not a delta.
165    async fn subscribe_keysets(
166        &self,
167    ) -> Result<tokio::sync::watch::Receiver<SignatoryKeysets>, Error>;
168
169    /// Add current keyset to inactive keysets
170    /// Generate new keyset
171    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<SignatoryKeySet, Error>;
172}
173
174#[cfg(test)]
175mod tests {
176    use std::collections::BTreeMap;
177    use std::str::FromStr;
178
179    use bitcoin::bip32::DerivationPath;
180    use bitcoin::secp256k1::Secp256k1;
181    use cdk_common::nuts::nut01::Keys;
182    use cdk_common::util::unix_time;
183    use cdk_common::{CurrencyUnit, Id};
184
185    use super::*;
186
187    fn dummy_signatory_keyset(final_expiry: Option<u64>) -> SignatoryKeySet {
188        SignatoryKeySet {
189            id: Id::from_str("009a1f293253e41e").unwrap(),
190            unit: CurrencyUnit::Sat,
191            active: true,
192            keys: Keys::new(BTreeMap::new()),
193            amounts: vec![1, 2, 4, 8, 16, 32, 64, 128, 256, 512],
194            input_fee_ppk: 0,
195            final_expiry,
196            issuer_version: None,
197            version: 0,
198        }
199    }
200
201    #[test]
202    fn test_is_expired_none() {
203        let ks = dummy_signatory_keyset(None);
204        assert!(!ks.is_expired());
205    }
206
207    #[test]
208    fn test_is_expired_far_future() {
209        let ks = dummy_signatory_keyset(Some(unix_time() + 1_000_000));
210        assert!(!ks.is_expired());
211    }
212
213    #[test]
214    fn test_is_expired_exactly_now_is_not_expired() {
215        // strict less-than: expiry == now is not yet expired
216        let ks = dummy_signatory_keyset(Some(unix_time()));
217        assert!(!ks.is_expired());
218    }
219
220    #[test]
221    fn test_is_expired_one_second_ago() {
222        let ks = dummy_signatory_keyset(Some(unix_time() - 1));
223        assert!(ks.is_expired());
224    }
225
226    #[test]
227    fn test_is_expired_zero() {
228        let ks = dummy_signatory_keyset(Some(0));
229        assert!(ks.is_expired());
230    }
231
232    #[test]
233    fn conversion_prefers_info_over_cached_key() {
234        let path = DerivationPath::from_str("m/0'/0'/0'").unwrap();
235        let key = MintKeySet::generate_from_seed(
236            &Secp256k1::new(),
237            b"test-seed-conversion",
238            &[1, 2, 4, 8],
239            CurrencyUnit::Usd,
240            path.clone(),
241            0,
242            Some(222),
243            KeySetVersion::Version00,
244        );
245
246        let info = MintKeySetInfo {
247            id: key.id,
248            unit: CurrencyUnit::Sat,
249            active: true,
250            valid_from: 0,
251            derivation_path: path,
252            derivation_path_index: Some(1),
253            amounts: vec![1, 2, 4, 8],
254            input_fee_ppk: 0,
255            final_expiry: Some(111),
256            issuer_version: None,
257        };
258
259        let keyset: SignatoryKeySet = (&(info, key)).into();
260
261        assert_eq!(
262            keyset.unit,
263            CurrencyUnit::Sat,
264            "unit must come from the keyset info, not the cached key"
265        );
266        assert_eq!(
267            keyset.final_expiry,
268            Some(111),
269            "final_expiry must come from the keyset info, not the cached key"
270        );
271    }
272}