Skip to content

Commit df75157

Browse files
committed
Add integration tests for Introspection opcodes
1 parent 6bf9d2c commit df75157

File tree

2 files changed

+272
-2
lines changed

2 files changed

+272
-2
lines changed

tests/setup/test_util.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
//! The keys/hashes are automatically translated so that the tests knows how to satisfy things that don't end with !
1818
//!
1919
20+
use std::collections::HashMap;
2021
use std::str::FromStr;
2122

2223
use bitcoin::hashes::hex::ToHex;
2324
use bitcoin::hashes::{hash160, ripemd160, sha256, sha256d, Hash};
2425
use bitcoin::secp256k1;
25-
use elements::{AddressParams, BlockHash};
26+
use elements::hashes::hex::FromHex;
27+
use elements::{confidential, encode, AddressParams, BlockHash};
2628
use miniscript::descriptor::{SinglePub, SinglePubKey};
2729
use miniscript::extensions::{CovExtArgs, CsfsKey, CsfsMsg};
2830
use miniscript::{
@@ -44,6 +46,9 @@ pub struct PubData {
4446
pub hash160: hash160::Hash,
4547
pub genesis_hash: elements::BlockHash,
4648
pub msg: CsfsMsg,
49+
pub values: HashMap<String, confidential::Value>,
50+
pub assets: HashMap<String, confidential::Asset>,
51+
pub spks: HashMap<String, elements::Script>,
4752
}
4853

4954
#[derive(Debug, Clone)]
@@ -124,6 +129,9 @@ impl TestData {
124129
x_only_pks,
125130
genesis_hash,
126131
msg,
132+
values: HashMap::new(),
133+
assets: HashMap::new(),
134+
spks: HashMap::new(),
127135
};
128136
let secretdata = SecretData {
129137
sks,
@@ -182,8 +190,32 @@ impl<'a> ExtTranslator<String, CovExtArgs, ()> for StrExtTransalator<'a> {
182190
let csfs_pk = CovExtArgs::XOnlyKey(CsfsKey(self.1.x_only_pks[self.0]));
183191
self.0 = self.0 + 1;
184192
Ok(csfs_pk)
193+
} else if e.starts_with("spk") {
194+
let default = elements::Script::from_str(
195+
"5120c73ac1b7a518499b9642aed8cfa15d5401e5bd85ad760b937b69521c297722f0",
196+
)
197+
.unwrap();
198+
Ok(CovExtArgs::spk(
199+
self.1.spks.get(e).unwrap_or(&default).clone(),
200+
))
201+
} else if e.starts_with("conf_asset") || e.starts_with("exp_asset") {
202+
let default = if e.starts_with("conf_asset") {
203+
"0adef814ab021498562ab4717287305d3f7abb5686832fe6183e1db495abef7cc7"
204+
} else {
205+
"01663fc0f93e82bdb0bf7da418f5caae09f3f132753114251ecc1bb366e6b2e4d7"
206+
};
207+
let default = encode::deserialize(&Vec::<u8>::from_hex(default).unwrap()).unwrap();
208+
Ok(CovExtArgs::asset(*self.1.assets.get(e).unwrap_or(&default)))
209+
} else if e.starts_with("conf_value") || e.starts_with("exp_value") {
210+
let default = if e.starts_with("conf_value") {
211+
"09def814ab021498562ab4717287305d3f7abb5686832fe6183e1db495abef7cc7"
212+
} else {
213+
"010000000011110000"
214+
};
215+
let default = encode::deserialize(&Vec::<u8>::from_hex(default).unwrap()).unwrap();
216+
Ok(CovExtArgs::value(*self.1.values.get(e).unwrap_or(&default)))
185217
} else {
186-
panic!("CSFS msg must be string 'msg'")
218+
panic!("Unknown extension")
187219
}
188220
}
189221
}

