bitcoin-encrypted-backup 0.0.2

Bitcoin Encrypted Backup
Documentation
#[cfg(feature = "miniscript_12_0")]
pub use mscript_12_0 as miniscript;
#[cfg(feature = "miniscript_12_3_5")]
pub use mscript_12_3_5 as miniscript;

extern crate alloc;

use alloc::{collections::BTreeSet, str::FromStr, vec::Vec};

use miniscript::{
    bitcoin::{self, bip32::DerivationPath, secp256k1},
    Descriptor, DescriptorPublicKey, ForEachKey,
};

use crate::Error;

pub fn dpk_to_pk(key: &DescriptorPublicKey) -> bitcoin::secp256k1::PublicKey {
    match key {
        DescriptorPublicKey::Single(key) => match key.key {
            miniscript::descriptor::SinglePubKey::FullKey(pk) => pk.inner,
            miniscript::descriptor::SinglePubKey::XOnly(pk) => {
                pk.public_key(bitcoin::key::Parity::Even)
            }
        },
        DescriptorPublicKey::XPub(key) => key.xkey.public_key,
        DescriptorPublicKey::MultiXPub(key) => key.xkey.public_key,
    }
}

fn dpk_to_deriv_path(key: &DescriptorPublicKey) -> Option<DerivationPath> {
    match key {
        DescriptorPublicKey::Single(key) => key.origin.clone().map(|(_, p)| p),
        DescriptorPublicKey::XPub(key) => key.origin.clone().map(|(_, p)| p),
        DescriptorPublicKey::MultiXPub(key) => key.origin.clone().map(|(_, p)| p),
    }
}

// See
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs:
// > One example of such a point is H =
// > lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) which is constructed
// > by taking the hash of the standard uncompressed encoding of the secp256k1 base point G as X
// > coordinate.
pub fn bip341_nums() -> bitcoin::secp256k1::PublicKey {
    bitcoin::secp256k1::PublicKey::from_str(
        "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
    )
    .expect("Valid pubkey: NUMS from BIP341")
}

pub fn descr_to_dpks(
    descriptor: &Descriptor<DescriptorPublicKey>,
) -> Result<Vec<DescriptorPublicKey>, Error> {
    let mut keys = BTreeSet::new();
    descriptor.for_each_key(|k| {
        let pk = dpk_to_pk(k);
        if pk != bip341_nums() {
            keys.insert(k.clone());
        }
        true
    });
    let keys: Vec<_> = keys.into_iter().collect();

    if keys.is_empty() {
        Err(Error::DescriptorHasNoKeys)
    } else {
        Ok(keys)
    }
}

pub fn dpks_to_derivation_keys_paths(
    dpks: &Vec<DescriptorPublicKey>,
) -> (Vec<secp256k1::PublicKey>, Vec<DerivationPath>) {
    let mut derivation_paths = BTreeSet::new();
    let mut keys = BTreeSet::new();
    for k in dpks {
        keys.insert(dpk_to_pk(k));
        if let Some(path) = dpk_to_deriv_path(k) {
            derivation_paths.insert(path);
        }
    }
    let deriv = derivation_paths.into_iter().collect();
    let keys = keys.into_iter().collect();
    (keys, deriv)
}

#[cfg(all(test, feature = "rand"))]
pub mod tests {
    use super::*;
    use alloc::{str::FromStr, vec};

    use miniscript::{
        bitcoin::bip32::{self, ChainCode, ChildNumber, Fingerprint},
        descriptor::{
            self, DerivPaths, DescriptorMultiXKey, DescriptorXKey, SinglePub, SinglePubKey,
            Wildcard,
        },
        Descriptor, DescriptorPublicKey, ToPublicKey,
    };

    pub fn descr_1() -> Descriptor<DescriptorPublicKey> {
        let descr_str = "wsh(or_d(pk([58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<0;1>/*),and_v(v:pkh([58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<2;3>/*),older(52596))))#pggrcdd0";

        Descriptor::<DescriptorPublicKey>::from_str(descr_str).unwrap()
    }

    pub fn dpk_1() -> DescriptorPublicKey {
        let dpk_str = "[58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<0;1>/*";
        DescriptorPublicKey::from_str(dpk_str).unwrap()
    }

