diff --git a/.travis.yml b/.travis.yml index 9c4120a8..24fa6c73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,11 @@ rust: - stable # - 1.31.0 # - 1.22.0 +before_script: + - rustup component add rustfmt script: - cd $TRAVIS_BUILD_DIR/core/lib/ + - cargo fmt -- --check --verbose - cargo build --verbose --all - cargo test --verbose --all diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index df5a5abd..d9b119a0 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -2,7 +2,10 @@ name = "magical-bitcoin-wallet" version = "0.1.0" authors = ["Riccardo Casatta ", "Alekos Filini "] -edition = "2018" [dependencies] -"electrum-client" = { version = "0.1.0-beta.1", optional = true } +log = "^0.4" +bitcoin = { version = "0.23", features = ["use-serde"] } +miniscript = { version = "0.12" } +serde = { version = "^1.0", features = ["derive"] } +serde_json = { version = "^1.0" } diff --git a/core/lib/examples/parse_descriptor.rs b/core/lib/examples/parse_descriptor.rs new file mode 100644 index 00000000..2af42b3a --- /dev/null +++ b/core/lib/examples/parse_descriptor.rs @@ -0,0 +1,27 @@ +extern crate magical_bitcoin_wallet; + +use std::str::FromStr; + +use magical_bitcoin_wallet::bitcoin::*; +use magical_bitcoin_wallet::descriptor::*; + +fn main() { + let desc = "sh(wsh(or_d(\ + thresh_m(\ + 2,[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*,tprv8ZgxMBicQKsPduL5QnGihpprdHyypMGi4DhimjtzYemu7se5YQNcZfAPLqXRuGHb5ZX2eTQj62oNqMnyxJ7B7wz54Uzswqw8fFqMVdcmVF7/1/*\ + ),\ + and_v(vc:pk_h(cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy),older(1000))\ + )))"; + + let extended_desc = ExtendedDescriptor::from_str(desc).unwrap(); + println!("{:?}", extended_desc); + + let derived_desc = extended_desc.derive(42).unwrap(); + println!("{:?}", derived_desc); + + let addr = derived_desc.address(Network::Testnet).unwrap(); + println!("{}", addr); + + let script = derived_desc.witness_script(); + println!("{:?}", script); +} diff --git a/core/lib/src/descriptor/error.rs b/core/lib/src/descriptor/error.rs new file mode 100644 index 00000000..f9a10870 --- /dev/null +++ b/core/lib/src/descriptor/error.rs @@ -0,0 +1,26 @@ +#[derive(Debug)] +pub enum Error { + InternalError, + InvalidPrefix(Vec), + HardenedDerivationOnXpub, + MalformedInput, + KeyParsingError(String), + + BIP32(bitcoin::util::bip32::Error), + Base58(bitcoin::util::base58::Error), + PK(bitcoin::util::key::Error), + Miniscript(miniscript::Error), + Hex(bitcoin::hashes::hex::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl_error!(bitcoin::util::bip32::Error, BIP32); +impl_error!(bitcoin::util::base58::Error, Base58); +impl_error!(bitcoin::util::key::Error, PK); +impl_error!(miniscript::Error, Miniscript); +impl_error!(bitcoin::hashes::hex::Error, Hex); diff --git a/core/lib/src/descriptor/extended_key.rs b/core/lib/src/descriptor/extended_key.rs new file mode 100644 index 00000000..0f7bd9e9 --- /dev/null +++ b/core/lib/src/descriptor/extended_key.rs @@ -0,0 +1,349 @@ +use std::fmt::{self, Display}; +use std::str::FromStr; + +use bitcoin::hashes::hex::{FromHex, ToHex}; +use bitcoin::secp256k1; +use bitcoin::util::base58; +use bitcoin::util::bip32::{ + ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, +}; +use bitcoin::PublicKey; + +#[allow(unused_imports)] +use log::{debug, error, info, trace}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum DerivationIndex { + Fixed, + Normal, + Hardened, +} + +impl DerivationIndex { + fn as_path(&self, index: u32) -> DerivationPath { + match self { + DerivationIndex::Fixed => vec![], + DerivationIndex::Normal => vec![ChildNumber::Normal { index }], + DerivationIndex::Hardened => vec![ChildNumber::Hardened { index }], + } + .into() + } +} + +impl Display for DerivationIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let chars = match *self { + Self::Fixed => "", + Self::Normal => "/*", + Self::Hardened => "/*'", + }; + + write!(f, "{}", chars) + } +} + +#[derive(Clone, Debug)] +pub struct DescriptorExtendedKey { + pub master_fingerprint: Option, + pub master_derivation: Option, + pub pubkey: ExtendedPubKey, + pub secret: Option, + pub path: DerivationPath, + pub final_index: DerivationIndex, +} + +impl DescriptorExtendedKey { + pub fn full_path(&self, index: u32) -> DerivationPath { + let mut final_path: Vec = self.path.clone().into(); + let other_path: Vec = self.final_index.as_path(index).into(); + final_path.extend_from_slice(&other_path); + + final_path.into() + } + + pub fn derive( + &self, + ctx: &secp256k1::Secp256k1, + index: u32, + ) -> Result { + Ok(self.derive_xpub(ctx, index)?.public_key) + } + + pub fn derive_xpub( + &self, + ctx: &secp256k1::Secp256k1, + index: u32, + ) -> Result { + if let Some(xprv) = self.secret { + let derive_priv = xprv.derive_priv(ctx, &self.full_path(index))?; + Ok(ExtendedPubKey::from_private(ctx, &derive_priv)) + } else { + Ok(self.pubkey.derive_pub(ctx, &self.full_path(index))?) + } + } + + pub fn root_xpub( + &self, + ctx: &secp256k1::Secp256k1, + ) -> ExtendedPubKey { + if let Some(ref xprv) = self.secret { + ExtendedPubKey::from_private(ctx, xprv) + } else { + self.pubkey + } + } +} + +impl Display for DescriptorExtendedKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref fingerprint) = self.master_fingerprint { + write!(f, "[{}", fingerprint.to_hex())?; + if let Some(ref path) = self.master_derivation { + write!(f, "{}", &path.to_string()[1..])?; + } + write!(f, "]")?; + } + + if let Some(xprv) = self.secret { + write!(f, "{}", xprv)? + } else { + write!(f, "{}", self.pubkey)? + } + + write!(f, "{}{}", &self.path.to_string()[1..], self.final_index) + } +} + +impl FromStr for DescriptorExtendedKey { + type Err = super::Error; + + fn from_str(inp: &str) -> Result { + let len = inp.len(); + + let (master_fingerprint, master_derivation, offset) = match inp.starts_with("[") { + false => (None, None, 0), + true => { + if inp.len() < 9 { + return Err(super::Error::MalformedInput); + } + + let master_fingerprint = &inp[1..9]; + let close_bracket_index = + &inp[9..].find("]").ok_or(super::Error::MalformedInput)?; + let path = if *close_bracket_index > 0 { + Some(DerivationPath::from_str(&format!( + "m{}", + &inp[9..9 + *close_bracket_index] + ))?) + } else { + None + }; + + ( + Some(Fingerprint::from_hex(master_fingerprint)?), + path, + 9 + *close_bracket_index + 1, + ) + } + }; + + let (key_range, offset) = match &inp[offset..].find("/") { + Some(index) => (offset..offset + *index, offset + *index), + None => (offset..len, len), + }; + let data = base58::from_check(&inp[key_range.clone()])?; + let secp = secp256k1::Secp256k1::new(); + let (pubkey, secret) = match &data[0..4] { + [0x04u8, 0x88, 0xB2, 0x1E] | [0x04u8, 0x35, 0x87, 0xCF] => { + (ExtendedPubKey::from_str(&inp[key_range])?, None) + } + [0x04u8, 0x88, 0xAD, 0xE4] | [0x04u8, 0x35, 0x83, 0x94] => { + let private = ExtendedPrivKey::from_str(&inp[key_range])?; + (ExtendedPubKey::from_private(&secp, &private), Some(private)) + } + data => return Err(super::Error::InvalidPrefix(data.into())), + }; + + let (path, final_index, _) = match &inp[offset..].starts_with("/") { + false => (DerivationPath::from(vec![]), DerivationIndex::Fixed, offset), + true => { + let (all, skip) = match &inp[len - 2..len] { + "/*" => (DerivationIndex::Normal, 2), + "*'" | "*h" => (DerivationIndex::Hardened, 3), + _ => (DerivationIndex::Fixed, 0), + }; + + if all == DerivationIndex::Hardened && secret.is_none() { + return Err(super::Error::HardenedDerivationOnXpub); + } + + ( + DerivationPath::from_str(&format!("m{}", &inp[offset..len - skip]))?, + all, + len, + ) + } + }; + + Ok(DescriptorExtendedKey { + master_fingerprint, + master_derivation, + pubkey, + secret, + path, + final_index, + }) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use bitcoin::hashes::hex::FromHex; + use bitcoin::util::bip32::{ChildNumber, DerivationPath}; + + use crate::descriptor::*; + + macro_rules! hex_fingerprint { + ($hex:expr) => { + Fingerprint::from_hex($hex).unwrap() + }; + } + + macro_rules! deriv_path { + ($str:expr) => { + DerivationPath::from_str($str).unwrap() + }; + + () => { + DerivationPath::from(vec![]) + }; + } + + #[test] + fn test_derivation_index_fixed() { + let index = DerivationIndex::Fixed; + assert_eq!(index.as_path(1337), DerivationPath::from(vec![])); + assert_eq!(format!("{}", index), ""); + } + + #[test] + fn test_derivation_index_normal() { + let index = DerivationIndex::Normal; + assert_eq!( + index.as_path(1337), + DerivationPath::from(vec![ChildNumber::Normal { index: 1337 }]) + ); + assert_eq!(format!("{}", index), "/*"); + } + + #[test] + fn test_derivation_index_hardened() { + let index = DerivationIndex::Hardened; + assert_eq!( + index.as_path(1337), + DerivationPath::from(vec![ChildNumber::Hardened { index: 1337 }]) + ); + assert_eq!(format!("{}", index), "/*'"); + } + + #[test] + fn test_parse_xpub_no_path_fixed() { + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.pubkey.fingerprint(), hex_fingerprint!("31a507b8")); + assert_eq!(ek.path, deriv_path!()); + assert_eq!(ek.final_index, DerivationIndex::Fixed); + } + + #[test] + fn test_parse_xpub_with_path_fixed() { + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/2/3"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.pubkey.fingerprint(), hex_fingerprint!("31a507b8")); + assert_eq!(ek.path, deriv_path!("m/1/2/3")); + assert_eq!(ek.final_index, DerivationIndex::Fixed); + } + + #[test] + fn test_parse_xpub_with_path_normal() { + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/2/3/*"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.pubkey.fingerprint(), hex_fingerprint!("31a507b8")); + assert_eq!(ek.path, deriv_path!("m/1/2/3")); + assert_eq!(ek.final_index, DerivationIndex::Normal); + } + + #[test] + #[should_panic(expected = "HardenedDerivationOnXpub")] + fn test_parse_xpub_with_path_hardened() { + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/*'"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.pubkey.fingerprint(), hex_fingerprint!("31a507b8")); + assert_eq!(ek.path, deriv_path!("m/1/2/3")); + assert_eq!(ek.final_index, DerivationIndex::Fixed); + } + + #[test] + fn test_parse_tprv_with_path_hardened() { + let key = "tprv8ZgxMBicQKsPduL5QnGihpprdHyypMGi4DhimjtzYemu7se5YQNcZfAPLqXRuGHb5ZX2eTQj62oNqMnyxJ7B7wz54Uzswqw8fFqMVdcmVF7/1/2/3/*'"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert!(ek.secret.is_some()); + assert_eq!(ek.pubkey.fingerprint(), hex_fingerprint!("5ea4190e")); + assert_eq!(ek.path, deriv_path!("m/1/2/3")); + assert_eq!(ek.final_index, DerivationIndex::Hardened); + } + + #[test] + fn test_parse_xpub_master_details() { + let key = "[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.master_fingerprint, Some(hex_fingerprint!("d34db33f"))); + assert_eq!(ek.master_derivation, Some(deriv_path!("m/44'/0'/0'"))); + } + + #[test] + fn test_parse_xpub_master_details_empty_derivation() { + let key = "[d34db33f]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.master_fingerprint, Some(hex_fingerprint!("d34db33f"))); + assert_eq!(ek.master_derivation, None); + } + + #[test] + #[should_panic(expected = "MalformedInput")] + fn test_parse_xpub_short_input() { + let key = "[d34d"; + DescriptorExtendedKey::from_str(key).unwrap(); + } + + #[test] + #[should_panic(expected = "MalformedInput")] + fn test_parse_xpub_missing_closing_bracket() { + let key = "[d34db33fxpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; + DescriptorExtendedKey::from_str(key).unwrap(); + } + + #[test] + #[should_panic(expected = "InvalidChar")] + fn test_parse_xpub_invalid_fingerprint() { + let key = "[d34db33z]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; + DescriptorExtendedKey::from_str(key).unwrap(); + } + + #[test] + fn test_xpub_normal_full_path() { + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/2/*"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.full_path(42), deriv_path!("m/1/2/42")); + } + + #[test] + fn test_xpub_fixed_full_path() { + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/2"; + let ek = DescriptorExtendedKey::from_str(key).unwrap(); + assert_eq!(ek.full_path(42), deriv_path!("m/1/2")); + assert_eq!(ek.full_path(1337), deriv_path!("m/1/2")); + } +} diff --git a/core/lib/src/descriptor/mod.rs b/core/lib/src/descriptor/mod.rs new file mode 100644 index 00000000..d3cb14fe --- /dev/null +++ b/core/lib/src/descriptor/mod.rs @@ -0,0 +1,494 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::convert::{Into, TryFrom}; +use std::fmt; +use std::str::FromStr; + +use bitcoin::blockdata::script::Script; +use bitcoin::hashes::{hash160, Hash}; +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, Fingerprint}; +use bitcoin::{PrivateKey, PublicKey}; + +pub use miniscript::descriptor::Descriptor; + +use serde::{Deserialize, Serialize}; + +pub mod error; +pub mod extended_key; + +pub use self::error::Error; +pub use self::extended_key::{DerivationIndex, DescriptorExtendedKey}; + +#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Default)] +struct DummyKey(); + +impl fmt::Display for DummyKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DummyKey") + } +} + +impl std::str::FromStr for DummyKey { + type Err = (); + + fn from_str(_: &str) -> Result { + Ok(DummyKey::default()) + } +} + +impl miniscript::MiniscriptKey for DummyKey { + type Hash = DummyKey; + + fn to_pubkeyhash(&self) -> DummyKey { + DummyKey::default() + } +} + +pub type DerivedDescriptor = Descriptor; +pub type StringDescriptor = Descriptor; + +pub trait DescriptorMeta { + fn is_witness(&self) -> bool; + fn psbt_redeem_script(&self) -> Option