diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index b512ea29..93cf2a56 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -24,6 +24,7 @@ parse_descriptor_secret, roundtrip_descriptor, roundtrip_concrete, compile_descriptor, +roundtrip_confidential, ] steps: - name: Install test dependencies diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 75c28e3e..4f09e76e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -44,3 +44,7 @@ path = "fuzz_targets/roundtrip_concrete.rs" [[bin]] name = "compile_descriptor" path = "fuzz_targets/compile_descriptor.rs" + +[[bin]] +name = "roundtrip_confidential" +path = "fuzz_targets/roundtrip_confidential.rs" diff --git a/fuzz/fuzz_targets/roundtrip_confidential.rs b/fuzz/fuzz_targets/roundtrip_confidential.rs new file mode 100644 index 00000000..58fc5057 --- /dev/null +++ b/fuzz/fuzz_targets/roundtrip_confidential.rs @@ -0,0 +1,38 @@ +extern crate elements_miniscript as miniscript; +extern crate regex; + +use std::str::FromStr; + +use miniscript::confidential; + +fn do_test(data: &[u8]) { + // This is how we test in rust-miniscript. It is difficult to enforce wrapping logic in fuzzer + // for alias like t: and_v(1), likely and unlikely. + // Just directly check whether the inferred descriptor is the same. + let s = String::from_utf8_lossy(data); + if let Ok(desc) = confidential::Descriptor::::from_str(&s) { + let str2 = desc.to_string(); + let desc2 = confidential::Descriptor::::from_str(&str2).unwrap(); + + assert_eq!(desc.to_string(), desc2.to_string()); + } +} + +fn main() { + loop { + honggfuzz::fuzz!(|data| { + do_test(data); + }); + } +} + +#[cfg(test)] +mod tests { + use miniscript::elements::hex::FromHex; + + #[test] + fn duplicate_crash() { + let hex = Vec::::from_hex("00").unwrap(); + super::do_test(&hex); + } +} diff --git a/src/confidential/bare.rs b/src/confidential/bare.rs new file mode 100644 index 00000000..d19edb71 --- /dev/null +++ b/src/confidential/bare.rs @@ -0,0 +1,122 @@ +// Miniscript +// Written in 2023 by +// Andrew Poelstra +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to +// the public domain worldwide. This software is distributed without +// any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication +// along with this software. +// If not, see . +// + +//! "Bare Key" Confidential Descriptors + +use bitcoin::hashes::{sha256t_hash_newtype, Hash}; +use elements::encode::Encodable; +use elements::secp256k1_zkp; + +use crate::ToPublicKey; + +/// The SHA-256 initial midstate value for the [`TweakHash`]. +const MIDSTATE_HASH_TO_PRIVATE_HASH: [u8; 32] = [ + 0x2f, 0x85, 0x61, 0xec, 0x30, 0x88, 0xad, 0xa9, 0x5a, 0xe7, 0x43, 0xcd, 0x3c, 0x5f, 0x59, 0x7d, + 0xc0, 0x4b, 0xd0, 0x7f, 0x06, 0x5f, 0x1c, 0x06, 0x47, 0x89, 0x36, 0x63, 0xf3, 0x92, 0x6e, 0x65, +]; + +sha256t_hash_newtype!( + TweakHash, + TweakTag, + MIDSTATE_HASH_TO_PRIVATE_HASH, + 64, + doc = "BIP-340 Tagged hash for tweaking blinding keys", + forward +); + +/// Tweaks a bare key using the scriptPubKey of a descriptor +pub fn tweak_key<'a, Pk, V>( + secp: &secp256k1_zkp::Secp256k1, + spk: &elements::Script, + pk: &Pk, +) -> secp256k1_zkp::PublicKey +where + Pk: ToPublicKey + 'a, + V: secp256k1_zkp::Verification, +{ + let mut eng = TweakHash::engine(); + pk.to_public_key() + .write_into(&mut eng) + .expect("engines don't error"); + spk.consensus_encode(&mut eng).expect("engines don't error"); + let hash_bytes = TweakHash::from_engine(eng).to_byte_array(); + let hash_scalar = secp256k1_zkp::Scalar::from_be_bytes(hash_bytes).expect("bytes from hash"); + pk.to_public_key() + .inner + .add_exp_tweak(secp, &hash_scalar) + .unwrap() +} + +/// Tweaks a bare key using the scriptPubKey of a descriptor +pub fn tweak_private_key<'a, Pk, V>( + secp: &secp256k1_zkp::Secp256k1, + spk: &elements::Script, + pk: &Pk, +) -> secp256k1_zkp::PublicKey +where + Pk: ToPublicKey + 'a, + V: secp256k1_zkp::Verification, +{ + let mut eng = TweakHash::engine(); + pk.to_public_key() + .write_into(&mut eng) + .expect("engines don't error"); + spk.consensus_encode(&mut eng).expect("engines don't error"); + let hash_bytes = TweakHash::from_engine(eng).to_byte_array(); + let hash_scalar = secp256k1_zkp::Scalar::from_be_bytes(hash_bytes).expect("bytes from hash"); + pk.to_public_key() + .inner + .add_exp_tweak(secp, &hash_scalar) + .unwrap() +} + +#[cfg(test)] +mod tests { + use bitcoin::hashes::sha256t::Tag; + use bitcoin::hashes::{sha256, HashEngine}; + + use super::*; + + #[test] + fn tagged_hash() { + // Check that cached midstate is computed correctly + // This code taken from `tag_engine` in the rust-bitcoin tests; it is identical + // to that used by the BIP-0340 hashes in Taproot + let mut engine = sha256::Hash::engine(); + let tag_hash = sha256::Hash::hash(b"CT-Blinding-Key/1.0"); + engine.input(&tag_hash[..]); + engine.input(&tag_hash[..]); + assert_eq!( + MIDSTATE_HASH_TO_PRIVATE_HASH, + engine.midstate().to_byte_array() + ); + + // Test empty hash + assert_eq!( + TweakHash::from_engine(TweakTag::engine()).to_string(), + "d12a140aca856fbb917b931f263c42f064608985e2ce17ae5157daa17c55e8d9", + ); + assert_eq!( + TweakHash::hash(&[]).to_string(), + "d12a140aca856fbb917b931f263c42f064608985e2ce17ae5157daa17c55e8d9", + ); + + // And hash of 100 bytes + let data: Vec = (0..80).collect(); + assert_eq!( + TweakHash::hash(&data).to_string(), + "e1e52419a2934d278c50e29608969d2f23c1bd1243a09bfc8026d4ed4b085e39", + ); + } +} diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs new file mode 100644 index 00000000..6f3e21d6 --- /dev/null +++ b/src/confidential/mod.rs @@ -0,0 +1,399 @@ +// Miniscript +// Written in 2022 by +// Andrew Poelstra +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to +// the public domain worldwide. This software is distributed without +// any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication +// along with this software. +// If not, see . +// + +//! Confidential Descriptors +//! +//! Implements ELIP ????, described at `URL` +//! + +pub mod bare; +pub mod slip77; + +use std::fmt; + +use elements::secp256k1_zkp; + +use crate::descriptor::checksum::{desc_checksum, verify_checksum}; +use crate::descriptor::DescriptorSecretKey; +use crate::expression::FromTree; +use crate::extensions::{CovExtArgs, CovenantExt, Extension, ParseableExt}; +use crate::{expression, Error, MiniscriptKey, ToPublicKey}; + +/// A description of a blinding key +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Key { + /// Blinding key is computed using SLIP77 with the given master key + Slip77(slip77::MasterBlindingKey), + /// Blinding key is given directly + Bare(Pk), + /// Blinding key is given directly, as a secret key + View(DescriptorSecretKey), +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Key::Slip77(data) => write!(f, "slip77({})", data), + Key::Bare(pk) => fmt::Display::fmt(pk, f), + Key::View(sk) => fmt::Display::fmt(sk, f), + } + } +} + +impl Key { + fn to_public_key( + &self, + secp: &secp256k1_zkp::Secp256k1, + spk: &elements::Script, + ) -> secp256k1_zkp::PublicKey { + match *self { + Key::Slip77(ref mbk) => mbk.blinding_key(secp, spk), + Key::Bare(ref pk) => bare::tweak_key(secp, spk, pk), + Key::View(ref sk) => bare::tweak_key(secp, spk, &sk.to_public(secp).expect("view keys cannot be multipath keys").at_derivation_index(0).expect("FIXME deal with derivation paths properly")), + } + } +} + +/// A confidential descriptor +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Descriptor> { + /// The blinding key + pub key: Key, + /// The script descriptor + pub descriptor: crate::Descriptor, +} + +impl Descriptor { + /// Sanity checks for the underlying descriptor. + pub fn sanity_check(&self) -> Result<(), Error> { + self.descriptor.sanity_check()?; + Ok(()) + } +} + +impl Descriptor { + /// Obtains the unblinded address for this descriptor. + pub fn unconfidential_address( + &self, + params: &'static elements::AddressParams, + ) -> Result { + self.descriptor.address(params) + } + + /// Obtains the blinded address for this descriptor. + pub fn address( + &self, + secp: &secp256k1_zkp::Secp256k1, + params: &'static elements::AddressParams, + ) -> Result { + let spk = self.descriptor.script_pubkey(); + self.descriptor + .blinded_address(self.key.to_public_key(secp, &spk), params) + } +} + +impl fmt::Display for Descriptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc_str = format!("ct({},{:#})", self.key, self.descriptor); + let checksum = desc_checksum(&desc_str).map_err(|_| fmt::Error)?; + write!(f, "{}#{}", desc_str, checksum) + } +} + +impl_from_str!( + ;T; Extension, + Descriptor, + type Err = Error;, + fn from_str(s: &str) -> Result, Error> { + let desc_str = verify_checksum(s)?; + let top = expression::Tree::from_str(desc_str)?; + + if top.name != "ct" { + return Err(Error::BadDescriptor(String::from( + "Not a CT Descriptor", + ))); + } + if top.args.len() != 2 { + return Err(Error::BadDescriptor( + format!("CT descriptor had {} arguments rather than 2", top.args.len()) + )); + } + + let keyexpr = &top.args[0]; + Ok(Descriptor { + key: match (keyexpr.name, keyexpr.args.len()) { + ("slip77", 1) => Key::Slip77(expression::terminal(&keyexpr.args[0], slip77::MasterBlindingKey::from_str)?), + ("slip77", _) => return Err(Error::BadDescriptor( + "slip77() must have exactly one argument".to_owned() + )), + _ => expression::terminal(keyexpr, Pk::from_str).map(Key::Bare) + .or_else(|_| expression::terminal(keyexpr, DescriptorSecretKey::from_str).map(Key::View))?, + }, + descriptor: crate::Descriptor::from_tree(&top.args[1])?, + }) + } +); + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use elements::Address; + + use super::*; + use crate::{DefiniteDescriptorKey, NoExt}; + + #[test] + fn bare_addr_to_confidential() { + let secp = secp256k1_zkp::Secp256k1::new(); + + // taken from libwally src/test/test_confidential_addr.py + let mut addr = Address::from_str("Q7qcjTLsYGoMA7TjUp97R6E6AM5VKqBik6").unwrap(); + let key = Key::Bare( + bitcoin::PublicKey::from_str( + "02dce16018bbbb8e36de7b394df5b5166e9adb7498be7d881a85a09aeecf76b623", + ) + .unwrap(), + ); + addr.blinding_pubkey = Some(key.to_public_key(&secp, &addr.script_pubkey())); + assert_eq!( + addr.to_string(), + "VTpt7krqRQPJwqe3XQXPg2cVdEKYVFbuprTr7es7pNRMe8mndnq2iYWddxJWYowhLAwoDF8QrZ1v2EXv" + ); + } + + struct ConfidentialTest { + key: Key, + descriptor: crate::Descriptor, + descriptor_str: String, + conf_addr: &'static str, + unconf_addr: &'static str, + } + + impl ConfidentialTest { + fn check( + &self, + secp: &secp256k1_zkp::Secp256k1, + ) { + let desc: Descriptor = Descriptor { + key: self.key.clone(), + descriptor: self.descriptor.clone(), + }; + assert_eq!(self.descriptor_str, desc.to_string()); + assert_eq!(desc, Descriptor::from_str(&desc.to_string()).unwrap()); + assert_eq!( + self.conf_addr, + desc.address(secp, &elements::AddressParams::ELEMENTS) + .unwrap() + .to_string(), + ); + assert_eq!( + self.unconf_addr, + desc.unconfidential_address(&elements::AddressParams::ELEMENTS) + .unwrap() + .to_string(), + ); + } + + #[allow(dead_code)] + fn output_elip_test_vector(&self, index: usize) { + println!( + "* Valid Descriptor {}: {}", + index, self.descriptor_str + ); + match self.key { + Key::Bare(ref pk) => println!("** Blinding public key: {}", pk), + Key::View(ref sk) => println!("** Blinding private key: {}", sk), + Key::Slip77(mbk) => println!("** SLIP77 master blinding key: {}", mbk), + } + println!("** Confidential address: {}", self.conf_addr); + println!( + "** Unconfidential address: {}", + self.unconf_addr + ); + println!(); + } + } + + #[test] + fn confidential_descriptor() { + let secp = secp256k1_zkp::Secp256k1::new(); + + // CT key used for bare keys + let ct_key = DefiniteDescriptorKey::from_str( + "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", + ) + .unwrap(); + // Auxiliary key to create scriptpubkeys from + let spk_key = DefiniteDescriptorKey::from_str( + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + ) + .unwrap(); + + let tests = vec![ + // Bare key, P2PKH + ConfidentialTest { + key: Key::Bare(ct_key.clone()), + descriptor: crate::Descriptor::new_pkh(spk_key.clone()), + descriptor_str: format!("ct({},elpkh({}))#y0lg3d5y", ct_key, spk_key), + conf_addr: "CTEnDa5fqGccV3g3jvp4exSQwRfb6FpGchNBF4ZrAaq8ip8gvLqHCtzw1F7d7U5gYJYXBwymgEMmJjca", + unconf_addr: "2dhfebpgPWpeqPdCMMam5F2UHAgx3bbLzAg", + }, + // Bare key, P2WPKH + ConfidentialTest { + key: Key::Bare(ct_key.clone()), + descriptor: crate::Descriptor::new_wpkh(spk_key.clone()).unwrap(), + descriptor_str: format!("ct({},elwpkh({}))#kt4e25qt", ct_key, spk_key), + conf_addr: "el1qqg5s7xj7upzl7h4q2k2wj4vq63nvaktn0egqu09nqcr6d44p4evaqknpl78t02k2xqgdh9ltmfmpy9ssk7qfvrldr2dttt3ez", + unconf_addr: "ert1qtfsllr4h4t9rqyxmjl4a5asjzcgt0qyk32h3ur", + }, + // Bare key, P2SH-WPKH + ConfidentialTest { + key: Key::Bare(ct_key.clone()), + descriptor: crate::Descriptor::new_sh_wpkh(spk_key.clone()).unwrap(), + descriptor_str: format!("ct({},elsh(wpkh({})))#xg9r4jej", ct_key, spk_key), + conf_addr: "AzpnREsN1RSi4JB7rAfpywmPsvGxyygmwm9o3iZcP43svg4frVW5DXvGj5yEx6mKcPtAyHgQWVikFRCM", + unconf_addr: "XKGUGskfGsNRR1Ww4ytemgBjuszohUaNgv", + }, + // Bare key, P2TR + ConfidentialTest { + key: Key::Bare(ct_key.clone()), + descriptor: crate::Descriptor::new_tr(spk_key.clone(), None).unwrap(), + descriptor_str: format!("ct({},eltr({}))#c0pjjxyw", ct_key, spk_key), + conf_addr: "el1pq0nsl8du3gsuk7r90sgm78259mmv6mt9d4yvj30zr3u052ufs5meuc2tuvwx7k7g9kvhhpux07vqpm3qjj8uwdj94650265ustv0xy8z2pc847zht4k0", + unconf_addr: "ert1pv997x8r0t0yzmxtms7r8lxqqacsffr78xez6a284d2wg9k8nzr3q3s6527", + }, + // SLIP77, P2PKH + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_pkh(spk_key.clone()), + descriptor_str: format!("ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elpkh({}))#hw2glz99", spk_key), + conf_addr: "CTEvn67jjJXDr3aDCZypTCJc6XHZ7ATyd89oXfNLQt1G2omPUpPkHA6zUAGPGF2YH4RnWfWut2f4dRSd", + unconf_addr: "2dhfebpgPWpeqPdCMMam5F2UHAgx3bbLzAg", + }, + // SLIP77, P2WPKH + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_wpkh(spk_key.clone()).unwrap(), + descriptor_str: format!("ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elwpkh({}))#545pl285", spk_key), + conf_addr: "el1qqdx5wnttttzulcs6ujlg9pfts6mp3r4sdwg5ekdej566n5wxzk88vknpl78t02k2xqgdh9ltmfmpy9ssk7qfvge347y58xukt", + unconf_addr: "ert1qtfsllr4h4t9rqyxmjl4a5asjzcgt0qyk32h3ur", + }, + // SLIP77, P2SH + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_sh_wpkh(spk_key.clone()).unwrap(), + descriptor_str: format!("ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elsh(wpkh({})))#m30vswxr", spk_key), + conf_addr: "AzptgrWR3xVX6Qg8mbkyZiESb6C9uy8VCUdCCmw7UtceiF5H8PdB6933YDT7vHsevK1yFmxfajdaedCH", + unconf_addr: "XKGUGskfGsNRR1Ww4ytemgBjuszohUaNgv", + }, + // SLIP77, P2TR + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_tr(spk_key.clone(), None).unwrap(), + descriptor_str: format!("ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),eltr({}))#n3v4t5cs", spk_key), + conf_addr: "el1pq26fndnz8ef6umlz6e2755sm6j5jwxv3tdt2295mr4mx6ux0uf8vcc2tuvwx7k7g9kvhhpux07vqpm3qjj8uwdj94650265ustv0xy8zwzhhycxfhdrm", + unconf_addr: "ert1pv997x8r0t0yzmxtms7r8lxqqacsffr78xez6a284d2wg9k8nzr3q3s6527", + }, + ]; + + for test in &tests { + test.check(&secp); + } + // Uncomment to regenerate test vectors; to see the output, run + // cargo test confidential::tests:;confidential_descriptor -- --nocapture + /* + for (n, test) in tests.iter().enumerate() { + test.output_elip_test_vector(n + 1); + } + */ + } + + #[test] + fn confidential_descriptor_invalid() { + let bad_strs = vec![ + ( + "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elsh(wpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4)))#xxxxxxxx", + "Invalid descriptor: Invalid checksum 'xxxxxxxx', expected 'qgjmm4as'", + ), + ( + "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04,b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elsh(wpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4)))#qs64ccxw", + "Invalid descriptor: slip77() must have exactly one argument", + ), + ( + "ct(slip77,elsh(wpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4)))#8p3zmumf", + "Invalid descriptor: slip77() must have exactly one argument", + ), + ( + "ct(elsh(wpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4)))#u9cwz9f3", + "Invalid descriptor: CT descriptor had 1 arguments rather than 2", + ), + ( + "ct(02dce16018bbbb8e36de7b394df5b5166e9adb7498be7d881a85a09aeecf76b623,02dce16018bbbb8e36de7b394df5b5166e9adb7498be7d881a85a09aeecf76b623,elwpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#cnsp2qsc", + "Invalid descriptor: CT descriptor had 3 arguments rather than 2", + ), + ( + "ct(pk(02dce16018bbbb8e36de7b394df5b5166e9adb7498be7d881a85a09aeecf76b623),elwpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#nvax6rau", + "unexpected «pk»", + ), + ]; + + /* + for (n, bad_str) in bad_strs.iter().enumerate() { + println!("* Invalid Descriptor {}", n + 1); + println!("** {}", bad_str.0); + println!("** Reason:"); + } + */ + + for bad_str in bad_strs { + let err = Descriptor::::from_str(bad_str.0).unwrap_err(); + assert_eq!(bad_str.1, err.to_string()); + } + } + + #[test] + fn view_descriptor() { + let secp = secp256k1_zkp::Secp256k1::new(); + + let view_key = DescriptorSecretKey::from_str( + "xprv9s21ZrQH143K28NgQ7bHCF61hy9VzwquBZvpzTwXLsbmQLRJ6iV9k2hUBRt5qzmBaSpeMj5LdcsHaXJvM7iFEivPryRcL8irN7Na9p65UUb", + ).unwrap(); + let ct_key = view_key.to_public(&secp).unwrap().at_derivation_index(0).unwrap(); // FIXME figure out derivation + let spk_key = DefiniteDescriptorKey::from_str( + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + ) + .unwrap(); + + // View key, P2PKH + let test = ConfidentialTest { + key: Key::View(view_key.clone()), + descriptor: crate::Descriptor::new_wpkh(spk_key.clone()).unwrap(), + descriptor_str: format!("ct({},elwpkh({}))#j95xktq7", view_key, spk_key), + conf_addr: "el1qq2r0pdvcknjpwev96qu9975alzqs78cvsut5ju82t7tv8d645dgmwknpl78t02k2xqgdh9ltmfmpy9ssk7qfvq78z9wukacu0", + unconf_addr: "ert1qtfsllr4h4t9rqyxmjl4a5asjzcgt0qyk32h3ur", + }; + test.check(&secp); + + // View key converted to Bare (note that addresses are the same) + let test = ConfidentialTest { + key: Key::Bare(ct_key.clone()), + descriptor: crate::Descriptor::new_wpkh(spk_key.clone()).unwrap(), + descriptor_str: format!("ct({},elwpkh({}))#elmfpmp9", ct_key, spk_key), + conf_addr: "el1qq2r0pdvcknjpwev96qu9975alzqs78cvsut5ju82t7tv8d645dgmwknpl78t02k2xqgdh9ltmfmpy9ssk7qfvq78z9wukacu0", + unconf_addr: "ert1qtfsllr4h4t9rqyxmjl4a5asjzcgt0qyk32h3ur", + }; + test.check(&secp); + } +} diff --git a/src/confidential/slip77.rs b/src/confidential/slip77.rs new file mode 100644 index 00000000..55aa28f1 --- /dev/null +++ b/src/confidential/slip77.rs @@ -0,0 +1,164 @@ +// Miniscript +// Written in 2022 by +// Andrew Poelstra +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to +// the public domain worldwide. This software is distributed without +// any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication +// along with this software. +// If not, see . +// + +//! SLIP77 +//! +//! Implementation of the SLIP77 protocol, documented at +//! https://github.com/satoshilabs/slips/blob/master/slip-0077.md +//! + +use std::{borrow, fmt}; + +use elements::hashes::{hex, sha256, sha512, Hash, HashEngine, Hmac, HmacEngine}; +use elements::secp256k1_zkp; + +/// A master blinding key, used for SLIP77-derived confidential addresses +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MasterBlindingKey([u8; 32]); + +impl fmt::Display for MasterBlindingKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + elements::hex::format_hex(&self.0, f) + } +} + +impl From<[u8; 32]> for MasterBlindingKey { + fn from(x: [u8; 32]) -> Self { + MasterBlindingKey(x) + } +} + +impl borrow::Borrow<[u8]> for MasterBlindingKey { + fn borrow(&self) -> &[u8] { + &self.0 + } +} + +impl MasterBlindingKey { + /// Compute a master blinding key from a seed + /// + /// The recommended in (SLIP-39) source of this seed is to obtain the + /// 64-byte seed from a BIP39 derivation. + pub fn from_seed(seed: &[u8]) -> Self { + const DOMAIN: &[u8] = b"Symmetric key seed"; + let mut root_eng = HmacEngine::::new(DOMAIN); + root_eng.input(seed); + let root = Hmac::from_engine(root_eng); + + const LABEL: &[u8] = b"SLIP-0077"; + let mut node_eng = HmacEngine::::new(&root[0..32]); + node_eng.input(&[0]); + node_eng.input(LABEL); + let node = Hmac::from_engine(node_eng); + + let mut ret = [0; 32]; + ret.copy_from_slice(&node[32..64]); + MasterBlindingKey(ret) + } + + /// Accessor for the underlying bytes + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Derives a blinding private key from a given script pubkey + pub fn blinding_private_key(&self, spk: &elements::Script) -> secp256k1_zkp::SecretKey { + let mut eng = HmacEngine::::new(&self.0); + eng.input(spk.as_bytes()); + // lol why is this conversion so hard + secp256k1_zkp::SecretKey::from_slice(&Hmac::from_engine(eng).to_byte_array()).unwrap() + } + + /// Derives a public private key from a given script pubkey + pub fn blinding_key( + &self, + secp: &secp256k1_zkp::Secp256k1, + spk: &elements::Script, + ) -> secp256k1_zkp::PublicKey { + let sk = self.blinding_private_key(spk); + secp256k1_zkp::PublicKey::from_secret_key(secp, &sk) + } +} + +impl hex::FromHex for MasterBlindingKey { + fn from_byte_iter(iter: I) -> Result + where + I: Iterator> + ExactSizeIterator + DoubleEndedIterator, + { + Ok(MasterBlindingKey(<[u8; 32]>::from_byte_iter(iter)?)) + } +} + +impl std::str::FromStr for MasterBlindingKey { + type Err = hex::Error; + fn from_str(s: &str) -> Result { + hex::FromHex::from_hex(s) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use elements::hashes::hex::FromHex; + + use super::*; + + fn unhex(s: &str) -> Vec { + elements::hex::FromHex::from_hex(s).unwrap() + } + + #[test] + fn mbk_from_seed() { + // taken from libwally src/test/test_confidential_addr.py + let mbk = MasterBlindingKey::from_seed(&unhex("c76c4ac4f4e4a00d6b274d5c39c700bb4a7ddc04fbc6f78e85ca75007b5b495f74a9043eeb77bdd53aa6fc3a0e31462270316fa04b8c19114c8798706cd02ac8")); + assert_eq!( + mbk.as_bytes(), + &unhex("6c2de18eabeff3f7822bc724ad482bef0557f3e1c1e1c75b7a393a5ced4de616")[..] + ); + + let secp = secp256k1_zkp::Secp256k1::new(); + let spk = elements::Script::from_str("76a914a579388225827d9f2fe9014add644487808c695d88ac") + .unwrap(); + let mut addr = elements::Address::from_str("2dpWh6jbhAowNsQ5agtFzi7j6nKscj6UnEr").unwrap(); + addr.blinding_pubkey = Some(mbk.blinding_key(&secp, &spk)); + assert_eq!( + addr.to_string(), + "CTEkf75DFff5ReB7juTg2oehrj41aMj21kvvJaQdWsEAQohz1EDhu7Ayh6goxpz3GZRVKidTtaXaXYEJ" + ); + } + + #[test] + fn local_test_elements_22_0() { + // Local test on elements 22.0 + let mbk = MasterBlindingKey::from_hex( + "64269a8de756da06ebe35d26dccb4dd46bddcf858b54eeaae315490cfe6cacc0", + ) + .unwrap(); + + let addr = elements::Address::from_str( + "el1qqg2pz79c0reryhr6hzxrzueju9m2asllwydrhexs6vj854cvwlen4tryh4thsdt2a26rte3fe87rf3my9t90wt78pcqrxv733", + ) + .unwrap(); + + let derived_blinding_key = mbk.blinding_private_key(&addr.script_pubkey()); + assert_eq!( + derived_blinding_key, + secp256k1_zkp::SecretKey::from_slice(&unhex( + "791a1081ae2ad98a5ad603737c648247f19d3c26e2beb54617638172edb230e7" + )) + .unwrap() + ); + } +} diff --git a/src/descriptor/bare.rs b/src/descriptor/bare.rs index 1885657b..96f84322 100644 --- a/src/descriptor/bare.rs +++ b/src/descriptor/bare.rs @@ -12,9 +12,8 @@ use core::fmt; use elements::{self, script, secp256k1_zkp, Script}; -use super::checksum::verify_checksum; use super::ELMTS_STR; -use crate::descriptor::checksum; +use crate::descriptor::checksum::{self, verify_checksum}; use crate::expression::{self, FromTree}; use crate::miniscript::context::ScriptContext; use crate::policy::{semantic, Liftable}; diff --git a/src/descriptor/blinded.rs b/src/descriptor/blinded.rs deleted file mode 100644 index 944b0a8e..00000000 --- a/src/descriptor/blinded.rs +++ /dev/null @@ -1,216 +0,0 @@ -// Miniscript -// Written in 2020 by rust-miniscript developers -// -// To the extent possible under law, the author(s) have dedicated all -// copyright and related and neighboring rights to this software to -// the public domain worldwide. This software is distributed without -// any warranty. -// -// You should have received a copy of the CC0 Public Domain Dedication -// along with this software. -// If not, see . -// - -//! # Bare Output Descriptors -//! -//! Implementation of Bare Descriptors (i.e descriptors that are) -//! wrapped inside wsh, or sh fragments. -//! Also includes pk, and pkh descriptors -//! - -use std::fmt; - -use elements::{self, Script}; - -use super::checksum::verify_checksum; -use super::{Descriptor, TranslatePk}; -use crate::descriptor::checksum; -use crate::expression::{self, FromTree}; -use crate::extensions::{CovExtArgs, CovenantExt}; -use crate::policy::{semantic, Liftable}; -use crate::{Error, MiniscriptKey, Satisfier, ToPublicKey, Translator}; - -/// Create a Bare Descriptor. That is descriptor that is -/// not wrapped in sh or wsh. This covers the Pk descriptor -#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)] -pub struct Blinded { - /// The blinding key - blinder: Pk, - /// underlying descriptor - /// Must be unblinded as blinding is only - /// permitted at the root level. - /// - /// TODO: Add blinding support to descriptor extensions - desc: Descriptor>, -} - -impl Blinded { - /// Create a new blinded descriptor from a descriptor and blinder - pub fn new(blinder: Pk, desc: Descriptor>) -> Self { - Self { blinder, desc } - } - - /// get the blinder - pub fn blinder(&self) -> &Pk { - &self.blinder - } - - /// get the unblinded descriptor - pub fn as_unblinded(&self) -> &Descriptor> { - &self.desc - } - - /// get the unblinded descriptor - pub fn into_unblinded(self) -> Descriptor> { - self.desc - } -} - -impl fmt::Debug for Blinded { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "blinded({:?},{:?})", self.blinder, self.desc) - } -} - -impl fmt::Display for Blinded { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // strip thec checksum from display - use fmt::Write; - let mut wrapped_f = checksum::Formatter::new(f); - write!(wrapped_f, "blinded({},{:#})", self.blinder, self.desc)?; - wrapped_f.write_checksum_if_not_alt() - } -} - -impl Liftable for Blinded { - fn lift(&self) -> Result, Error> { - self.desc.lift() - } -} - -impl_from_tree!( - Blinded, - fn from_tree(top: &expression::Tree<'_>) -> Result { - if top.name == "blinded" && top.args.len() == 2 { - let blinder = expression::terminal(&top.args[0], |pk| Pk::from_str(pk))?; - let desc = Descriptor::>::from_tree(&top.args[1])?; - if top.args[1].name == "blinded" { - return Err(Error::BadDescriptor( - "Blinding only permitted at root level".to_string(), - )); - } - Ok(Blinded { blinder, desc }) - } else { - Err(Error::Unexpected(format!( - "{}({} args) while parsing sh descriptor", - top.name, - top.args.len(), - ))) - } - } -); - -impl_from_str!( - Blinded, - type Err = Error;, - fn from_str(s: &str) -> Result { - let desc_str = verify_checksum(s)?; - let top = expression::Tree::from_str(desc_str)?; - Self::from_tree(&top) - } -); - -impl Blinded { - /// Sanity checks for the underlying descriptor. - pub fn sanity_check(&self) -> Result<(), Error> { - self.desc.sanity_check()?; - Ok(()) - } - - /// Obtains the blinded address for this descriptor. - pub fn address( - &self, - params: &'static elements::AddressParams, - ) -> Result - where - Pk: ToPublicKey, - { - self.desc - .blinded_address(self.blinder.to_public_key().inner, params) - } - - /// Obtains the script pubkey for this descriptor. - pub fn script_pubkey(&self) -> Script - where - Pk: ToPublicKey, - { - self.desc.script_pubkey() - } - - /// Computes the scriptSig that will be in place for an unsigned input - /// spending an output with this descriptor. - pub fn unsigned_script_sig(&self) -> Script - where - Pk: ToPublicKey, - { - self.desc.unsigned_script_sig() - } - - /// Computes the the underlying script before any hashing is done. - pub fn explicit_script(&self) -> Result - where - Pk: ToPublicKey, - { - self.desc.explicit_script() - } - - /// Returns satisfying non-malleable witness and scriptSig to spend an - /// output controlled by the given descriptor if it possible to - /// construct one using the satisfier S. - pub fn get_satisfaction(&self, satisfier: S) -> Result<(Vec>, Script), Error> - where - Pk: ToPublicKey, - S: Satisfier, - { - self.desc.get_satisfaction(satisfier) - } - - /// Computes an upper bound on the weight of a satisfying witness to the - /// transaction. - pub fn max_satisfaction_weight(&self) -> Result { - self.desc.max_weight_to_satisfy() - } - - /// Computes the `scriptCode` of a transaction output. - pub fn script_code(&self) -> Result - where - Pk: ToPublicKey, - { - self.desc.script_code() - } - - /// Returns a possilbly mallable satisfying non-malleable witness and scriptSig to spend an - /// output controlled by the given descriptor if it possible to - /// construct one using the satisfier S. - pub fn get_satisfaction_mall(&self, satisfier: S) -> Result<(Vec>, Script), Error> - where - Pk: ToPublicKey, - S: Satisfier, - { - self.desc.get_satisfaction_mall(satisfier) - } -} - -impl TranslatePk for Blinded

{ - type Output = Blinded; - - fn translate_pk(&self, t: &mut T) -> Result - where - T: Translator, - { - Ok(Blinded::new( - t.pk(&self.blinder)?, - self.desc.translate_pk(t)?, - )) - } -} diff --git a/src/descriptor/checksum.rs b/src/descriptor/checksum.rs index 1b02e97f..0cef0390 100644 --- a/src/descriptor/checksum.rs +++ b/src/descriptor/checksum.rs @@ -50,7 +50,7 @@ pub fn desc_checksum(desc: &str) -> Result { /// descriptor types. Checks and verifies the checksum /// if it is present and returns the descriptor string /// without the checksum -pub(super) fn verify_checksum(s: &str) -> Result<&str, Error> { +pub(crate) fn verify_checksum(s: &str) -> Result<&str, Error> { for ch in s.as_bytes() { if *ch < 20 || *ch > 127 { return Err(Error::Unprintable(*ch)); diff --git a/src/descriptor/csfs_cov/cov.rs b/src/descriptor/csfs_cov/cov.rs index 2feb4906..c2274301 100644 --- a/src/descriptor/csfs_cov/cov.rs +++ b/src/descriptor/csfs_cov/cov.rs @@ -47,9 +47,9 @@ use elements::encode::{serialize, Encodable}; use elements::hashes::{sha256d, Hash}; use elements::{self, script, secp256k1_zkp, Script}; -use super::super::checksum::{desc_checksum, verify_checksum}; use super::super::ELMTS_STR; use super::{CovError, CovOperations}; +use crate::descriptor::checksum::{self, verify_checksum}; use crate::expression::{self, FromTree}; use crate::extensions::ParseableExt; use crate::miniscript::lex::{lex, Token as Tk, TokenIter}; @@ -322,9 +322,10 @@ where Ext: Extension, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let desc = format!("{}covwsh({},{})", ELMTS_STR, self.pk, self.ms); - let checksum = desc_checksum(&desc).map_err(|_| fmt::Error)?; - write!(f, "{}#{}", &desc, &checksum) + use fmt::Write; + let mut wrapped_f = checksum::Formatter::new(f); + write!(wrapped_f, "{}covwsh({},{})", ELMTS_STR, self.pk, self.ms)?; + wrapped_f.write_checksum_if_not_alt() } } diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 2c14fcfb..d3a4328f 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -35,7 +35,6 @@ use crate::{ }; mod bare; -mod blinded; mod csfs_cov; mod segwitv0; mod sh; @@ -44,7 +43,6 @@ mod tr; // Descriptor Exports pub use self::bare::{Bare, Pkh}; -pub use self::blinded::Blinded; pub use self::segwitv0::{Wpkh, Wsh, WshInner}; pub use self::sh::{Sh, ShInner}; pub use self::sortedmulti::SortedMultiVec; diff --git a/src/expression.rs b/src/expression.rs index 7b2c2f92..da8c1f18 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -52,7 +52,7 @@ fn next_expr(sl: &str, delim: char) -> Found { // We keep count of lparan whenever we are inside a key context // We exit the context whenever we find the corresponding ')' // in which we entered the context. This allows to special case - // parse the '(' ')' inside key expressions.(slip77 and musig). + // parse the '(' ')' inside key expressions.(key or musig(keys)). let mut key_ctx = false; let mut key_lparan_count = 0; let mut found = Found::Nothing; @@ -63,7 +63,7 @@ fn next_expr(sl: &str, delim: char) -> Found { // already inside a key context if key_ctx { key_lparan_count += 1; - } else if &sl[..n] == "slip77" || &sl[..n] == "musig" { + } else if &sl[..n] == "musig" { key_lparan_count = 1; key_ctx = true; } else { diff --git a/src/lib.rs b/src/lib.rs index eed53e9f..333c578b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,7 @@ mod pub_macros; pub use pub_macros::*; +pub mod confidential; pub mod descriptor; pub mod expression; pub mod extensions; @@ -145,6 +146,7 @@ use elements::hashes::sha256; use elements::secp256k1_zkp::Secp256k1; use elements::{locktime, opcodes, script, secp256k1_zkp}; +pub use crate::confidential::Descriptor as ConfidentialDescriptor; pub use crate::descriptor::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey}; pub use crate::extensions::{CovenantExt, Extension, NoExt, TxEnv}; pub use crate::interpreter::Interpreter; diff --git a/src/miniscript/mod.rs b/src/miniscript/mod.rs index 213bb40b..cf367e49 100644 --- a/src/miniscript/mod.rs +++ b/src/miniscript/mod.rs @@ -717,7 +717,6 @@ mod tests { #[test] fn recursive_key_parsing() { type MsStr = Miniscript; - assert!(MsStr::from_str("pk(slip77(k))").is_ok()); assert!(MsStr::from_str("pk(musig(a))").is_ok()); assert!(MsStr::from_str("pk(musig(a,b))").is_ok()); assert!(MsStr::from_str("pk(musig(a,musig(b,c,d)))").is_ok());