cdk-signatory 0.18.2

CDK signatory default implementation
Documentation
//! Signatory mod
//!
//! This module abstract all the key related operations, defining an interface for the necessary
//! operations, to be implemented by the different signatory implementations.
//!
//! There is an in memory implementation, when the keys are stored in memory, in the same process,
//! but it is isolated from the rest of the application, and they communicate through a channel with
//! the defined API.
use cdk_common::common::IssuerVersion;
use cdk_common::error::Error;
use cdk_common::mint::MintKeySetInfo;
use cdk_common::nuts::nut02::KeySetVersion;
use cdk_common::{
    BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, Keys, MintKeySet, Proof, PublicKey,
};

/// RotateKeyArguments
///
/// This struct is used to pass the arguments to the rotate_keyset function
#[derive(Debug, Clone)]
pub struct RotateKeyArguments {
    /// Unit
    pub unit: CurrencyUnit,
    /// List of amounts to support
    pub amounts: Vec<u64>,
    /// Input fee
    pub input_fee_ppk: u64,
    /// KeySet Version
    pub keyset_id_type: KeySetVersion,
    /// FinalExpiry
    pub final_expiry: Option<u64>,
}

#[derive(Debug, Clone)]
/// Signatory keysets
pub struct SignatoryKeysets {
    /// The public key
    pub pubkey: PublicKey,
    /// The list of keysets
    pub keysets: Vec<SignatoryKeySet>,
}

#[derive(Debug, Clone)]
/// SignatoryKeySet
///
/// This struct is used to represent a keyset and its info, pretty much all the information but the
/// private key, that will never leave the signatory
pub struct SignatoryKeySet {
    /// The keyset Id
    pub id: Id,
    /// The Currency Unit
    pub unit: CurrencyUnit,
    /// Whether to set it as active or not
    pub active: bool,
    /// The list of public keys
    pub keys: Keys,
    /// Amounts supported by the keyset
    pub amounts: Vec<u64>,
    /// Input fee for the keyset (parts per thousand)
    pub input_fee_ppk: u64,
    /// Final expiry of the keyset (unix timestamp in the future)
    pub final_expiry: Option<u64>,
    /// Issuer Version
    pub issuer_version: Option<IssuerVersion>,
    /// Version is the derivation_path_index
    pub version: u32,
}

impl SignatoryKeySet {
    /// Returns true if `final_expiry` is set and strictly in the past.
    pub fn is_expired(&self) -> bool {
        self.final_expiry
            .is_some_and(|expiry| expiry < cdk_common::util::unix_time())
    }
}

impl From<&SignatoryKeySet> for KeySet {
    fn from(val: &SignatoryKeySet) -> Self {
        val.to_owned().into()
    }
}

impl From<SignatoryKeySet> for KeySet {
    fn from(val: SignatoryKeySet) -> Self {
        KeySet {
            id: val.id,
            unit: val.unit,
            active: Some(val.active),
            keys: val.keys,
            input_fee_ppk: val.input_fee_ppk,
            final_expiry: val.final_expiry,
        }
    }
}

impl From<&SignatoryKeySet> for MintKeySetInfo {
    fn from(val: &SignatoryKeySet) -> Self {
        val.to_owned().into()
    }
}

impl From<SignatoryKeySet> for MintKeySetInfo {
    fn from(val: SignatoryKeySet) -> Self {
        MintKeySetInfo {
            id: val.id,
            unit: val.unit,
            active: val.active,
            input_fee_ppk: val.input_fee_ppk,
            derivation_path: Default::default(),
            derivation_path_index: Default::default(),
            amounts: val.amounts,
            final_expiry: val.final_expiry,
            issuer_version: val.issuer_version,
            valid_from: 0,
        }
    }
}

/// The info is authoritative for every field but the key material: a reload
/// reuses the already-derived [`MintKeySet`] while re-reading the info, so
/// sourcing anything else from the key would publish a stale value.
impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
    fn from((info, key): &(MintKeySetInfo, MintKeySet)) -> Self {
        Self {
            id: info.id,
            unit: info.unit.clone(),
            active: info.active,
            input_fee_ppk: info.input_fee_ppk,
            amounts: info.amounts.clone(),
            keys: key.keys.clone().into(),
            version: info.derivation_path_index.unwrap_or(1),
            final_expiry: info.final_expiry,
            issuer_version: info.issuer_version.clone(),
        }
    }
}