tests/test_introspect.rs

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
//! # rust-miniscript integration test
2+
//!
3+
//! Arith expression fragment integration tests
4+
//!
5+
6+
use elements::pset::PartiallySignedTransaction as Psbt;
7+
use elements::sighash::SigHashCache;
8+
use elements::taproot::{LeafVersion, TapLeafHash};
9+
use elements::{
10+
self, confidential, pset as psbt, secp256k1_zkp as secp256k1, sighash, OutPoint, Script, TxIn,
11+
TxOut, Txid,
12+
};
13+
use elementsd::ElementsD;
14+
use miniscript::miniscript::iter;
15+
use miniscript::psbt::{PsbtExt, PsbtInputExt};
16+
use miniscript::{Descriptor, MiniscriptKey, ToPublicKey};
17+
use rand::RngCore;
18+
mod setup;
19+
use setup::test_util::{self, TestData, PARAMS};
20+
use setup::Call;
21+
use {actual_rand as rand, elements_miniscript as miniscript};
22+
23+
// Find the Outpoint by value.
24+
// Ideally, we should find by scriptPubkey, but this
25+
// works for temp test case
26+
fn get_vout(cl: &ElementsD, txid: Txid, value: u64, spk: Script) -> (OutPoint, TxOut) {
27+
let tx = cl.get_transaction(&txid);
28+
for (i, txout) in tx.output.into_iter().enumerate() {
29+
if txout.value == confidential::Value::Explicit(value) && txout.script_pubkey == spk {
30+
return (OutPoint::new(txid, i as u32), txout);
31+
}
32+
}
33+
unreachable!("Only call get vout on functions which have the expected outpoint");
34+
}
35+
36+
pub fn test_desc_satisfy(cl: &ElementsD, testdata: &TestData, desc: &str) -> Vec<Vec<u8>> {
37+
/* Convert desc into elements one by adding a prefix*/
38+
let desc = format!("el{}", desc);
39+
//
40+
let secp = secp256k1::Secp256k1::new();
41+
let xonly_keypairs = &testdata.secretdata.x_only_keypairs;
42+
let x_only_pks = &testdata.pubdata.x_only_pks;
43+
// Generate some blocks
44+
cl.generate(1);
45+
46+
let desc = test_util::parse_test_desc(&desc, &testdata.pubdata);
47+
let derived_desc = desc.derived_descriptor(&secp, 0).unwrap();
48+
// Next send some btc to each address corresponding to the miniscript
49+
let txid = cl.send_to_address(
50+
&derived_desc.address(&PARAMS).unwrap(), // No blinding
51+
"1", // 1 BTC
52+
);
53+
// Wait for the funds to mature.
54+
cl.generate(2);
55+
// Create a PSBT for each transaction.
56+
// Spend one input and spend one output for simplicity.
57+
let mut psbt = Psbt::new_v2();
58+
// figure out the outpoint from the txid
59+
let (outpoint, witness_utxo) = get_vout(&cl, txid, 100_000_000, derived_desc.script_pubkey());
60+
let txin = TxIn {
61+
previous_output: outpoint,
62+
is_pegin: false,
63+
has_issuance: false,
64+
script_sig: Script::new(),
65+
sequence: 1,
66+
asset_issuance: Default::default(),
67+
witness: Default::default(),
68+
};
69+
psbt.add_input(psbt::Input::from_txin(txin));
70+
// Get a new script pubkey from the node so that
71+
// the node wallet tracks the receiving transaction
72+
// and we can check it by gettransaction RPC.
73+
let addr = cl.get_new_address();
74+
let out = TxOut {
75+
// Had to decrease 'value', so that fees can be increased
76+
// (Was getting insufficient fees error, for deep script trees)
77+
value: confidential::Value::Explicit(99_997_000),
78+
script_pubkey: addr.script_pubkey(),
79+
asset: witness_utxo.asset,
80+
nonce: confidential::Nonce::Null,
81+
witness: Default::default(),
82+
};
83+
psbt.add_output(psbt::Output::from_txout(out));
84+
// ELEMENTS: Add fee output
85+
let fee_out = TxOut::new_fee(3_000, witness_utxo.asset.explicit().unwrap());
86+
psbt.add_output(psbt::Output::from_txout(fee_out));
87+
88+
psbt.inputs_mut()[0]
89+
.update_with_descriptor_unchecked(&desc)
90+
.unwrap();
91+
psbt.inputs_mut()[0].witness_utxo = Some(witness_utxo.clone());
92+
93+
// --------------------------------------------
94+
// Sign the transactions with all keys
95+
// AKA the signer role of psbt
96+
// Get all the pubkeys and the corresponding secret keys
97+
98+
let unsigned_tx = &psbt.extract_tx().unwrap();
99+
let mut sighash_cache = SigHashCache::new(unsigned_tx);
100+
match derived_desc {
101+
Descriptor::TrExt(ref tr) => {
102+
let hash_ty = sighash::SchnorrSigHashType::Default;
103+
104+
let prevouts = [witness_utxo.clone()];
105+
let prevouts = sighash::Prevouts::All(&prevouts);
106+
// ------------------ script spend -------------
107+
let x_only_keypairs_reqd: Vec<(secp256k1::KeyPair, TapLeafHash)> = tr
108+
.iter_scripts()
109+
.flat_map(|(_depth, ms)| {
110+
let leaf_hash = TapLeafHash::from_script(&ms.encode(), LeafVersion::default());
111+
ms.iter_pk_pkh().filter_map(move |pk_pkh| match pk_pkh {
112+
iter::PkPkh::PlainPubkey(pk) => {
113+
let i = x_only_pks.iter().position(|&x| x.to_public_key() == pk);
114+
i.map(|idx| (xonly_keypairs[idx].clone(), leaf_hash))
115+
}
116+
iter::PkPkh::HashedPubkey(hash) => {
117+
let i = x_only_pks
118+
.iter()
119+
.position(|&x| x.to_public_key().to_pubkeyhash() == hash);
120+
i.map(|idx| (xonly_keypairs[idx].clone(), leaf_hash))
121+
}
122+
})
123+
})
124+
.collect();
125+
for (keypair, leaf_hash) in x_only_keypairs_reqd {
126+
let sighash_msg = sighash_cache
127+
.taproot_script_spend_signature_hash(
128+
0,
129+
&prevouts,
130+
leaf_hash,
131+
hash_ty,
132+
testdata.pubdata.genesis_hash,
133+
)
134+
.unwrap();
135+
let msg = secp256k1::Message::from_slice(&sighash_msg[..]).unwrap();
136+
let mut aux_rand = [0u8; 32];
137+
rand::thread_rng().fill_bytes(&mut aux_rand);
138+
let sig = secp.sign_schnorr_with_aux_rand(&msg, &keypair, &aux_rand);
139+
// FIXME: uncomment when == is supported for secp256k1::KeyPair. (next major release)
140+
// let x_only_pk = pks[xonly_keypairs.iter().position(|&x| x == keypair).unwrap()];
141+
// Just recalc public key
142+
let x_only_pk = secp256k1::XOnlyPublicKey::from_keypair(&keypair);
143+
psbt.inputs_mut()[0].tap_script_sigs.insert(
144+
(x_only_pk, leaf_hash),
145+
elements::SchnorrSig {
146+
sig,
147+
hash_ty: hash_ty,
148+
},
149+
);
150+
}
151+
}
152+
_ => {
153+
// Non-tr descriptors
154+
panic!("Only testing Tr covenant descriptor")
155+
}
156+
}
157+
// Add the hash preimages to the psbt
158+
psbt.inputs_mut()[0].sha256_preimages.insert(
159+
testdata.pubdata.sha256,
160+
testdata.secretdata.sha256_pre.to_vec(),
161+
);
162+
println!("Testing descriptor: {}", desc);
163+
// Finalize the transaction using psbt
164+
// Let miniscript do it's magic!
165+
if let Err(e) = psbt.finalize_mall_mut(&secp, testdata.pubdata.genesis_hash) {
166+
// All miniscripts should satisfy
167+
panic!(
168+
"Could not satisfy non-malleably: error{} desc:{} ",
169+
e[0], desc
170+
);
171+
}
172+
let tx = psbt
173+
.extract(&secp, testdata.pubdata.genesis_hash)
174+
.expect("Extraction error");
175+
176+
// Send the transactions to bitcoin node for mining.
177+
// Regtest mode has standardness checks
178+
// Check whether the node accepts the transactions
179+
let txid = cl.send_raw_transaction(&tx);
180+
181+
// Finally mine the blocks and await confirmations
182+
let _blocks = cl.generate(1);
183+
// Get the required transactions from the node mined in the blocks.
184+
// Check whether the transaction is mined in blocks
185+
// Assert that the confirmations are > 0.
186+
let num_conf = cl.call("gettransaction", &[txid.to_string().into()])["confirmations"]
187+
.as_u64()
188+
.unwrap();
189+
assert!(num_conf > 0);
190+
tx.input[0].witness.script_witness.clone()
191+
}
192+
193+
#[rustfmt::skip]
194+
fn test_descs(cl: &ElementsD, testdata: &mut TestData) {
195+
// K : Compressed key available
196+
// K!: Compressed key with corresponding secret key unknown
197+
// X: X-only key available
198+
// X!: X-only key with corresponding secret key unknown
199+
200+
// Test 1: Simple spend with internal key
201+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_asset(exp_asset)))");
202+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_asset(curr_inp_asset)))");
203+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_asset(inp_asset(0))))");
204+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_asset(out_asset(1))))");
205+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),asset_eq(curr_inp_asset,inp_asset(0))))");
206+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),asset_eq(curr_inp_asset,out_asset(0))))");
207+
208+
// same tests for values
209+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_value(exp_value)))");
210+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_value(curr_inp_value)))");
211+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_value(inp_value(0))))");
212+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_value(out_value(1))))");
213+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),value_eq(curr_inp_value,inp_value(0))))");
214+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),value_eq(out_value(0),out_value(0))))");
215+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),value_eq(out_value(1),out_value(1))))");
216+
217+
// same tests for spks
218+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),spk_eq(out_spk(1),out_spk(1))))");
219+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),spk_eq(spk_v1,spk_v1)))");
220+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),spk_eq(curr_inp_spk,inp_spk(0))))");
221+
222+
// Testing the current input index
223+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),curr_idx_eq(0)))");
224+
225+
// test some misc combinations with other miniscript fragments
226+
test_desc_satisfy(cl, testdata,
227+
"tr(X!,and_v(v:pk(X1),and_v(v:is_exp_value(out_value(0)),is_exp_asset(out_asset(0)))))",
228+
);
229+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X1),and_v(v:value_eq(conf_value,conf_value),spk_eq(spk,spk))))");
230+
test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X1),and_v(v:value_eq(conf_value,conf_value),and_v(v:spk_eq(spk,spk),curr_idx_eq(0)))))");
231+
}
232+
233+
#[test]
234+
fn test_introspect() {
235+
let (cl, _, genesis_hash) = &setup::setup(false);
236+
let mut testdata = TestData::new_fixed_data(50, *genesis_hash);
237+
test_descs(cl, &mut testdata);
238+
}

0 commit comments

Comments
 (0)