Last active
December 12, 2022 21:08
-
-
Save futurepaul/7c09ce3491dcfb9ca103bc46127435d4 to your computer and use it in GitHub Desktop.
example usage of miniscript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use bitcoin::blockdata::{opcodes, script}; | |
use miniscript::{Descriptor, Miniscript, Policy}; | |
use std::str::FromStr; | |
use secp256k1; | |
fn key_from_secret_key(sk: secp256k1::SecretKey, secp: &secp256k1::Secp256k1<secp256k1::All>) -> bitcoin::PublicKey { | |
bitcoin::PublicKey { | |
key: secp256k1::PublicKey::from_secret_key( | |
&secp, | |
&secp256k1::SecretKey::from_slice(&sk[..]).expect("secret key"), | |
), | |
compressed: true, | |
} | |
} | |
fn main() { | |
//Let's get a public key and a secret key to get started | |
let secp = secp256k1::Secp256k1::new(); | |
let sk = secp256k1::SecretKey::from_slice(&b"sally was a secret key, she said"[..]).unwrap(); | |
let pk = key_from_secret_key(sk, &secp); | |
// Here are three ways to represent the same spending policy | |
// this policy can be written in Miniscript notation as: | |
// and(pk(C),or(pk(C),aor(pk(C),time(1000)))) | |
// where C is a compressed publickey | |
// Bitcoin Script | |
let policy_script = script::Builder::new() | |
.push_key(&pk) | |
.push_opcode(opcodes::all::OP_CHECKSIG) | |
.push_opcode(opcodes::all::OP_NOTIF) | |
.push_key(&pk) | |
.push_opcode(opcodes::all::OP_CHECKSIG) | |
.push_opcode(opcodes::all::OP_NOTIF) | |
.push_int(1000) | |
.push_opcode(opcodes::OP_CSV) | |
.push_opcode(opcodes::all::OP_DROP) | |
.push_opcode(opcodes::all::OP_ENDIF) | |
.push_opcode(opcodes::all::OP_ENDIF) | |
.push_key(&pk) | |
.push_opcode(opcodes::all::OP_CHECKSIG) | |
.into_script(); | |
// Miniscript AST | |
let policy_ast = Policy::And( | |
Box::new(Policy::Key(pk.clone())), | |
Box::new(Policy::Or( | |
Box::new(Policy::Key(pk.clone())), | |
Box::new(Policy::AsymmetricOr( | |
Box::new(Policy::Key(pk.clone())), | |
Box::new(Policy::Time(1000)), | |
)), | |
)), | |
); | |
// Parsed Miniscript String | |
let pk_string = pk.to_string(); | |
let policy_string = format!( | |
"and(pk({}),or(pk({}),aor(pk({}),time(1000))))", | |
pk_string, pk_string, pk_string | |
); | |
// Once the Miniscript policies are encoded to Script, all three scripts are identical | |
let policy_ast_encoded = policy_ast.compile().encode(); | |
let policy_string_encoded = Policy::<bitcoin::PublicKey>::from_str(&policy_string) | |
.unwrap() | |
.compile() | |
.encode(); | |
assert_eq!(policy_script, policy_ast_encoded); | |
assert_eq!(policy_script, policy_string_encoded); | |
// If you don't need a real key, you can use DummyKey | |
use miniscript::DummyKey; | |
let policy_string_dummy = | |
Policy::<DummyKey>::from_str("and(pk(),or(pk(),aor(pk(),time(1000))))").unwrap(); | |
// Or you can make an abstract policy and ask it questions | |
let policy_string_abstract = | |
Policy::<String>::from_str("and(pk(),or(pk(),aor(pk(),time(1000))))").unwrap(); | |
let policy_abstract = policy_string_abstract.abstract_policy(); | |
// Now you can learn all sorts of things! | |
dbg!(policy_abstract.minimum_n_keys()); | |
dbg!(policy_abstract.timelocks()); | |
dbg!(policy_abstract.n_keys()); | |
// While we can write any Script we want, there are standard ways to embed Scripts in transaction outputs | |
// These are called [Output Descriptors](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) | |
// First we create a policy and compile it to a Miniscript | |
let policy = Policy::<bitcoin::PublicKey>::from_str(&policy_string).unwrap(); | |
let miniscript = policy.compile(); | |
// Then we wrap it in a Descriptor. This one is Pay To Script Hash. | |
let descriptor_p2sh = Descriptor::Sh(miniscript.clone()); | |
// As a quick sanity check, we can make sure the P2SH address starts with a 3 | |
let address = descriptor_p2sh.address(bitcoin::Network::Bitcoin).unwrap(); | |
dbg!(address); | |
// We can also estimate the satisfaction cost | |
dbg!(descriptor_p2sh.max_satisfaction_weight()); | |
// Here's our pubkey, ready to share with the world (or put on the blockchain) | |
let script_pubkey = descriptor_p2sh.script_pubkey(); | |
// Here's another way to get the same script_pubkey, just so you know what's happening | |
let same_script_pubkey = miniscript.encode().to_p2sh(); | |
assert_eq!(script_pubkey, same_script_pubkey); | |
// Now let's create a transaction and a scriptSig to satisfy our Script | |
let mut txin = bitcoin::TxIn { | |
previous_output: bitcoin::OutPoint::default(), | |
script_sig: bitcoin::Script::new(), | |
sequence: 100, | |
witness: vec![], | |
}; | |
let msg = | |
secp256k1::Message::from_slice(&b"michael was a message, amusingly"[..]).expect("32 bytes"); | |
// We're finally using our secret key from earlier! | |
let sig = secp.sign(&msg, &sk); | |
// Given a public key, return the corresponding signature | |
let sigfn = |key: &bitcoin::PublicKey| { | |
if *key == pk { | |
Some((sig, bitcoin::SigHashType::All)) | |
} else { | |
None | |
} | |
}; | |
// NO_HASHES is the "None" type, because we have no hash preimage in this scenario | |
// Alternatively, we could provide a hashfn which would take a hash and return the preimage | |
use miniscript::NO_HASHES; | |
// The satisfy method creates a scriptSig or witness as appropriate and adds it to the transaction | |
descriptor_p2sh | |
.satisfy(&mut txin, Some(&sigfn), NO_HASHES, 0) | |
.expect("satisfaction to succeed"); | |
dbg!(txin.clone()); | |
// | |
// | |
// Now for something (slightly more) practical: let's make a two of three multisig that becomes one of three after 100 blocks | |
// First we need some keys | |
let sk1 = secp256k1::SecretKey::from_slice(&b"alice's key plus pad for valid k"[..]).unwrap(); | |
let sk2 = secp256k1::SecretKey::from_slice(&b"bob's key plus pad for valid key"[..]).unwrap(); | |
let sk3 = secp256k1::SecretKey::from_slice(&b"carol's key plus pad for valid k"[..]).unwrap(); | |
let pk1 = key_from_secret_key(sk1, &secp); | |
let pk2 = key_from_secret_key(sk2, &secp); | |
let pk3 = key_from_secret_key(sk3, &secp); | |
// Here's our Policy. Pretty straightforward | |
let multi_miniscript = Policy::<bitcoin::PublicKey>::Or( | |
Box::new(Policy::Multi(2, vec![pk1, pk2, pk3])), | |
Box::new(Policy::And( | |
Box::new(Policy::Time(100)), | |
Box::new(Policy::Multi(1, vec![pk1, pk2, pk3])) | |
))).compile(); | |
// Let's check to see if we wrote the policy correctly | |
let mut abs = multi_miniscript.abstract_policy(); | |
// The least keys necessary to unlock is one | |
assert_eq!(abs.minimum_n_keys(), 1); | |
// But at time < 100 we need two keys | |
abs = abs.before_time(99).unwrap(); | |
assert_eq!(abs.minimum_n_keys(), 2); | |
// We're going to use p2sh again because it's good for multi-sig | |
let multi_p2sh = Descriptor::Sh(multi_miniscript); | |
// Another elegant Bitcoin Script achieved: | |
let multi_script_pubkey = multi_p2sh.script_pubkey(); | |
// QUESTION just to make sure I'm not crazy: this is what we send the Bitcoin to, correct? | |
dbg!(multi_p2sh.address(bitcoin::Network::Bitcoin).unwrap()); | |
//Now let's try to move the bitcoin | |
let mut multi_txin = bitcoin::TxIn { | |
previous_output: bitcoin::OutPoint::default(), | |
script_sig: bitcoin::Script::new(), | |
sequence: 100, | |
witness: vec![], | |
}; | |
//QUESTION does it ever matter what the message is? | |
let msg = | |
secp256k1::Message::from_slice(&b"michael was a message, amusingly"[..]).expect("32 bytes"); | |
let sig1 = secp.sign(&msg, &sk1); | |
let sig2 = secp.sign(&msg, &sk2); | |
let sig3 = secp.sign(&msg, &sk3); | |
// Given a public key, return the corresponding signature | |
let sigfn_multi = |key: &bitcoin::PublicKey| { | |
if *key == pk1 { | |
Some((sig1, bitcoin::SigHashType::All)) | |
} else if *key == pk2 { | |
Some((sig2, bitcoin::SigHashType::All)) | |
} else if *key == pk3 { | |
Some((sig3, bitcoin::SigHashType::All)) | |
} else { | |
None | |
} | |
}; | |
// Try commenting out different signatures to see what works | |
// At age 0-99 you should need two, at 100+ you should only need one | |
multi_p2sh | |
.satisfy(&mut multi_txin, Some(&sigfn_multi), NO_HASHES, 99) | |
.expect("satisfaction to succeed"); | |
dbg!(multi_txin.clone()); | |
} |
Hey Paul,
sigfn = oracle for the signature function which takes the public key as input and gives the corresponding signature
hashfn = oracle for the hash function which takes hash as the input and gives back the preimage.
Finally, the miniscript you are compiling does not match the scriptsig. If you want the last assert to satisfy, you must use miniscript= Miniscript(astelem::AstElem::Pk(pk))
. Or Alternatively, you can supply a witness for and(pk(),or(pk(),aor(pk(),time(1000))))
which should be
let manual_txin = bitcoin::TxIn {
previous_output: bitcoin::OutPoint::default(),
script_sig: script::Builder::new()
.push_slice(&sigser[..])
.push_slice(&sigser[..]) //note the second push
.push_slice(&miniscript.encode()[..])
.into_script(),
sequence: 100,
witness: vec![],
};
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's my cargo.toml: