Skip to content

Commit 7a32381

Browse files
committed
Add ELIP-151 module
Implementation for deterministic descriptor blinding keys.
1 parent af410f6 commit 7a32381

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

src/confidential/elip151.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// Miniscript
2+
// Written in 2023 by Leonardo Comandini
3+
//
4+
// To the extent possible under law, the author(s) have dedicated all
5+
// copyright and related and neighboring rights to this software to
6+
// the public domain worldwide. This software is distributed without
7+
// any warranty.
8+
//
9+
// You should have received a copy of the CC0 Public Domain Dedication
10+
// along with this software.
11+
// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
12+
//
13+
14+
//! ELIP151
15+
//!
16+
//! Implementation of the ELIP151 protocol, documented at
17+
//! https://github.com/ElementsProject/ELIPs/blob/main/elip-0151.md
18+
//!
19+
20+
use bitcoin::hashes::{sha256t_hash_newtype, Hash};
21+
use bitcoin::secp256k1;
22+
use bitcoin::Network;
23+
use elements::encode::Encodable;
24+
use elements::opcodes;
25+
use elements::script::Builder;
26+
27+
use crate::confidential::{Descriptor as ConfidentialDescriptor, Key};
28+
use crate::descriptor::{DescriptorSecretKey, SinglePriv};
29+
use crate::extensions::{Extension, ParseableExt};
30+
use crate::{Descriptor as OrdinaryDescriptor, DescriptorPublicKey, Error};
31+
32+
/// The SHA-256 initial midstate value for the [`ElipXXXXHash`].
33+
const MIDSTATE_ELIP151: [u8; 32] = [
34+
0x49, 0x81, 0x61, 0xd8, 0x52, 0x45, 0xf7, 0xaa, 0xd8, 0x24, 0x27, 0xb5, 0x64, 0x69, 0xe7, 0xd6,
35+
0x98, 0x17, 0xeb, 0x0f, 0x27, 0x14, 0x6f, 0x4e, 0x7b, 0x95, 0xb3, 0x6e, 0x46, 0xc1, 0xb5, 0x61,
36+
];
37+
38+
sha256t_hash_newtype!(
39+
Elip151Hash,
40+
Elip151Tag,
41+
MIDSTATE_ELIP151,
42+
64,
43+
doc = "ELIP-151 Deterministic descriptor blinding keys",
44+
forward
45+
);
46+
47+
impl Key {
48+
pub fn from_elip151<T: Extension + ParseableExt>(
49+
descriptor: &OrdinaryDescriptor<DescriptorPublicKey, T>,
50+
network: Network,
51+
) -> Result<Self, Error> {
52+
if !descriptor.has_wildcard() {
53+
return Err(Error::Unexpected(
54+
"Descriptors without wildcards are not supported in elip151".into(),
55+
));
56+
}
57+
58+
// Handle multi-path
59+
let script_pubkeys: Vec<_> = descriptor
60+
.clone()
61+
.into_single_descriptors()
62+
.expect("valid descriptor")
63+
.iter()
64+
.map(|descriptor| {
65+
// Remove wildcards
66+
descriptor
67+
.at_derivation_index((1 << 31) - 1)
68+
.expect("index not hardened, not multi-path")
69+
.script_pubkey()
70+
})
71+
.collect();
72+
73+
let mut eng = Elip151Hash::engine();
74+
for script_pubkey in script_pubkeys {
75+
Builder::new()
76+
.push_opcode(opcodes::all::OP_INVALIDOPCODE)
77+
.into_script()
78+
.consensus_encode(&mut eng)
79+
.expect("engines don't error");
80+
script_pubkey
81+
.consensus_encode(&mut eng)
82+
.expect("engines don't error");
83+
}
84+
let hash_bytes = Elip151Hash::from_engine(eng).to_byte_array();
85+
86+
// This computes mod n
87+
let scalar = secp256k1::scalar::Scalar::from_be_bytes(hash_bytes).expect("bytes from hash");
88+
let secret_key =
89+
secp256k1::SecretKey::from_slice(&scalar.to_be_bytes()).expect("bytes from scalar");
90+
91+
Ok(Key::View(DescriptorSecretKey::Single(SinglePriv {
92+
origin: None,
93+
key: bitcoin::key::PrivateKey::new(secret_key, network),
94+
})))
95+
}
96+
}
97+
98+
impl<T: Extension + ParseableExt> ConfidentialDescriptor<DescriptorPublicKey, T> {
99+
pub fn with_elip151_descriptor_blinding_key(
100+
descriptor: OrdinaryDescriptor<DescriptorPublicKey, T>,
101+
network: Network,
102+
) -> Result<Self, Error> {
103+
Ok(ConfidentialDescriptor {
104+
key: Key::from_elip151(&descriptor, network)?,
105+
descriptor,
106+
})
107+
}
108+
}
109+
110+
#[cfg(test)]
111+
mod test {
112+
use super::*;
113+
use crate::descriptor::checksum::desc_checksum;
114+
use bitcoin::hashes::{sha256, HashEngine};
115+
use std::str::FromStr;
116+
117+
#[test]
118+
fn tagged_hash_elip151() {
119+
// Check that cached midstate is computed correctly, code from rust-bitcoin
120+
let mut engine = sha256::Hash::engine();
121+
let tag_hash = sha256::Hash::hash(b"Deterministic-View-Key/1.0");
122+
engine.input(&tag_hash[..]);
123+
engine.input(&tag_hash[..]);
124+
assert_eq!(MIDSTATE_ELIP151, engine.midstate().to_byte_array());
125+
}
126+
127+
fn add_checksum(desc: &str) -> String {
128+
if desc.find('#').is_some() {
129+
desc.into()
130+
} else {
131+
format!("{}#{}", desc, desc_checksum(desc).unwrap())
132+
}
133+
}
134+
135+
fn confidential_descriptor(
136+
desc: &str,
137+
network: Network,
138+
) -> Result<ConfidentialDescriptor<DescriptorPublicKey>, Error> {
139+
let desc = add_checksum(desc);
140+
let desc = OrdinaryDescriptor::<DescriptorPublicKey>::from_str(&desc).unwrap();
141+
ConfidentialDescriptor::with_elip151_descriptor_blinding_key(desc, network)
142+
}
143+
144+
fn _first_address(desc: &ConfidentialDescriptor<DescriptorPublicKey>) -> String {
145+
let single_desc = if desc.descriptor.is_multipath() {
146+
let descriptor = desc
147+
.descriptor
148+
.clone()
149+
.into_single_descriptors()
150+
.unwrap()
151+
.first()
152+
.unwrap()
153+
.clone();
154+
ConfidentialDescriptor {
155+
key: desc.key.clone(),
156+
descriptor,
157+
}
158+
} else {
159+
desc.clone()
160+
};
161+
let definite_desc = single_desc.at_derivation_index(0).unwrap();
162+
let secp = elements::secp256k1_zkp::Secp256k1::new();
163+
let params = &elements::AddressParams::ELEMENTS;
164+
definite_desc.address(&secp, params).unwrap().to_string()
165+
}
166+
167+
#[test]
168+
fn test_vectors_elip151() {
169+
let network = Network::Bitcoin;
170+
let xpub = "xpub661MyMwAqRbcFkPHucMnrGNzDwb6teAX1RbKQmqtEF8kK3Z7LZ59qafCjB9eCRLiTVG3uxBxgKvRgbubRhqSKXnGGb1aoaqLrpMBDrVxga8";
171+
let pubkey = "03d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee0494";
172+
173+
let mut _i = 0;
174+
for desc in [
175+
&format!("elwpkh({xpub}/<0;1>/*)"),
176+
&format!("elwpkh({xpub}/0/*)"),
177+
] {
178+
let _conf_desc = confidential_descriptor(desc, network).unwrap();
179+
180+
// Uncomment this and below to regenerate test vectors; to see the output, run
181+
// cargo test test_vectors_elipxxxx -- --nocapture
182+
/*
183+
_i = _i + 1;
184+
println!("* Test vector {}", _i);
185+
println!("** Ordinary descriptor: <code>{}</code>", add_checksum(desc));
186+
println!("** Derived descriptor blinding key: <code>{}</code>", _conf_desc.key);
187+
println!("** Derived confidential descriptor: <code>{}</code>", _conf_desc);
188+
println!("** Derived confidential descriptor (equivalent version): <code>{}</code>", add_checksum(&format!("ct(elip151,{})", desc)));
189+
println!("** First address: <code>{}</code>", _first_address(&_conf_desc))
190+
*/
191+
}
192+
193+
_i = 0;
194+
for invalid_desc in [&format!("elwpkh({xpub})"), &format!("elwpkh({pubkey})")] {
195+
let err = confidential_descriptor(invalid_desc, network).unwrap_err();
196+
let text = "Descriptors without wildcards are not supported in elip151".to_string();
197+
assert_eq!(err, Error::Unexpected(text));
198+
/*
199+
_i = _i + 1;
200+
println!("* Invalid Test vector {}", _i);
201+
println!("** Ordinary descriptor: <code>{}</code>", add_checksum(invalid_desc));
202+
println!("** Invalid confidential descriptor: <code>{}</code>", add_checksum(&format!("ct(elip151,{})", invalid_desc)));
203+
*/
204+
}
205+
}
206+
}

src/confidential/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
//!
1919
2020
pub mod bare;
21+
pub mod elip151;
2122
pub mod slip77;
2223

2324
use std::fmt;

0 commit comments

Comments
 (0)