diff --git a/src/confidential/elip151.rs b/src/confidential/elip151.rs new file mode 100644 index 00000000..281981f1 --- /dev/null +++ b/src/confidential/elip151.rs @@ -0,0 +1,207 @@ +// Miniscript +// Written in 2023 by Leonardo Comandini +// +// 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 . +// + +//! ELIP151 +//! +//! Implementation of the ELIP151 protocol, documented at +//! https://github.com/ElementsProject/ELIPs/blob/main/elip-0151.md +//! + +use bitcoin::hashes::{sha256t_hash_newtype, Hash}; +use bitcoin::secp256k1; +use bitcoin::Network; +use elements::encode::Encodable; +use elements::opcodes; +use elements::script::Builder; + +use crate::confidential::{Descriptor as ConfidentialDescriptor, Key}; +use crate::descriptor::{DescriptorSecretKey, SinglePriv}; +use crate::extensions::{Extension, ParseableExt}; +use crate::{Descriptor as OrdinaryDescriptor, DescriptorPublicKey, Error}; + +/// The SHA-256 initial midstate value for the [`Elip151Hash`]. +const MIDSTATE_ELIP151: [u8; 32] = [ + 0x49, 0x81, 0x61, 0xd8, 0x52, 0x45, 0xf7, 0xaa, 0xd8, 0x24, 0x27, 0xb5, 0x64, 0x69, 0xe7, 0xd6, + 0x98, 0x17, 0xeb, 0x0f, 0x27, 0x14, 0x6f, 0x4e, 0x7b, 0x95, 0xb3, 0x6e, 0x46, 0xc1, 0xb5, 0x61, +]; + +sha256t_hash_newtype!( + Elip151Hash, + Elip151Tag, + MIDSTATE_ELIP151, + 64, + doc = "ELIP-151 Deterministic descriptor blinding keys", + forward +); + +impl Key { + pub fn from_elip151( + descriptor: &OrdinaryDescriptor, + ) -> Result { + if !descriptor.has_wildcard() { + return Err(Error::Unexpected( + "Descriptors without wildcards are not supported in elip151".into(), + )); + } + + // Handle multi-path + let script_pubkeys: Vec<_> = descriptor + .clone() + .into_single_descriptors() + .expect("valid descriptor") + .iter() + .map(|descriptor| { + // Remove wildcards + descriptor + .at_derivation_index((1 << 31) - 1) + .expect("index not hardened, not multi-path") + .script_pubkey() + }) + .collect(); + + let mut eng = Elip151Hash::engine(); + for script_pubkey in script_pubkeys { + Builder::new() + .push_opcode(opcodes::all::OP_INVALIDOPCODE) + .into_script() + .consensus_encode(&mut eng) + .expect("engines don't error"); + script_pubkey + .consensus_encode(&mut eng) + .expect("engines don't error"); + } + let hash_bytes = Elip151Hash::from_engine(eng).to_byte_array(); + + // This computes mod n + let scalar = secp256k1::scalar::Scalar::from_be_bytes(hash_bytes).expect("bytes from hash"); + let secret_key = + secp256k1::SecretKey::from_slice(&scalar.to_be_bytes()).expect("bytes from scalar"); + + // Single view keys are displayed as hex (not WIF) so we can choose any netowrk here + let network = Network::Bitcoin; + Ok(Key::View(DescriptorSecretKey::Single(SinglePriv { + origin: None, + key: bitcoin::key::PrivateKey::new(secret_key, network), + }))) + } +} + +impl ConfidentialDescriptor { + pub fn with_elip151_descriptor_blinding_key( + descriptor: OrdinaryDescriptor, + ) -> Result { + Ok(ConfidentialDescriptor { + key: Key::from_elip151(&descriptor)?, + descriptor, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::descriptor::checksum::desc_checksum; + use bitcoin::hashes::{sha256, HashEngine}; + use std::str::FromStr; + + #[test] + fn tagged_hash_elip151() { + // Check that cached midstate is computed correctly, code from rust-bitcoin + let mut engine = sha256::Hash::engine(); + let tag_hash = sha256::Hash::hash(b"Deterministic-View-Key/1.0"); + engine.input(&tag_hash[..]); + engine.input(&tag_hash[..]); + assert_eq!(MIDSTATE_ELIP151, engine.midstate().to_byte_array()); + } + + fn add_checksum(desc: &str) -> String { + if desc.find('#').is_some() { + desc.into() + } else { + format!("{}#{}", desc, desc_checksum(desc).unwrap()) + } + } + + fn confidential_descriptor( + desc: &str, + ) -> Result, Error> { + let desc = add_checksum(desc); + let desc = OrdinaryDescriptor::::from_str(&desc).unwrap(); + ConfidentialDescriptor::with_elip151_descriptor_blinding_key(desc) + } + + fn _first_address(desc: &ConfidentialDescriptor) -> String { + let single_desc = if desc.descriptor.is_multipath() { + let descriptor = desc + .descriptor + .clone() + .into_single_descriptors() + .unwrap() + .first() + .unwrap() + .clone(); + ConfidentialDescriptor { + key: desc.key.clone(), + descriptor, + } + } else { + desc.clone() + }; + let definite_desc = single_desc.at_derivation_index(0).unwrap(); + let secp = elements::secp256k1_zkp::Secp256k1::new(); + let params = &elements::AddressParams::ELEMENTS; + definite_desc.address(&secp, params).unwrap().to_string() + } + + #[test] + fn test_vectors_elip151() { + let xpub = "xpub661MyMwAqRbcFkPHucMnrGNzDwb6teAX1RbKQmqtEF8kK3Z7LZ59qafCjB9eCRLiTVG3uxBxgKvRgbubRhqSKXnGGb1aoaqLrpMBDrVxga8"; + let pubkey = "03d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee0494"; + + let mut _i = 0; + for desc in [ + &format!("elwpkh({xpub}/<0;1>/*)"), + &format!("elwpkh({xpub}/0/*)"), + ] { + let conf_desc = confidential_descriptor(desc).unwrap(); + let elip151_desc = add_checksum(&format!("ct(elip151,{})", desc)); + let conf_desc_elip151 = ConfidentialDescriptor::::from_str(&elip151_desc).unwrap(); + assert_eq!(conf_desc, conf_desc_elip151); + + // Uncomment this and below to regenerate test vectors; to see the output, run + // cargo test test_vectors_elip151 -- --nocapture + /* + _i = _i + 1; + println!("* Test vector {}", _i); + println!("** Ordinary descriptor: {}", add_checksum(desc)); + println!("** Derived descriptor blinding key: {}", conf_desc.key); + println!("** Derived confidential descriptor: {}", conf_desc); + println!("** Derived confidential descriptor (equivalent version): {}", elip151_desc); + println!("** First address: {}", _first_address(&conf_desc)) + */ + } + + _i = 0; + for invalid_desc in [&format!("elwpkh({xpub})"), &format!("elwpkh({pubkey})")] { + let err = confidential_descriptor(invalid_desc).unwrap_err(); + let text = "Descriptors without wildcards are not supported in elip151".to_string(); + assert_eq!(err, Error::Unexpected(text)); + /* + _i = _i + 1; + println!("* Invalid Test vector {}", _i); + println!("** Ordinary descriptor: {}", add_checksum(invalid_desc)); + println!("** Invalid confidential descriptor: {}", add_checksum(&format!("ct(elip151,{})", invalid_desc))); + */ + } + } +} diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 083cddd5..7249cb06 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -18,6 +18,7 @@ //! pub mod bare; +pub mod elip151; pub mod slip77; use std::fmt; @@ -216,6 +217,10 @@ impl_from_str!( let keyexpr = &top.args[0]; Ok(Descriptor { key: match (keyexpr.name, keyexpr.args.len()) { + ("elip151", 0) => { + let d = crate::Descriptor::::from_tree(&top.args[1])?; + Key::from_elip151(&d)? + } ("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()