Wrap Miniscript descriptors to support xpubs
This commit is contained in:
		
							parent
							
								
									94ca008a90
								
							
						
					
					
						commit
						fa9a62fbee
					
				| @ -3,8 +3,11 @@ rust: | |||||||
|   - stable |   - stable | ||||||
| #  - 1.31.0 | #  - 1.31.0 | ||||||
| #  - 1.22.0 | #  - 1.22.0 | ||||||
|  | before_script: | ||||||
|  |   - rustup component add rustfmt | ||||||
| script: | script: | ||||||
|   - cd $TRAVIS_BUILD_DIR/core/lib/ |   - cd $TRAVIS_BUILD_DIR/core/lib/ | ||||||
|  |   - cargo fmt -- --check --verbose | ||||||
|   - cargo build --verbose --all |   - cargo build --verbose --all | ||||||
|   - cargo test --verbose --all |   - cargo test --verbose --all | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,7 +2,10 @@ | |||||||
| name = "magical-bitcoin-wallet" | name = "magical-bitcoin-wallet" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| authors = ["Riccardo Casatta <riccardo@casatta.it>", "Alekos Filini <alekos.filini@gmail.com>"] | authors = ["Riccardo Casatta <riccardo@casatta.it>", "Alekos Filini <alekos.filini@gmail.com>"] | ||||||
| edition = "2018" |  | ||||||
| 
 | 
 | ||||||
