Skip to content

Commit 0b4d3b3

Browse files
committed
Add example for spending via internal key for taproot
1 parent 297c954 commit 0b4d3b3

File tree

3 files changed

+188
-12
lines changed

3 files changed

+188
-12
lines changed

examples/keyexpr.rs

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use core::str::FromStr;
2+
use std::collections::HashMap;
3+
4+
use actual_rand;
5+
use actual_rand::RngCore;
6+
use bitcoin::hashes::{hash160, ripemd160, sha256};
7+
use bitcoin::secp256k1::XOnlyPublicKey;
8+
use bitcoin::{self, secp256k1, Network};
9+
use miniscript::{hash256, Descriptor, TranslatePk, Translator};
10+
use secp256k1::{KeyPair, Secp256k1, SecretKey};
11+
#[cfg(feature = "std")]
12+
use secp256k1_zkp::{Message, MusigAggNonce, MusigKeyAggCache, MusigSession};
13+
14+
// xonly_keys generates a pair of vector containing public keys and secret keys
15+
fn xonly_keys(n: usize) -> (Vec<bitcoin::XOnlyPublicKey>, Vec<SecretKey>) {
16+
let mut pubkeys = Vec::with_capacity(n);
17+
let mut seckeys = Vec::with_capacity(n);
18+
let secp = secp256k1::Secp256k1::new();
19+
for _ in 0..n {
20+
let key_pair = KeyPair::new(&secp, &mut secp256k1::rand::thread_rng());
21+
let pk = XOnlyPublicKey::from_keypair(&key_pair);
22+
let sk = SecretKey::from_keypair(&key_pair);
23+
pubkeys.push(pk);
24+
seckeys.push(sk);
25+
}
26+
(pubkeys, seckeys)
27+
}
28+
29+
// StrPkTranslator helps replacing string with actual keys in descriptor/miniscript
30+
struct StrPkTranslator {
31+
pk_map: HashMap<String, bitcoin::XOnlyPublicKey>,
32+
}
33+
34+
impl Translator<String, bitcoin::XOnlyPublicKey, ()> for StrPkTranslator {
35+
fn pk(&mut self, pk: &String) -> Result<bitcoin::XOnlyPublicKey, ()> {
36+
self.pk_map.get(pk).copied().ok_or(())
37+
}
38+
39+
fn pkh(&mut self, _pkh: &String) -> Result<hash160::Hash, ()> {
40+
unreachable!("Policy doesn't contain any pkh fragment");
41+
}
42+
43+
fn sha256(&mut self, _sha256: &String) -> Result<sha256::Hash, ()> {
44+
unreachable!("Policy does not contain any sha256 fragment");
45+
}
46+
47+
fn hash256(&mut self, _sha256: &String) -> Result<hash256::Hash, ()> {
48+
unreachable!("Policy does not contain any hash256 fragment");
49+
}
50+
51+
fn ripemd160(&mut self, _ripemd160: &String) -> Result<ripemd160::Hash, ()> {
52+
unreachable!("Policy does not contain any ripemd160 fragment");
53+
}
54+
55+
fn hash160(&mut self, _hash160: &String) -> Result<hash160::Hash, ()> {
56+
unreachable!("Policy does not contain any hash160 fragment");
57+
}
58+
}
59+
60+
#[cfg(not(feature = "std"))]
61+
fn main() {}
62+
63+
#[cfg(feature = "std")]
64+
fn main() {
65+
let desc =
66+
Descriptor::<String>::from_str("tr(musig(E,F),{pk(A),multi_a(1,B,musig(C,D))})").unwrap();
67+
68+
// generate the public and secret keys
69+
let (pubkeys, seckeys) = xonly_keys(6);
70+
71+
// create the hashMap (from String to XonlyPublicKey)
72+
let mut pk_map = HashMap::new();
73+
pk_map.insert("A".to_string(), pubkeys[0]);
74+
pk_map.insert("B".to_string(), pubkeys[1]);
75+
pk_map.insert("C".to_string(), pubkeys[2]);
76+
pk_map.insert("D".to_string(), pubkeys[3]);
77+
pk_map.insert("E".to_string(), pubkeys[4]);
78+
pk_map.insert("F".to_string(), pubkeys[5]);
79+
80+
let mut t = StrPkTranslator { pk_map };
81+
// replace with actual keys
82+
let real_desc = desc.translate_pk(&mut t).unwrap();
83+
84+
// bitcoin script for the descriptor
85+
let script = real_desc.script_pubkey();
86+
println!("The script is {}", script);
87+
88+
// address for the descriptor (bc1...)
89+
let address = real_desc.address(Network::Bitcoin).unwrap();
90+
println!("The address is {}", address);
91+
92+
let secp = Secp256k1::new();
93+
// we are spending with the internal key (musig(E,F))
94+
let key_agg_cache = MusigKeyAggCache::new(&secp, &[pubkeys[4], pubkeys[5]]);
95+
// aggregated publickey
96+
let agg_pk = key_agg_cache.agg_pk();
97+
98+
let mut session_id = [0; 32];
99+
actual_rand::thread_rng().fill_bytes(&mut session_id);
100+
101+
// msg should actually be the hash of the transaction, but we use some random
102+
// 32 byte array.
103+
let msg = Message::from_slice(&[3; 32]).unwrap();
104+
let mut pub_nonces = Vec::with_capacity(2);
105+
let mut sec_nonces = Vec::with_capacity(2);
106+
match &real_desc {
107+
Descriptor::Tr(tr) => {
108+
let mut ind = 4;
109+
for _ in tr.internal_key().iter() {
110+
// generate public and secret nonces
111+
let (sec_nonce, pub_nonce) = key_agg_cache
112+
.nonce_gen(&secp, session_id, seckeys[ind], msg, None)
113+
.expect("Non zero session id");
114+
pub_nonces.push(pub_nonce);
115+
sec_nonces.push(sec_nonce);
116+
ind += 1;
117+
}
118+
}
119+
_ => (),
120+
}
121+
122+
// aggregate nonces
123+
let aggnonce = MusigAggNonce::new(&secp, pub_nonces.as_slice());
124+
let session = MusigSession::new(&secp, &key_agg_cache, aggnonce, msg, None);
125+
let mut partial_sigs = Vec::with_capacity(2);
126+
match &real_desc {
127+
Descriptor::Tr(tr) => {
128+
let mut ind = 0;
129+
for _ in tr.internal_key().iter() {
130+
// generate the partial signature for this key
131+
let partial_sig = session
132+
.partial_sign(
133+
&secp,
134+
&mut sec_nonces[ind],
135+
&KeyPair::from_secret_key(&secp, seckeys[4 + ind]),
136+
&key_agg_cache,
137+
)
138+
.unwrap();
139+
partial_sigs.push(partial_sig);
140+
ind += 1;
141+
}
142+
}
143+
_ => (),
144+
}
145+
146+
// aggregate the signature
147+
let signature = session.partial_sig_agg(partial_sigs.as_slice());
148+
// now verify the signature
149+
assert!(secp.verify_schnorr(&signature, &msg, &agg_pk).is_ok())
150+
}

