From b11c8182c5e7f0328cec162b32db06377f4fef0c Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 16:21:26 +0000 Subject: [PATCH 01/13] descriptor: fix Display impl for legacy CSFS covenants --- src/descriptor/bare.rs | 3 +-- src/descriptor/csfs_cov/cov.rs | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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/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() } } From 67e9422d633172db555d35e71f74df56639ff264 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 14:31:41 +0000 Subject: [PATCH 02/13] descriptor: expose checksum function to whole crate --- src/descriptor/checksum.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)); From 5d2755d96968491f31f8b47587e1e1209ada1026 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 29 Mar 2023 17:18:39 +0000 Subject: [PATCH 03/13] delete descriptor::Blinded --- src/descriptor/blinded.rs | 216 -------------------------------------- src/descriptor/mod.rs | 2 - 2 files changed, 218 deletions(-) delete mode 100644 src/descriptor/blinded.rs 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/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; From 450d4d1e3a6c04186317a7dcd097c289a0d8c7ec Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 18:31:06 +0000 Subject: [PATCH 04/13] expression: stop special-casing the text `slip77` In a previous incarnation of CT descriptors we considered "musig" and "slip77" expressions to constitute keys, allowing them to be parsed in contexts where normally only keys would be allowed (and _not_ expanding them as expression trees which would then need to be reconstructed). This is still plausibly the direction we want to go in for MuSig keys, but it never made a lot of sense for SLIP77, and indeed, we now want to go a different direction, where SLIP77 expressions are allowed only as the first argument of a CT descriptor. --- src/expression.rs | 4 ++-- src/miniscript/mod.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) 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/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()); From 277b1a5c476ea279da40021bfab32d9b5fd83453 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 14:26:17 +0000 Subject: [PATCH 05/13] CT descriptors: block in module --- src/confidential/mod.rs | 142 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 143 insertions(+) create mode 100644 src/confidential/mod.rs diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs new file mode 100644 index 00000000..2f089df1 --- /dev/null +++ b/src/confidential/mod.rs @@ -0,0 +1,142 @@ +// 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` +//! + +use std::fmt; + +use elements::secp256k1_zkp; + +use crate::descriptor::checksum::{desc_checksum, verify_checksum}; +use crate::expression::FromTree; +use crate::extensions::{CovExtArgs, CovenantExt, Extension, ParseableExt}; +use crate::{expression, Error, MiniscriptKey, ToPublicKey}; + +mod slip77 { + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] + pub enum MasterBlindingKey {} +} + +/// A description of a blinding key +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub enum Key { + /// Blinding key is computed using SLIP77 with the given master key + Slip77(slip77::MasterBlindingKey), + /// Blinding key is given directly + Bare(Pk), +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Key::Slip77(ref _data) => unimplemented!(), + Key::Bare(ref pk) => fmt::Display::fmt(pk, f), + } + } +} + +impl Key { + fn to_public_key( + &self, + _secp: &secp256k1_zkp::Secp256k1, + _spk: &elements::Script, + ) -> secp256k1_zkp::PublicKey { + match *self { + Key::Slip77(ref _mbk) => unimplemented!(), + Key::Bare(ref _pk) => unimplemented!(), + } + } +} + +/// A confidential descriptor +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, 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) => unimplemented!(), + ("slip77", _) => return Err(Error::BadDescriptor( + "slip77() must have exactly one argument".to_owned() + )), + _ => Key::Bare(expression::terminal(keyexpr, Pk::from_str)?), + }, + descriptor: crate::Descriptor::from_tree(&top.args[1])?, + }) + } +); diff --git a/src/lib.rs b/src/lib.rs index eed53e9f..7963d4a1 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; From f5b2fe572bfe73ed8ecf35406088ec5198d94aa2 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 19:16:23 +0000 Subject: [PATCH 06/13] CT descriptors: add `slip77` module --- src/confidential/mod.rs | 21 ++--- src/confidential/slip77.rs | 164 +++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 src/confidential/slip77.rs diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 2f089df1..deb611e9 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -17,6 +17,8 @@ //! Implements ELIP ????, described at `URL` //! +pub mod slip77; + use std::fmt; use elements::secp256k1_zkp; @@ -26,11 +28,6 @@ use crate::expression::FromTree; use crate::extensions::{CovExtArgs, CovenantExt, Extension, ParseableExt}; use crate::{expression, Error, MiniscriptKey, ToPublicKey}; -mod slip77 { - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] - pub enum MasterBlindingKey {} -} - /// A description of a blinding key #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] pub enum Key { @@ -42,9 +39,9 @@ pub enum Key { impl fmt::Display for Key { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Key::Slip77(ref _data) => unimplemented!(), - Key::Bare(ref pk) => fmt::Display::fmt(pk, f), + match self { + Key::Slip77(data) => write!(f, "slip77({})", data), + Key::Bare(pk) => fmt::Display::fmt(pk, f), } } } @@ -52,11 +49,11 @@ impl fmt::Display for Key { impl Key { fn to_public_key( &self, - _secp: &secp256k1_zkp::Secp256k1, - _spk: &elements::Script, + secp: &secp256k1_zkp::Secp256k1, + spk: &elements::Script, ) -> secp256k1_zkp::PublicKey { match *self { - Key::Slip77(ref _mbk) => unimplemented!(), + Key::Slip77(ref mbk) => mbk.blinding_key(secp, spk), Key::Bare(ref _pk) => unimplemented!(), } } @@ -130,7 +127,7 @@ impl_from_str!( let keyexpr = &top.args[0]; Ok(Descriptor { key: match (keyexpr.name, keyexpr.args.len()) { - ("slip77", 1) => unimplemented!(), + ("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() )), 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() + ); + } +} From e9ac0bc4caa2f80c5c375d3a4def2625efc6a941 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 14:54:55 +0000 Subject: [PATCH 07/13] CT descriptors: add `bare` module --- src/confidential/bare.rs | 99 ++++++++++++++++++++++++++++++++++++++++ src/confidential/mod.rs | 3 +- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/confidential/bare.rs diff --git a/src/confidential/bare.rs b/src/confidential/bare.rs new file mode 100644 index 00000000..ed35c127 --- /dev/null +++ b/src/confidential/bare.rs @@ -0,0 +1,99 @@ +// 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() +} + +#[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 index deb611e9..8cb32941 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -17,6 +17,7 @@ //! Implements ELIP ????, described at `URL` //! +pub mod bare; pub mod slip77; use std::fmt; @@ -54,7 +55,7 @@ impl Key { ) -> secp256k1_zkp::PublicKey { match *self { Key::Slip77(ref mbk) => mbk.blinding_key(secp, spk), - Key::Bare(ref _pk) => unimplemented!(), + Key::Bare(ref pk) => bare::tweak_key(secp, spk, pk), } } } From 604c7e3ed29bf911c60502e19e984be70459d6e3 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 14:40:02 +0000 Subject: [PATCH 08/13] CT descriptors: add test vectors --- src/confidential/mod.rs | 150 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 8cb32941..e62c090f 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -138,3 +138,153 @@ impl_from_str!( }) } ); + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use elements::Address; + + use super::*; + use crate::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, + 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(), + ); + } + } + + #[test] + fn confidential_descriptor() { + let secp = secp256k1_zkp::Secp256k1::new(); + + // CT key used for bare keys + let ct_key = secp256k1_zkp::PublicKey::from_str( + "02dce16018bbbb8e36de7b394df5b5166e9adb7498be7d881a85a09aeecf76b623", + ) + .unwrap(); + // Auxiliary key to create scriptpubkeys from + let spk_key = secp256k1_zkp::PublicKey::from_str( + "03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4", + ) + .unwrap(); + + let tests = vec![ + // Bare key, P2PKH + ConfidentialTest { + key: Key::Bare(ct_key), + descriptor: crate::Descriptor::new_pkh(spk_key), + descriptor_str: format!("ct({},elpkh({}))#y6sgetu5", ct_key, spk_key), + conf_addr: "CTEp9vcs3eU7zQoyrAqeu9LwdcB8QtC2igYSWt7dhaEQvDwVCceLHdLrREAeYrhC5Jz9Wedn3JyxMzpo", + unconf_addr: "2daq3zWYvigZd3i8VmBnFrZd4DPT9iV94EP", + }, + // Bare key, P2WPKH + ConfidentialTest { + key: Key::Bare(ct_key), + descriptor: crate::Descriptor::new_wpkh(spk_key).unwrap(), + descriptor_str: format!("ct({},elwpkh({}))#h5e0p6m9", ct_key, spk_key), + conf_addr: "el1qq0r6pegudzm0tzpszelc34qjln4fdxawgwmgnza63wwpzdy6jrm0grmqvvk2ce5ksnxcs9ecgtnryt7xg34060uctupg60d02", + unconf_addr: "ert1qpasxxt9vv6tgfnvgzuuy9e3j9lryg6ha53x9q0", + }, + // Bare key, P2SH-WPKH + ConfidentialTest { + key: Key::Bare(ct_key), + descriptor: crate::Descriptor::new_sh_wpkh(spk_key).unwrap(), + descriptor_str: format!("ct({},elsh(wpkh({})))#3kvhe0a8", ct_key, spk_key), + conf_addr: "AzpsK7uqP1KVEMfDQvBXYUkpHmFagD3W4vaLe1X7uy8MS6nj41kNYnaexuXgx14PcbNnYAqBdCSWcbga", + unconf_addr: "XQ7ffnJkhMwj1H8Ma6N1vcU9mqAa96wB9w", + }, + // Bare key, P2TR + ConfidentialTest { + key: Key::Bare(ct_key), + descriptor: crate::Descriptor::new_tr(spk_key, None).unwrap(), + descriptor_str: format!("ct({},eltr({}))#ytq9w7f3", ct_key, spk_key), + conf_addr: "el1pqw5c43qvxyvj52ua7crx7tv62zca5356rsx439dqkyyqyavpmq2hz6r5jd0rkpzukq6hd965kepcmwtxvg0fh4ak4f636gv25yky23ce6z5pdt3ksqn2", + unconf_addr: "ert1pdp6fxh3mq3wtqdtkja2tvsudh9nxy85m67m25agayx92ztz9guvs9wr5lg", + }, + // SLIP77, P2PKH + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_pkh(spk_key), + descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#8cdjnvav".into(), + conf_addr: "CTEkBfH2b6fyhfpn2iW1aoLFrC9DHooTsa7ouxXDuKVjLNF3GwJdgqQn63GriXDvTs7ntSU8NwXGrLKg", + unconf_addr: "2daq3zWYvigZd3i8VmBnFrZd4DPT9iV94EP", + }, + // SLIP77, P2WPKH + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_wpkh(spk_key).unwrap(), + descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elwpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#z5dnzfhk".into(), + conf_addr: "el1qqva2r6mg26rr86u9t3qz2amya9v3ckks9ztcxgur6y6pktfa26d2qrmqvvk2ce5ksnxcs9ecgtnryt7xg3406z5cvgcqsgt35", + unconf_addr: "ert1qpasxxt9vv6tgfnvgzuuy9e3j9lryg6ha53x9q0", + }, + // SLIP77, P2SH + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_sh_wpkh(spk_key).unwrap(), + descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elsh(wpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4)))#qgjmm4as".into(), + conf_addr: "AzprjJt3poXAJWLmanTHYB2zrkUMXiiVyCRhDS7VRRaXUsRqeCMi3vKe4YufizpYDyzvQFLsvjfMeLMD", + unconf_addr: "XQ7ffnJkhMwj1H8Ma6N1vcU9mqAa96wB9w", + }, + // SLIP77, P2TR + ConfidentialTest { + key: Key::Slip77(slip77::MasterBlindingKey::from_seed(b"abcd")), + descriptor: crate::Descriptor::new_tr(spk_key, None).unwrap(), + descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),eltr(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#65xky8c4".into(), + conf_addr: "el1pqgkj53t6cpqytk65s7dygws4a6hny3rrev7rw3r5gl7ymnjdqtt9k6r5jd0rkpzukq6hd965kepcmwtxvg0fh4ak4f636gv25yky23cehlfsszhd84mc", + unconf_addr: "ert1pdp6fxh3mq3wtqdtkja2tvsudh9nxy85m67m25agayx92ztz9guvs9wr5lg", + }, + ]; + + for test in tests { + test.check(&secp); + } + } +} From 2b5bd5535fef22e889a01edb494c7f329dafdde8 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 18:43:29 +0000 Subject: [PATCH 09/13] CT descriptors: generate ELIP test vectors --- src/confidential/mod.rs | 70 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index e62c090f..cb067c3d 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -199,6 +199,24 @@ mod tests { .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(pk) => println!("** Blinding key: {}", pk), + Key::Slip77(mbk) => println!("** SLIP77 master blinding key: {}", mbk), + } + println!("** Confidential address: {}", self.conf_addr); + println!( + "** Unconfidential address: {}", + self.unconf_addr + ); + println!(); + } } #[test] @@ -283,8 +301,58 @@ mod tests { }, ]; - for test in tests { + 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()); + } } } From 694628f3814c7f8f55b2b91d040b578496ac2a87 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 11 Jun 2023 15:29:38 +0000 Subject: [PATCH 10/13] CT descriptors: add fuzz test --- .github/workflows/fuzz.yml | 1 + fuzz/Cargo.toml | 4 +++ fuzz/fuzz_targets/roundtrip_confidential.rs | 38 +++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 fuzz/fuzz_targets/roundtrip_confidential.rs 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); + } +} From a332208314926b9cd019db619292c7bfcc3790ef Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 17 Oct 2022 23:35:27 +0000 Subject: [PATCH 11/13] re-export confidential::Descirptor as ConfidentialDescirptor --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 7963d4a1..333c578b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,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; From 6359f033827d5104b0ba66d73d3052ede83b2cf5 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 29 Jun 2023 21:16:15 +0000 Subject: [PATCH 12/13] confidential: switch test vectors to use xpubs rather than bare keys --- src/confidential/mod.rs | 94 ++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index cb067c3d..ae757df3 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -146,7 +146,7 @@ mod tests { use elements::Address; use super::*; - use crate::NoExt; + use crate::{DefiniteDescriptorKey, NoExt}; #[test] fn bare_addr_to_confidential() { @@ -168,8 +168,8 @@ mod tests { } struct ConfidentialTest { - key: Key, - descriptor: crate::Descriptor, + key: Key, + descriptor: crate::Descriptor, descriptor_str: String, conf_addr: &'static str, unconf_addr: &'static str, @@ -180,8 +180,8 @@ mod tests { &self, secp: &secp256k1_zkp::Secp256k1, ) { - let desc: Descriptor = Descriptor { - key: self.key, + let desc: Descriptor = Descriptor { + key: self.key.clone(), descriptor: self.descriptor.clone(), }; assert_eq!(self.descriptor_str, desc.to_string()); @@ -207,7 +207,7 @@ mod tests { index, self.descriptor_str ); match self.key { - Key::Bare(pk) => println!("** Blinding key: {}", pk), + Key::Bare(ref pk) => println!("** Blinding key: {}", pk), Key::Slip77(mbk) => println!("** SLIP77 master blinding key: {}", mbk), } println!("** Confidential address: {}", self.conf_addr); @@ -224,80 +224,80 @@ mod tests { let secp = secp256k1_zkp::Secp256k1::new(); // CT key used for bare keys - let ct_key = secp256k1_zkp::PublicKey::from_str( - "02dce16018bbbb8e36de7b394df5b5166e9adb7498be7d881a85a09aeecf76b623", + let ct_key = DefiniteDescriptorKey::from_str( + "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", ) .unwrap(); // Auxiliary key to create scriptpubkeys from - let spk_key = secp256k1_zkp::PublicKey::from_str( - "03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4", + let spk_key = DefiniteDescriptorKey::from_str( + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", ) .unwrap(); let tests = vec![ // Bare key, P2PKH ConfidentialTest { - key: Key::Bare(ct_key), - descriptor: crate::Descriptor::new_pkh(spk_key), - descriptor_str: format!("ct({},elpkh({}))#y6sgetu5", ct_key, spk_key), - conf_addr: "CTEp9vcs3eU7zQoyrAqeu9LwdcB8QtC2igYSWt7dhaEQvDwVCceLHdLrREAeYrhC5Jz9Wedn3JyxMzpo", - unconf_addr: "2daq3zWYvigZd3i8VmBnFrZd4DPT9iV94EP", + 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), - descriptor: crate::Descriptor::new_wpkh(spk_key).unwrap(), - descriptor_str: format!("ct({},elwpkh({}))#h5e0p6m9", ct_key, spk_key), - conf_addr: "el1qq0r6pegudzm0tzpszelc34qjln4fdxawgwmgnza63wwpzdy6jrm0grmqvvk2ce5ksnxcs9ecgtnryt7xg34060uctupg60d02", - unconf_addr: "ert1qpasxxt9vv6tgfnvgzuuy9e3j9lryg6ha53x9q0", + 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), - descriptor: crate::Descriptor::new_sh_wpkh(spk_key).unwrap(), - descriptor_str: format!("ct({},elsh(wpkh({})))#3kvhe0a8", ct_key, spk_key), - conf_addr: "AzpsK7uqP1KVEMfDQvBXYUkpHmFagD3W4vaLe1X7uy8MS6nj41kNYnaexuXgx14PcbNnYAqBdCSWcbga", - unconf_addr: "XQ7ffnJkhMwj1H8Ma6N1vcU9mqAa96wB9w", + 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), - descriptor: crate::Descriptor::new_tr(spk_key, None).unwrap(), - descriptor_str: format!("ct({},eltr({}))#ytq9w7f3", ct_key, spk_key), - conf_addr: "el1pqw5c43qvxyvj52ua7crx7tv62zca5356rsx439dqkyyqyavpmq2hz6r5jd0rkpzukq6hd965kepcmwtxvg0fh4ak4f636gv25yky23ce6z5pdt3ksqn2", - unconf_addr: "ert1pdp6fxh3mq3wtqdtkja2tvsudh9nxy85m67m25agayx92ztz9guvs9wr5lg", + 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), - descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#8cdjnvav".into(), - conf_addr: "CTEkBfH2b6fyhfpn2iW1aoLFrC9DHooTsa7ouxXDuKVjLNF3GwJdgqQn63GriXDvTs7ntSU8NwXGrLKg", - unconf_addr: "2daq3zWYvigZd3i8VmBnFrZd4DPT9iV94EP", + 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).unwrap(), - descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elwpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#z5dnzfhk".into(), - conf_addr: "el1qqva2r6mg26rr86u9t3qz2amya9v3ckks9ztcxgur6y6pktfa26d2qrmqvvk2ce5ksnxcs9ecgtnryt7xg3406z5cvgcqsgt35", - unconf_addr: "ert1qpasxxt9vv6tgfnvgzuuy9e3j9lryg6ha53x9q0", + 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).unwrap(), - descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),elsh(wpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4)))#qgjmm4as".into(), - conf_addr: "AzprjJt3poXAJWLmanTHYB2zrkUMXiiVyCRhDS7VRRaXUsRqeCMi3vKe4YufizpYDyzvQFLsvjfMeLMD", - unconf_addr: "XQ7ffnJkhMwj1H8Ma6N1vcU9mqAa96wB9w", + 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, None).unwrap(), - descriptor_str: "ct(slip77(b2396b3ee20509cdb64fe24180a14a72dbd671728eaa49bac69d2bdecb5f5a04),eltr(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#65xky8c4".into(), - conf_addr: "el1pqgkj53t6cpqytk65s7dygws4a6hny3rrev7rw3r5gl7ymnjdqtt9k6r5jd0rkpzukq6hd965kepcmwtxvg0fh4ak4f636gv25yky23cehlfsszhd84mc", - unconf_addr: "ert1pdp6fxh3mq3wtqdtkja2tvsudh9nxy85m67m25agayx92ztz9guvs9wr5lg", + 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", }, ]; @@ -351,7 +351,7 @@ mod tests { */ for bad_str in bad_strs { - let err = Descriptor::::from_str(bad_str.0).unwrap_err(); + let err = Descriptor::::from_str(bad_str.0).unwrap_err(); assert_eq!(bad_str.1, err.to_string()); } } From 725f4502c9e00a805f9a50cae58f64907e303530 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 29 Jun 2023 22:14:57 +0000 Subject: [PATCH 13/13] confidential: hack in view descriptor support Need to re-think the API for this; but sufficient to produce a test vector. --- src/confidential/bare.rs | 23 +++++++++++++++++++ src/confidential/mod.rs | 49 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/confidential/bare.rs b/src/confidential/bare.rs index ed35c127..d19edb71 100644 --- a/src/confidential/bare.rs +++ b/src/confidential/bare.rs @@ -58,6 +58,29 @@ where .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; diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index ae757df3..6f3e21d6 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -25,17 +25,20 @@ 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(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +#[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 { @@ -43,6 +46,7 @@ impl fmt::Display for Key { 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), } } } @@ -56,12 +60,13 @@ impl Key { 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, PartialOrd, Ord, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct Descriptor> { /// The blinding key pub key: Key, @@ -132,7 +137,8 @@ impl_from_str!( ("slip77", _) => return Err(Error::BadDescriptor( "slip77() must have exactly one argument".to_owned() )), - _ => Key::Bare(expression::terminal(keyexpr, Pk::from_str)?), + _ => 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])?, }) @@ -207,7 +213,8 @@ mod tests { index, self.descriptor_str ); match self.key { - Key::Bare(ref pk) => println!("** Blinding key: {}", pk), + 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); @@ -355,4 +362,38 @@ mod tests { 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); + } }