| [dependencies] | [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" } | ||||||
|  | |||||||
							
								
								
									
										27
									
								
								core/lib/examples/parse_descriptor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								core/lib/examples/parse_descriptor.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								core/lib/src/descriptor/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								core/lib/src/descriptor/error.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum Error { | ||||||
|  |     InternalError, | ||||||
|  |     InvalidPrefix(Vec<u8>), | ||||||
|  |     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); | ||||||
							
								
								
									
										349
									
								
								core/lib/src/descriptor/extended_key.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								core/lib/src/descriptor/extended_key.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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<Fingerprint>, | ||||||
|  |     pub master_derivation: Option<DerivationPath>, | ||||||
|  |     pub pubkey: ExtendedPubKey, | ||||||
|  |     pub secret: Option<ExtendedPrivKey>, | ||||||
|  |     pub path: DerivationPath, | ||||||
|  |     pub final_index: DerivationIndex, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl DescriptorExtendedKey { | ||||||
|  |     pub fn full_path(&self, index: u32) -> DerivationPath { | ||||||
|  |         let mut final_path: Vec<ChildNumber> = self.path.clone().into(); | ||||||
|  |         let other_path: Vec<ChildNumber> = self.final_index.as_path(index).into(); | ||||||
|  |         final_path.extend_from_slice(&other_path); | ||||||
|  | 
 | ||||||
|  |         final_path.into() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn derive<C: secp256k1::Verification + secp256k1::Signing>( | ||||||
|  |         &self, | ||||||
|  |         ctx: &secp256k1::Secp256k1<C>, | ||||||
|  |         index: u32, | ||||||
|  |     ) -> Result<PublicKey, super::Error> { | ||||||
|  |         Ok(self.derive_xpub(ctx, index)?.public_key) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn derive_xpub<C: secp256k1::Verification + secp256k1::Signing>( | ||||||
|  |         &self, | ||||||
|  |         ctx: &secp256k1::Secp256k1<C>, | ||||||
|  |         index: u32, | ||||||
|  |     ) -> Result<ExtendedPubKey, super::Error> { | ||||||
|  |         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<C: secp256k1::Verification + secp256k1::Signing>( | ||||||
|  |         &self, | ||||||
|  |         ctx: &secp256k1::Secp256k1<C>, | ||||||
|  |     ) -> 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<DescriptorExtendedKey, Self::Err> { | ||||||
|  |         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")); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										494
									
								
								core/lib/src/descriptor/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										494
									
								
								core/lib/src/descriptor/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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<Self, Self::Err> { | ||||||
|  |         Ok(DummyKey::default()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl miniscript::MiniscriptKey for DummyKey { | ||||||
|  |     type Hash = DummyKey; | ||||||
|  | 
 | ||||||
|  |     fn to_pubkeyhash(&self) -> DummyKey { | ||||||
|  |         DummyKey::default() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub type DerivedDescriptor = Descriptor<PublicKey>; | ||||||
|  | pub type StringDescriptor = Descriptor<String>; | ||||||
|  | 
 | ||||||
|  | pub trait DescriptorMeta { | ||||||
|  |     fn is_witness(&self) -> bool; | ||||||
|  |     fn psbt_redeem_script(&self) -> Option<Script>; | ||||||
|  |     fn psbt_witness_script(&self) -> Option<Script>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<T> DescriptorMeta for Descriptor<T> | ||||||
|  | where | ||||||
|  |     T: miniscript::MiniscriptKey + miniscript::ToPublicKey, | ||||||
|  | { | ||||||
|  |     fn is_witness(&self) -> bool { | ||||||
|  |         match self { | ||||||
|  |             Descriptor::Bare(_) | Descriptor::Pk(_) | Descriptor::Pkh(_) | Descriptor::Sh(_) => { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |             Descriptor::Wpkh(_) | ||||||
|  |             | Descriptor::ShWpkh(_) | ||||||
|  |             | Descriptor::Wsh(_) | ||||||
|  |             | Descriptor::ShWsh(_) => true, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn psbt_redeem_script(&self) -> Option<Script> { | ||||||
|  |         match self { | ||||||
|  |             Descriptor::ShWpkh(ref pk) => { | ||||||
|  |                 let addr = | ||||||
|  |                     bitcoin::Address::p2shwpkh(&pk.to_public_key(), bitcoin::Network::Bitcoin); | ||||||
|  |                 Some(addr.script_pubkey()) | ||||||
|  |             } | ||||||
|  |             Descriptor::ShWsh(ref script) => Some(script.encode().to_v0_p2wsh()), | ||||||
|  |             Descriptor::Sh(ref script) => Some(script.encode()), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn psbt_witness_script(&self) -> Option<Script> { | ||||||
|  |         match self { | ||||||
|  |             Descriptor::Wsh(ref script) => Some(script.encode()), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | trait Key: std::fmt::Debug { | ||||||
|  |     fn fingerprint(&self, secp: &Secp256k1<All>) -> Option<Fingerprint>; | ||||||
|  |     fn as_public_key(&self, secp: &Secp256k1<All>, index: Option<u32>) -> Result<PublicKey, Error>; | ||||||
|  |     fn as_secret_key( | ||||||
|  |         &self, | ||||||
|  |         secp: &Secp256k1<All>, | ||||||
|  |         index: Option<u32>, | ||||||
|  |     ) -> Result<Option<PrivateKey>, Error>; | ||||||
|  |     fn xprv(&self) -> Option<ExtendedPrivKey>; | ||||||
|  |     fn full_path(&self, index: u32) -> Option<DerivationPath>; | ||||||
|  |     fn is_fixed(&self) -> bool; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Key for PublicKey { | ||||||
|  |     fn fingerprint(&self, _secp: &Secp256k1<All>) -> Option<Fingerprint> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn as_public_key( | ||||||
|  |         &self, | ||||||
|  |         _secp: &Secp256k1<All>, | ||||||
|  |         _index: Option<u32>, | ||||||
|  |     ) -> Result<PublicKey, Error> { | ||||||
|  |         Ok(PublicKey::clone(self)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn as_secret_key( | ||||||
|  |         &self, | ||||||
|  |         _secp: &Secp256k1<All>, | ||||||
|  |         _index: Option<u32>, | ||||||
|  |     ) -> Result<Option<PrivateKey>, Error> { | ||||||
|  |         Ok(None) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn xprv(&self) -> Option<ExtendedPrivKey> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn full_path(&self, _index: u32) -> Option<DerivationPath> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn is_fixed(&self) -> bool { | ||||||
|  |         true | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Key for PrivateKey { | ||||||
|  |     fn fingerprint(&self, _secp: &Secp256k1<All>) -> Option<Fingerprint> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn as_public_key( | ||||||
|  |         &self, | ||||||
|  |         secp: &Secp256k1<All>, | ||||||
|  |         _index: Option<u32>, | ||||||
|  |     ) -> Result<PublicKey, Error> { | ||||||
|  |         Ok(self.public_key(secp)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn as_secret_key( | ||||||
|  |         &self, | ||||||
|  |         _secp: &Secp256k1<All>, | ||||||
|  |         _index: Option<u32>, | ||||||
|  |     ) -> Result<Option<PrivateKey>, Error> { | ||||||
|  |         Ok(Some(PrivateKey::clone(self))) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn xprv(&self) -> Option<ExtendedPrivKey> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn full_path(&self, _index: u32) -> Option<DerivationPath> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn is_fixed(&self) -> bool { | ||||||
|  |         true | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Key for DescriptorExtendedKey { | ||||||
|  |     fn fingerprint(&self, secp: &Secp256k1<All>) -> Option<Fingerprint> { | ||||||
|  |         Some(self.root_xpub(secp).fingerprint()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn as_public_key(&self, secp: &Secp256k1<All>, index: Option<u32>) -> Result<PublicKey, Error> { | ||||||
|  |         Ok(self.derive_xpub(secp, index.unwrap_or(0))?.public_key) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn as_secret_key( | ||||||
|  |         &self, | ||||||
|  |         secp: &Secp256k1<All>, | ||||||
|  |         index: Option<u32>, | ||||||
|  |     ) -> Result<Option<PrivateKey>, Error> { | ||||||
|  |         if self.secret.is_none() { | ||||||
|  |             return Ok(None); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let derivation_path = self.full_path(index.unwrap_or(0)); | ||||||
|  |         Ok(Some( | ||||||
|  |             self.secret | ||||||
|  |                 .unwrap() | ||||||
|  |                 .derive_priv(secp, &derivation_path)? | ||||||
|  |                 .private_key, | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn xprv(&self) -> Option<ExtendedPrivKey> { | ||||||
|  |         self.secret | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn full_path(&self, index: u32) -> Option<DerivationPath> { | ||||||
|  |         Some(self.full_path(index)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn is_fixed(&self) -> bool { | ||||||
|  |         self.final_index == DerivationIndex::Fixed | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[serde(try_from = "&str", into = "String")] | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | pub struct ExtendedDescriptor { | ||||||
|  |     #[serde(flatten)] | ||||||
|  |     internal: StringDescriptor, | ||||||
|  | 
 | ||||||
|  |     #[serde(skip)] | ||||||
|  |     keys: BTreeMap<String, Box<dyn Key>>, | ||||||
|  | 
 | ||||||
|  |     #[serde(skip)] | ||||||
|  |     ctx: Secp256k1<All>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl std::clone::Clone for ExtendedDescriptor { | ||||||
|  |     fn clone(&self) -> Self { | ||||||
|  |         Self { | ||||||
|  |             internal: self.internal.clone(), | ||||||
|  |             ctx: self.ctx.clone(), | ||||||
|  |             keys: BTreeMap::new(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ExtendedDescriptor { | ||||||
|  |     fn parse_string(string: &str) -> Result<(String, Box<dyn Key>), Error> { | ||||||
|  |         if let Ok(pk) = PublicKey::from_str(string) { | ||||||
|  |             return Ok((string.to_string(), Box::new(pk))); | ||||||
|  |         } else if let Ok(sk) = PrivateKey::from_wif(string) { | ||||||
|  |             return Ok((string.to_string(), Box::new(sk))); | ||||||
|  |         } else if let Ok(ext_key) = DescriptorExtendedKey::from_str(string) { | ||||||
|  |             return Ok((string.to_string(), Box::new(ext_key))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Err(Error::KeyParsingError(string.to_string())); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn new(sd: StringDescriptor) -> Result<Self, Error> { | ||||||
|  |         let ctx = Secp256k1::gen_new(); | ||||||
|  |         let keys: RefCell<BTreeMap<String, Box<dyn Key>>> = RefCell::new(BTreeMap::new()); | ||||||
|  | 
 | ||||||
|  |         let translatefpk = |string: &String| -> Result<_, Error> { | ||||||
|  |             let (key, parsed) = Self::parse_string(string)?; | ||||||
|  |             keys.borrow_mut().insert(key, parsed); | ||||||
|  | 
 | ||||||
|  |             Ok(DummyKey::default()) | ||||||
|  |         }; | ||||||
|  |         let translatefpkh = |string: &String| -> Result<_, Error> { | ||||||
|  |             let (key, parsed) = Self::parse_string(string)?; | ||||||
|  |             keys.borrow_mut().insert(key, parsed); | ||||||
|  | 
 | ||||||
|  |             Ok(DummyKey::default()) | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         sd.translate_pk(translatefpk, translatefpkh)?; | ||||||
|  | 
 | ||||||
|  |         Ok(ExtendedDescriptor { | ||||||
|  |             internal: sd, | ||||||
|  |             keys: keys.into_inner(), | ||||||
|  |             ctx, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn derive(&self, index: u32) -> Result<DerivedDescriptor, Error> { | ||||||
|  |         let translatefpk = |xpub: &String| { | ||||||
|  |             self.keys | ||||||
|  |                 .get(xpub) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .as_public_key(&self.ctx, Some(index)) | ||||||
|  |         }; | ||||||
|  |         let translatefpkh = | ||||||
|  |             |xpub: &String| Ok(hash160::Hash::hash(&translatefpk(xpub)?.to_bytes())); | ||||||
|  | 
 | ||||||
|  |         Ok(self.internal.translate_pk(translatefpk, translatefpkh)?) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn get_xprv(&self) -> Vec<ExtendedPrivKey> { | ||||||
|  |         self.keys | ||||||
|  |             .iter() | ||||||
|  |             .filter(|(_, v)| v.xprv().is_some()) | ||||||
|  |             .map(|(_, v)| v.xprv().unwrap()) | ||||||
|  |             .collect() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn get_hd_keypaths( | ||||||
|  |         &self, | ||||||
|  |         index: u32, | ||||||
|  |     ) -> Result<BTreeMap<PublicKey, (Fingerprint, DerivationPath)>, Error> { | ||||||
|  |         let mut answer = BTreeMap::new(); | ||||||
|  | 
 | ||||||
|  |         for (_, key) in &self.keys { | ||||||
|  |             if let Some(fingerprint) = key.fingerprint(&self.ctx) { | ||||||
|  |                 let derivation_path = key.full_path(index).unwrap(); | ||||||
|  |                 let pubkey = key.as_public_key(&self.ctx, Some(index))?; | ||||||
|  | 
 | ||||||
|  |                 answer.insert(pubkey, (fingerprint, derivation_path)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(answer) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn max_satisfaction_weight(&self) -> usize { | ||||||
|  |         let fake_pk = PublicKey::from_slice(&[ | ||||||
|  |             2, 140, 40, 169, 123, 248, 41, 139, 192, 210, 61, 140, 116, 148, 82, 163, 46, 105, 75, | ||||||
|  |             101, 227, 10, 148, 114, 163, 149, 74, 179, 15, 229, 50, 76, 170, | ||||||
|  |         ]) | ||||||
|  |         .unwrap(); | ||||||
|  |         let translated: Descriptor<PublicKey> = self | ||||||
|  |             .internal | ||||||
|  |             .translate_pk( | ||||||
|  |                 |_| -> Result<_, ()> { Ok(fake_pk.clone()) }, | ||||||
|  |                 |_| -> Result<_, ()> { Ok(Default::default()) }, | ||||||
|  |             ) | ||||||
|  |             .unwrap(); | ||||||
|  | 
 | ||||||
|  |         translated.max_satisfaction_weight() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn is_fixed(&self) -> bool { | ||||||
|  |         self.keys.iter().all(|(_, key)| key.is_fixed()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TryFrom<&str> for ExtendedDescriptor { | ||||||
|  |     type Error = Error; | ||||||
|  | 
 | ||||||
|  |     fn try_from(value: &str) -> Result<Self, Self::Error> { | ||||||
|  |         let internal = StringDescriptor::from_str(value)?; | ||||||
|  |         ExtendedDescriptor::new(internal) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TryFrom<StringDescriptor> for ExtendedDescriptor { | ||||||
|  |     type Error = Error; | ||||||
|  | 
 | ||||||
|  |     fn try_from(other: StringDescriptor) -> Result<Self, Self::Error> { | ||||||
|  |         ExtendedDescriptor::new(other) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl FromStr for ExtendedDescriptor { | ||||||
|  |     type Err = Error; | ||||||
|  | 
 | ||||||
|  |     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||||
|  |         Self::try_from(s) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Into<String> for ExtendedDescriptor { | ||||||
|  |     fn into(self) -> String { | ||||||
|  |         format!("{}", self.internal) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod test { | ||||||
|  |     use std::str::FromStr; | ||||||
|  | 
 | ||||||
|  |     use bitcoin::hashes::hex::FromHex; | ||||||
|  |     use bitcoin::{Network, PublicKey}; | ||||||
|  | 
 | ||||||
|  |     use crate::descriptor::*; | ||||||
|  | 
 | ||||||
|  |     macro_rules! hex_fingerprint { | ||||||
|  |         ($hex:expr) => { | ||||||
|  |             Fingerprint::from_hex($hex).unwrap() | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     macro_rules! hex_pubkey { | ||||||
|  |         ($hex:expr) => { | ||||||
|  |             PublicKey::from_str($hex).unwrap() | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     macro_rules! deriv_path { | ||||||
|  |         ($str:expr) => { | ||||||
|  |             DerivationPath::from_str($str).unwrap() | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         () => { | ||||||
|  |             DerivationPath::from(vec![]) | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_descriptor_parse_wif() { | ||||||
|  |         let string = "pkh(cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy)"; | ||||||
|  |         let desc = ExtendedDescriptor::from_str(string).unwrap(); | ||||||
|  |         assert!(desc.is_fixed()); | ||||||
|  |         assert_eq!( | ||||||
|  |             desc.derive(0) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .address(Network::Testnet) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .to_string(), | ||||||
|  |             "mqwpxxvfv3QbM8PU8uBx2jaNt9btQqvQNx" | ||||||
|  |         ); | ||||||
|  |         assert_eq!( | ||||||
|  |             desc.derive(42) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .address(Network::Testnet) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .to_string(), | ||||||
|  |             "mqwpxxvfv3QbM8PU8uBx2jaNt9btQqvQNx" | ||||||
|  |         ); | ||||||
|  |         assert_eq!(desc.get_secret_keys().len(), 1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_descriptor_parse_pubkey() { | ||||||
|  |         let string = "pkh(039b6347398505f5ec93826dc61c19f47c66c0283ee9be980e29ce325a0f4679ef)"; | ||||||
|  |         let desc = ExtendedDescriptor::from_str(string).unwrap(); | ||||||
|  |         assert!(desc.is_fixed()); | ||||||
|  |         assert_eq!( | ||||||
|  |             desc.derive(0) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .address(Network::Testnet) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .to_string(), | ||||||
|  |             "mqwpxxvfv3QbM8PU8uBx2jaNt9btQqvQNx" | ||||||
|  |         ); | ||||||
|  |         assert_eq!( | ||||||
|  |             desc.derive(42) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .address(Network::Testnet) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .to_string(), | ||||||
|  |             "mqwpxxvfv3QbM8PU8uBx2jaNt9btQqvQNx" | ||||||
|  |         ); | ||||||
|  |         assert_eq!(desc.get_secret_keys().len(), 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_descriptor_parse_xpub() { | ||||||
|  |         let string = "pkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/*)"; | ||||||
|  |         let desc = ExtendedDescriptor::from_str(string).unwrap(); | ||||||
|  |         assert!(!desc.is_fixed()); | ||||||
|  |         assert_eq!( | ||||||
|  |             desc.derive(0) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .address(Network::Testnet) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .to_string(), | ||||||
|  |             "mxbXpnVkwARGtYXk5yeGYf59bGWuPpdE4X" | ||||||
|  |         ); | ||||||
|  |         assert_eq!( | ||||||
|  |             desc.derive(42) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .address(Network::Testnet) | ||||||
|  |                 .unwrap() | ||||||
|  |                 .to_string(), | ||||||
|  |             "mhtuS1QaEV4HPcK4bWk4Wvpd64SUjiC5Zt" | ||||||
|  |         ); | ||||||
|  |         assert_eq!(desc.get_xprv().len(), 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     #[should_panic(expected = "KeyParsingError")] | ||||||
|  |     fn test_descriptor_parse_fail() { | ||||||
|  |         let string = "pkh(this_is_not_a_valid_key)"; | ||||||
|  |         ExtendedDescriptor::from_str(string).unwrap(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_descriptor_hd_keypaths() { | ||||||
|  |         let string = "pkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/*)"; | ||||||
|  |         let desc = ExtendedDescriptor::from_str(string).unwrap(); | ||||||
|  |         let keypaths = desc.get_hd_keypaths(0).unwrap(); | ||||||
|  |         assert!(keypaths.contains_key(&hex_pubkey!( | ||||||
|  |             "025d5fc65ebb8d44a5274b53bac21ff8307fec2334a32df05553459f8b1f7fe1b6" | ||||||
|  |         ))); | ||||||
|  |         assert_eq!( | ||||||
|  |             keypaths.get(&hex_pubkey!( | ||||||
|  |                 "025d5fc65ebb8d44a5274b53bac21ff8307fec2334a32df05553459f8b1f7fe1b6" | ||||||
|  |             )), | ||||||
|  |             Some(&(hex_fingerprint!("31a507b8"), deriv_path!("m/0"))) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								core/lib/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								core/lib/src/error.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum Error { | ||||||
|  |     KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey), | ||||||
|  |     MissingInputUTXO(usize), | ||||||
|  | 
 | ||||||
|  |     BIP32(bitcoin::util::bip32::Error), | ||||||
|  |     Secp256k1(bitcoin::secp256k1::Error), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | macro_rules! impl_error { | ||||||
|  |     ( $from:ty, $to:ident ) => { | ||||||
|  |         impl std::convert::From<$from> for Error { | ||||||
|  |             fn from(err: $from) -> Self { | ||||||
|  |                 Error::$to(err) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl_error!(bitcoin::util::bip32::Error, BIP32); | ||||||
|  | impl_error!(bitcoin::secp256k1::Error, Secp256k1); | ||||||
| @ -1,7 +1,8 @@ | |||||||
| #[cfg(test)] | pub extern crate bitcoin; | ||||||
| mod tests { | extern crate log; | ||||||
|     #[test] | pub extern crate miniscript; | ||||||
|     fn it_works() { | extern crate serde; | ||||||
|         assert_eq!(2 + 2, 4); | extern crate serde_json; | ||||||
|     } | 
 | ||||||
| } | pub mod descriptor; | ||||||
|  | pub mod error; | ||||||
|  | |||||||
							
								
								
									
										198
									
								
								core/lib/src/psbt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								core/lib/src/psbt.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,198 @@ | |||||||
|  | use std::collections::BTreeMap; | ||||||
|  | 
 | ||||||
|  | use bitcoin::hashes::Hash; | ||||||
|  | use bitcoin::util::bip143::SighashComponents; | ||||||
|  | use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, Fingerprint}; | ||||||
|  | use bitcoin::util::psbt; | ||||||
|  | use bitcoin::{PrivateKey, PublicKey, Script, SigHashType, Transaction}; | ||||||
|  | 
 | ||||||
|  | use bitcoin::secp256k1::{self, All, Message, Secp256k1}; | ||||||
|  | 
 | ||||||
|  | use miniscript::{BitcoinSig, Satisfier}; | ||||||
|  | 
 | ||||||
|  | use crate::descriptor::ExtendedDescriptor; | ||||||
|  | use crate::error::Error; | ||||||
|  | use crate::signer::Signer; | ||||||
|  | 
 | ||||||
|  | pub struct PSBTSatisfier<'a> { | ||||||
|  |     input: &'a psbt::Input, | ||||||
|  |     create_height: Option<u32>, | ||||||
|  |     current_height: Option<u32>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> PSBTSatisfier<'a> { | ||||||
|  |     pub fn new( | ||||||
|  |         input: &'a psbt::Input, | ||||||
|  |         create_height: Option<u32>, | ||||||
|  |         current_height: Option<u32>, | ||||||
|  |     ) -> Self { | ||||||
|  |         PSBTSatisfier { | ||||||
|  |             input, | ||||||
|  |             create_height, | ||||||
|  |             current_height, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: also support hash preimages through the "unknown" section of PSBT
 | ||||||
|  | impl<'a> Satisfier<bitcoin::PublicKey> for PSBTSatisfier<'a> { | ||||||
|  |     // from https://docs.rs/miniscript/0.12.0/src/miniscript/psbt/mod.rs.html#96
 | ||||||
|  |     fn lookup_sig(&self, pk: &bitcoin::PublicKey) -> Option<BitcoinSig> { | ||||||
|  |         if let Some(rawsig) = self.input.partial_sigs.get(pk) { | ||||||
|  |             let (flag, sig) = rawsig.split_last().unwrap(); | ||||||
|  |             let flag = bitcoin::SigHashType::from_u32(*flag as u32); | ||||||
|  |             let sig = match secp256k1::Signature::from_der(sig) { | ||||||
|  |                 Ok(sig) => sig, | ||||||
|  |                 Err(..) => return None, | ||||||
|  |             }; | ||||||
|  |             Some((sig, flag)) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn check_older(&self, height: u32) -> bool { | ||||||
|  |         // TODO: test >= / >
 | ||||||
|  |         self.current_height.unwrap_or(0) >= self.create_height.unwrap_or(0) + height | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn check_after(&self, height: u32) -> bool { | ||||||
|  |         self.current_height.unwrap_or(0) > height | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct PSBTSigner<'a> { | ||||||
|  |     tx: &'a Transaction, | ||||||
|  |     secp: Secp256k1<All>, | ||||||
|  | 
 | ||||||
|  |     // psbt: &'b psbt::PartiallySignedTransaction,
 | ||||||
|  |     extended_keys: BTreeMap<Fingerprint, ExtendedPrivKey>, | ||||||
|  |     private_keys: BTreeMap<PublicKey, PrivateKey>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> PSBTSigner<'a> { | ||||||
|  |     pub fn from_descriptor(tx: &'a Transaction, desc: &ExtendedDescriptor) -> Result<Self, Error> { | ||||||
|  |         let secp = Secp256k1::gen_new(); | ||||||
|  | 
 | ||||||
|  |         let mut extended_keys = BTreeMap::new(); | ||||||
|  |         for xprv in desc.get_xprv() { | ||||||
|  |             let fing = xprv.fingerprint(&secp); | ||||||
|  |             extended_keys.insert(fing, xprv); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let mut private_keys = BTreeMap::new(); | ||||||
|  |         for privkey in desc.get_secret_keys() { | ||||||
|  |             let pubkey = privkey.public_key(&secp); | ||||||
|  |             private_keys.insert(pubkey, privkey); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(PSBTSigner { | ||||||
|  |             tx, | ||||||
|  |             secp, | ||||||
|  |             extended_keys, | ||||||
|  |             private_keys, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> Signer for PSBTSigner<'a> { | ||||||
|  |     fn sig_legacy_from_fingerprint( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         fingerprint: &Fingerprint, | ||||||
|  |         path: &DerivationPath, | ||||||
|  |         script: &Script, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         self.extended_keys | ||||||
|  |             .get(fingerprint) | ||||||
|  |             .map_or(Ok(None), |xprv| { | ||||||
|  |                 let privkey = xprv.derive_priv(&self.secp, path)?; | ||||||
|  |                 // let derived_pubkey = secp256k1::PublicKey::from_secret_key(&self.secp, &privkey.private_key.key);
 | ||||||
|  | 
 | ||||||
|  |                 let hash = self.tx.signature_hash(index, script, sighash.as_u32()); | ||||||
|  | 
 | ||||||
|  |                 let signature = self.secp.sign( | ||||||
|  |                     &Message::from_slice(&hash.into_inner()[..])?, | ||||||
|  |                     &privkey.private_key.key, | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 Ok(Some((signature, sighash))) | ||||||
|  |             }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn sig_legacy_from_pubkey( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         public_key: &PublicKey, | ||||||
|  |         script: &Script, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         self.private_keys | ||||||
|  |             .get(public_key) | ||||||
|  |             .map_or(Ok(None), |privkey| { | ||||||
|  |                 let hash = self.tx.signature_hash(index, script, sighash.as_u32()); | ||||||
|  | 
 | ||||||
|  |                 let signature = self | ||||||
|  |                     .secp | ||||||
|  |                     .sign(&Message::from_slice(&hash.into_inner()[..])?, &privkey.key); | ||||||
|  | 
 | ||||||
|  |                 Ok(Some((signature, sighash))) | ||||||
|  |             }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn sig_segwit_from_fingerprint( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         fingerprint: &Fingerprint, | ||||||
|  |         path: &DerivationPath, | ||||||
|  |         script: &Script, | ||||||
|  |         value: u64, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         self.extended_keys | ||||||
|  |             .get(fingerprint) | ||||||
|  |             .map_or(Ok(None), |xprv| { | ||||||
|  |                 let privkey = xprv.derive_priv(&self.secp, path)?; | ||||||
|  | 
 | ||||||
|  |                 let hash = SighashComponents::new(self.tx).sighash_all( | ||||||
|  |                     &self.tx.input[index], | ||||||
|  |                     script, | ||||||
|  |                     value, | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 let signature = self.secp.sign( | ||||||
|  |                     &Message::from_slice(&hash.into_inner()[..])?, | ||||||
|  |                     &privkey.private_key.key, | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 Ok(Some((signature, sighash))) | ||||||
|  |             }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn sig_segwit_from_pubkey( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         public_key: &PublicKey, | ||||||
|  |         script: &Script, | ||||||
|  |         value: u64, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         self.private_keys | ||||||
|  |             .get(public_key) | ||||||
|  |             .map_or(Ok(None), |privkey| { | ||||||
|  |                 let hash = SighashComponents::new(self.tx).sighash_all( | ||||||
|  |                     &self.tx.input[index], | ||||||
|  |                     script, | ||||||
|  |                     value, | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 let signature = self | ||||||
|  |                     .secp | ||||||
|  |                     .sign(&Message::from_slice(&hash.into_inner()[..])?, &privkey.key); | ||||||
|  | 
 | ||||||
|  |                 Ok(Some((signature, sighash))) | ||||||
|  |             }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										87
									
								
								core/lib/src/signer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								core/lib/src/signer.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | use bitcoin::util::bip32::{DerivationPath, Fingerprint}; | ||||||
|  | use bitcoin::{PublicKey, Script, SigHashType}; | ||||||
|  | 
 | ||||||
|  | use miniscript::miniscript::satisfy::BitcoinSig; | ||||||
|  | 
 | ||||||
|  | use crate::error::Error; | ||||||
|  | 
 | ||||||
|  | pub trait Signer { | ||||||
|  |     fn sig_legacy_from_fingerprint( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         fingerprint: &Fingerprint, | ||||||
|  |         path: &DerivationPath, | ||||||
|  |         script: &Script, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error>; | ||||||
|  |     fn sig_legacy_from_pubkey( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         public_key: &PublicKey, | ||||||
|  |         script: &Script, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error>; | ||||||
|  | 
 | ||||||
|  |     fn sig_segwit_from_fingerprint( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         fingerprint: &Fingerprint, | ||||||
|  |         path: &DerivationPath, | ||||||
|  |         script: &Script, | ||||||
|  |         value: u64, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error>; | ||||||
|  |     fn sig_segwit_from_pubkey( | ||||||
|  |         &self, | ||||||
|  |         index: usize, | ||||||
|  |         sighash: SigHashType, | ||||||
|  |         public_key: &PublicKey, | ||||||
|  |         script: &Script, | ||||||
|  |         value: u64, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[allow(dead_code)] | ||||||
|  | impl dyn Signer { | ||||||
|  |     fn sig_legacy_from_fingerprint( | ||||||
|  |         &self, | ||||||
|  |         _index: usize, | ||||||
|  |         _sighash: SigHashType, | ||||||
|  |         _fingerprint: &Fingerprint, | ||||||
|  |         _path: &DerivationPath, | ||||||
|  |         _script: &Script, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         Ok(None) | ||||||
|  |     } | ||||||
|  |     fn sig_legacy_from_pubkey( | ||||||
|  |         &self, | ||||||
|  |         _index: usize, | ||||||
|  |         _sighash: SigHashType, | ||||||
|  |         _public_key: &PublicKey, | ||||||
|  |         _script: &Script, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         Ok(None) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn sig_segwit_from_fingerprint( | ||||||
|  |         &self, | ||||||
|  |         _index: usize, | ||||||
|  |         _sighash: SigHashType, | ||||||
|  |         _fingerprint: &Fingerprint, | ||||||
|  |         _path: &DerivationPath, | ||||||
|  |         _script: &Script, | ||||||
|  |         _value: u64, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         Ok(None) | ||||||
|  |     } | ||||||
|  |     fn sig_segwit_from_pubkey( | ||||||
|  |         &self, | ||||||
|  |         _index: usize, | ||||||
|  |         _sighash: SigHashType, | ||||||
|  |         _public_key: &PublicKey, | ||||||
|  |         _script: &Script, | ||||||
|  |         _value: u64, | ||||||
|  |     ) -> Result<Option<BitcoinSig>, Error> { | ||||||
|  |         Ok(None) | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user