src/descriptor/tr.rs

+23-12
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ fn parse_tr_tree(s: &str) -> Result<expression::Tree, Error> {
502502
});
503503
}
504504
// use str::split_once() method to refactor this when compiler version bumps up
505-
let (key, script) = split_once(rest, ',')
505+
let (key, script) = split_key_and_tree(rest)
506506
.ok_or_else(|| Error::BadDescriptor("invalid taproot descriptor".to_string()))?;
507507

508508
let internal_key = expression::Tree {
@@ -529,23 +529,34 @@ fn parse_tr_tree(s: &str) -> Result<expression::Tree, Error> {
529529
}
530530
}
531531

532-
fn split_once(inp: &str, delim: char) -> Option<(&str, &str)> {
532+
fn split_key_and_tree(inp: &str) -> Option<(&str, &str)> {
533533
if inp.is_empty() {
534534
None
535535
} else {
536-
let mut found = inp.len();
537-
for (idx, ch) in inp.chars().enumerate() {
538-
if ch == delim {
539-
found = idx;
540-
break;
536+
// hit the first comma when the open_bracket == 0, we can split at that point
537+
let mut open_brackets = 0;
538+
let mut ind = 0;
539+
for c in inp.chars() {
540+
if c == '(' {
541+
open_brackets += 1;
542+
} else if c == ')' {
543+
open_brackets -= 1;
544+
} else if c == ',' {
545+
if open_brackets == 0 {
546+
break;
547+
}
541548
}
549+
ind += 1;
542550
}
543-
// No comma or trailing comma found
544-
if found >= inp.len() - 1 {
545-
Some((inp, ""))
546-
} else {
547-
Some((&inp[..found], &inp[found + 1..]))
551+
if ind == 0 {
552+
return Some(("", &inp[1..]));
553+
}
554+
if inp.len() == ind {
555+
return Some((inp, ""));
548556
}
557+
let key = &inp[..ind];
558+
let script = &inp[ind + 1..];
559+
Some((key, script))
549560
}
550561
}
551562

src/miniscript/mod.rs

+15
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,21 @@ mod tests {
524524

525525
let desc = Descriptor::<String>::from_str("tr(musig(D),pk(musig(A,B,musig(C))))");
526526
assert_eq!(desc.is_ok(), true);
527+
528+
let desc = Descriptor::<String>::from_str("tr(musig(E,F),{pk(A),multi_a(1,B,musig(C,D))})");
529+
assert_eq!(desc.is_ok(), true);
530+
531+
let desc = Descriptor::<String>::from_str("tr(musig(D),pk(musig(A,B,musig(C))))");
532+
assert_eq!(desc.is_ok(), true);
533+
534+
let desc = Descriptor::<String>::from_str("tr(musig(A,B))");
535+
assert_eq!(desc.is_ok(), true);
536+
537+
let desc = Descriptor::<String>::from_str("tr(A)");
538+
assert_eq!(desc.is_ok(), true);
539+
540+
let desc = Descriptor::<String>::from_str("tr(SomeKey)");
541+
assert_eq!(desc.is_ok(), true);
527542
}
528543

529544
#[test]

0 commit comments

Comments
 (0)