    fn dpk_2() -> DescriptorPublicKey {
        let dpk_str = "[58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<2;3>/*";
        DescriptorPublicKey::from_str(dpk_str).unwrap()
    }

    fn dpk_3() -> DescriptorPublicKey {
        let dpk_str = "tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<2;3>/*";
        DescriptorPublicKey::from_str(dpk_str).unwrap()
    }
    pub fn pk() -> secp256k1::PublicKey {
        let raw = [
            3, 235, 210, 82, 202, 8, 119, 170, 224, 155, 157, 5, 130, 25, 104, 39, 117, 170, 60,
            188, 208, 73, 193, 47, 7, 131, 47, 44, 246, 163, 181, 23, 8,
        ];
        secp256k1::PublicKey::from_slice(&raw).unwrap()
    }

    #[test]
    fn test_dpk_to_pk() {
        let expected = pk();
        let pk = dpk_to_pk(&dpk_1());
        assert_eq!(pk, expected);
        let pk = dpk_to_pk(&dpk_2());
        assert_eq!(pk, expected);

        // Single
        let single_str = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
        let dpk = DescriptorPublicKey::from_str(single_str).unwrap();
        let pk = dpk_to_pk(&dpk);
        let expected = bitcoin::secp256k1::PublicKey::from_str(single_str).unwrap();
        assert_eq!(expected, pk);

        // Single Xonly
        let xonly = bitcoin::PublicKey::from_str(single_str)
            .unwrap()
            .to_x_only_pubkey();
        let dpk = DescriptorPublicKey::Single(SinglePub {
            origin: None,
            key: descriptor::SinglePubKey::XOnly(xonly),
        });
        let pk = dpk_to_pk(&dpk);
        assert_eq!(expected, pk);

        // Xpub
        let xpub = bip32::Xpub {
            network: bitcoin::NetworkKind::Test,
            depth: 1,
            parent_fingerprint: Fingerprint::from_str("00000000").unwrap(),
            child_number: ChildNumber::from_normal_idx(0).unwrap(),
            public_key: bitcoin::secp256k1::PublicKey::from_str(single_str).unwrap(),
            chain_code: ChainCode::from(&[1u8; 32]),
        };
        let dpk = DescriptorPublicKey::XPub(DescriptorXKey {
            origin: None,
            xkey: xpub,
            derivation_path: DerivationPath::default(),
            wildcard: Wildcard::None,
        });
        let pk = dpk_to_pk(&dpk);
        assert_eq!(expected, pk);

        // MultiXpub
        let dpk = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
            origin: None,
            xkey: xpub,
            derivation_paths: DerivPaths::new(vec![DerivationPath::from_str("0").unwrap()])
                .unwrap(),
            wildcard: Wildcard::None,
        });
        let pk = dpk_to_pk(&dpk);
        assert_eq!(expected, pk);
    }

    #[test]
    fn test_dpk_to_deriv() {
        let deriv_1 = dpk_to_deriv_path(&dpk_1()).unwrap();
        assert_eq!(deriv_1, DerivationPath::from_str("48'/1'/0'/2'").unwrap());
        let deriv_2 = dpk_to_deriv_path(&dpk_2()).unwrap();
        assert_eq!(deriv_2, DerivationPath::from_str("48'/1'/0'/2'").unwrap());
        let deriv_3 = dpk_to_deriv_path(&dpk_3());
        assert!(deriv_3.is_none());

        let dp = DerivationPath::from_str("0/0").unwrap();
        let origin = Some((Fingerprint::from_str("aabbccdd").unwrap(), dp.clone()));

        // Single
        let single_str = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
        let dpk = DescriptorPublicKey::from_str(single_str).unwrap();
        let none = dpk_to_deriv_path(&dpk);
        assert!(none.is_none());
        let single_pk = SinglePubKey::FullKey(dpk_to_pk(&dpk).into());
        let dpk = DescriptorPublicKey::Single(SinglePub {
            origin: origin.clone(),
            key: single_pk,
        });
        let deriv = dpk_to_deriv_path(&dpk).unwrap();
        assert_eq!(deriv, dp);

        // Xpub
        let xpub = bip32::Xpub {
            network: bitcoin::NetworkKind::Test,
            depth: 1,
            parent_fingerprint: Fingerprint::from_str("00000000").unwrap(),
            child_number: ChildNumber::from_normal_idx(0).unwrap(),
            public_key: bitcoin::secp256k1::PublicKey::from_str(single_str).unwrap(),
            chain_code: ChainCode::from(&[1u8; 32]),
        };
        let dpk = DescriptorPublicKey::XPub(DescriptorXKey {
            origin: None,
            xkey: xpub,
            derivation_path: DerivationPath::default(),
            wildcard: Wildcard::None,
        });
        let none = dpk_to_deriv_path(&dpk);
        assert!(none.is_none());
        let dpk = DescriptorPublicKey::XPub(DescriptorXKey {
            origin: origin.clone(),
            xkey: xpub,
            derivation_path: DerivationPath::default(),
            wildcard: Wildcard::None,
        });
        let deriv = dpk_to_deriv_path(&dpk).unwrap();
        assert_eq!(deriv, dp);

        // MultiXpub
        let dpk = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
            origin: None,
            xkey: xpub,
            derivation_paths: DerivPaths::new(vec![DerivationPath::from_str("0").unwrap()])
                .unwrap(),
            wildcard: Wildcard::None,
        });
        let none = dpk_to_deriv_path(&dpk);
        assert!(none.is_none());
        let dpk = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
            origin: origin.clone(),
            xkey: xpub,
            derivation_paths: DerivPaths::new(vec![DerivationPath::from_str("0").unwrap()])
                .unwrap(),
            wildcard: Wildcard::None,
        });
        let deriv = dpk_to_deriv_path(&dpk).unwrap();
        assert_eq!(deriv, dp);
    }

    #[test]
    fn test_descript_to_dpk() {
        let dpks = descr_to_dpks(&descr_1()).unwrap();
        let expected = vec![dpk_1(), dpk_2()];
        assert_eq!(dpks, expected);
    }

    #[test]
    fn test_descriptor_to_dpk_unspendable() {
        let descr_str = "tr(tpubD6NzVbkrYhZ4XWBqjZ7DTB4eFvi8eQZ79UvNbQFsxXiaMNaBn83jpMWTXLX2Gx6JgC5n9jWvx6vnijcAUgxXmRtFd4ntasRGNsYSCvQteSr/<0;1>/*,{and_v(v:and_v(v:pk([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<2;3>/*),pk([79af2d8a/48'/1'/0'/2']tpubDEtHs6m9crfv1oeETj6EXteAtW7eoSSBVBaypEdWZt8VftbHF9R12xSZpzWGNuAofeGPL6cz48dLdCYbVioHL8ygA56yuPW76Xz5WZ3dt8o/<2;3>/*)),older(52596)),and_v(v:pk([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<0;1>/*),pk([79af2d8a/48'/1'/0'/2']tpubDEtHs6m9crfv1oeETj6EXteAtW7eoSSBVBaypEdWZt8VftbHF9R12xSZpzWGNuAofeGPL6cz48dLdCYbVioHL8ygA56yuPW76Xz5WZ3dt8o/<0;1>/*))})#vudj49fm";
        let descriptor = Descriptor::<DescriptorPublicKey>::from_str(descr_str).unwrap();
        // unspendable keys must have been dropped
        let keys = descr_to_dpks(&descriptor).unwrap();
        for key in keys {
            let pk = dpk_to_pk(&key);
            assert_ne!(pk, bip341_nums());
        }
        // but the descriptor contains unspendable
        let contains_unspendable = descriptor.for_any_key(|k| {
            let pk = dpk_to_pk(k);
            pk == bip341_nums()
        });
        assert!(contains_unspendable);
    }

    #[test]
    fn test_dpks_to_deriv_paths() {
        let dpks = vec![dpk_1(), dpk_2()];
        let pks = vec![pk()];
        let deriv = vec![DerivationPath::from_str("48'/1'/0'/2'").unwrap()];
        let res = dpks_to_derivation_keys_paths(&dpks);
        assert_eq!(res, (pks, deriv));
    }
}