#[async_trait::async_trait]
/// Signatory trait
pub trait Signatory {
    /// The Signatory implementation name. This may be exposed, so being as discrete as possible is
    /// advised.
    fn name(&self) -> String;

    /// Blind sign a message.
    ///
    /// The message can be for a coin or an auth token.
    async fn blind_sign(
        &self,
        blinded_messages: Vec<BlindedMessage>,
    ) -> Result<Vec<BlindSignature>, Error>;

    /// Verify [`Proof`] meets conditions and is signed by the mint (ignores P2PK/HTLC signatures"
    async fn verify_proofs(&self, proofs: Vec<Proof>) -> Result<(), Error>;

    /// Retrieve the list of all mint keysets
    async fn keysets(&self) -> Result<SignatoryKeysets, Error>;

    /// Subscribe to keyset updates.
    ///
    /// The returned receiver holds the latest full set of keysets. The current
    /// set is available immediately and the value is replaced on every
    /// rotation. Consumers should treat each value as the complete current
    /// state, not a delta.
    async fn subscribe_keysets(
        &self,
    ) -> Result<tokio::sync::watch::Receiver<SignatoryKeysets>, Error>;

    /// Add current keyset to inactive keysets
    /// Generate new keyset
    async fn rotate_keyset(&self, args: RotateKeyArguments) -> Result<SignatoryKeySet, Error>;
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use std::str::FromStr;

    use bitcoin::bip32::DerivationPath;
    use bitcoin::secp256k1::Secp256k1;
    use cdk_common::nuts::nut01::Keys;
    use cdk_common::util::unix_time;
    use cdk_common::{CurrencyUnit, Id};

    use super::*;

    fn dummy_signatory_keyset(final_expiry: Option<u64>) -> SignatoryKeySet {
        SignatoryKeySet {
            id: Id::from_str("009a1f293253e41e").unwrap(),
            unit: CurrencyUnit::Sat,
            active: true,
            keys: Keys::new(BTreeMap::new()),
            amounts: vec![1, 2, 4, 8, 16, 32, 64, 128, 256, 512],
            input_fee_ppk: 0,
            final_expiry,
            issuer_version: None,
            version: 0,
        }
    }

    #[test]
    fn test_is_expired_none() {
        let ks = dummy_signatory_keyset(None);
        assert!(!ks.is_expired());
    }

    #[test]
    fn test_is_expired_far_future() {
        let ks = dummy_signatory_keyset(Some(unix_time() + 1_000_000));
        assert!(!ks.is_expired());
    }

    #[test]
    fn test_is_expired_exactly_now_is_not_expired() {
        // strict less-than: expiry == now is not yet expired
        let ks = dummy_signatory_keyset(Some(unix_time()));
        assert!(!ks.is_expired());
    }

    #[test]
    fn test_is_expired_one_second_ago() {
        let ks = dummy_signatory_keyset(Some(unix_time() - 1));
        assert!(ks.is_expired());
    }

    #[test]
    fn test_is_expired_zero() {
        let ks = dummy_signatory_keyset(Some(0));
        assert!(ks.is_expired());
    }

    #[test]
    fn conversion_prefers_info_over_cached_key() {
        let path = DerivationPath::from_str("m/0'/0'/0'").unwrap();
        let key = MintKeySet::generate_from_seed(
            &Secp256k1::new(),
            b"test-seed-conversion",
            &[1, 2, 4, 8],
            CurrencyUnit::Usd,
            path.clone(),
            0,
            Some(222),
            KeySetVersion::Version00,
        );

        let info = MintKeySetInfo {
            id: key.id,
            unit: CurrencyUnit::Sat,
            active: true,
            valid_from: 0,
            derivation_path: path,
            derivation_path_index: Some(1),
            amounts: vec![1, 2, 4, 8],
            input_fee_ppk: 0,
            final_expiry: Some(111),
            issuer_version: None,
        };

        let keyset: SignatoryKeySet = (&(info, key)).into();

        assert_eq!(
            keyset.unit,
            CurrencyUnit::Sat,
            "unit must come from the keyset info, not the cached key"
        );
        assert_eq!(
            keyset.final_expiry,
            Some(111),
            "final_expiry must come from the keyset info, not the cached key"
        );
    }
}