diff --git a/src/bin/hal-simplicity/cmd/simplicity/info.rs b/src/bin/hal-simplicity/cmd/simplicity/info.rs index 41e2c8c..0f82bcf 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/info.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/info.rs @@ -7,6 +7,7 @@ use super::{Error, ErrorExt as _}; use hal_simplicity::hal_simplicity::{elements_address, Program}; use hal_simplicity::simplicity::{jet, Amr, Cmr, Ihr}; +use simplicity::hex::parse::FromHex as _; use serde::Serialize; @@ -42,20 +43,32 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> { cmd::arg("witness", "a hex encoding of all the witness data for the program") .takes_value(true) .required(false), + cmd::opt( + "state", + "32-byte state commitment to put alongside the program when generating addresess (hex)", + ) + .takes_value(true) + .short("s") + .required(false), ]) } pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { let program = matches.value_of("program").expect("program is mandatory"); let witness = matches.value_of("witness"); + let state = matches.value_of("state"); - match exec_inner(program, witness) { + match exec_inner(program, witness, state) { Ok(info) => cmd::print_output(matches, &info), Err(e) => cmd::print_output(matches, &e), } } -fn exec_inner(program: &str, witness: Option<&str>) -> Result { +fn exec_inner( + program: &str, + witness: Option<&str>, + state: Option<&str>, +) -> Result { // In the future we should attempt to parse as a Bitcoin program if parsing as // Elements fails. May be tricky/annoying in Rust since Program is a // different type from Program. @@ -73,6 +86,11 @@ fn exec_inner(program: &str, witness: Option<&str>) -> Result::from_hex) + .transpose() + .result_context("parsing 32-byte state commitment as hex")?; + Ok(ProgramInfo { jets: "core", commit_base64: program.commit_prog().to_string(), @@ -80,10 +98,15 @@ fn exec_inner(program: &str, witness: Option<&str>) -> Result() -> clap::App<'a, 'a> { cmd::subcommand("update-input", "Attach UTXO data to a PSET input") @@ -32,6 +33,13 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> { .short("c") .takes_value(true) .required(false), + cmd::opt( + "state", + "32-byte state commitment to put alongside the program when generating addresess (hex)", + ) + .takes_value(true) + .short("s") + .required(false), // FIXME add merkle path, needed to compute nontrivial control blocks ]) } @@ -43,8 +51,9 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { let internal_key = matches.value_of("internal-key"); let cmr = matches.value_of("cmr"); + let state = matches.value_of("state"); - match exec_inner(pset_b64, input_idx, input_utxo, internal_key, cmr) { + match exec_inner(pset_b64, input_idx, input_utxo, internal_key, cmr, state) { Ok(info) => cmd::print_output(matches, &info), Err(e) => cmd::print_output(matches, &e), } @@ -57,6 +66,7 @@ fn exec_inner( input_utxo: &str, internal_key: Option<&str>, cmr: Option<&str>, + state: Option<&str>, ) -> Result { let mut pset: elements::pset::PartiallySignedTransaction = pset_b64.parse().result_context("decoding PSET")?; @@ -87,6 +97,14 @@ fn exec_inner( .result_context("input UTXO"); } + // FIXME state is meaningless without CMR; should we warn here + // FIXME also should we warn if you don't provide a CMR? seems like if you're calling `simplicity pset update-input` + // you probably have a simplicity program right? maybe we should even provide a --no-cmr flag + let state = state + .map(<[u8; 32]>::from_hex) + .transpose() + .result_context("parsing 32-byte state commitment as hex")?; + let mut updated_values = vec![]; if let Some(internal_key) = internal_key { updated_values.push("tap_internal_key"); @@ -97,7 +115,7 @@ fn exec_inner( // Guess that the given program is the only Tapleaf. This is the case for addresses // generated from the web IDE, and from `hal-simplicity simplicity info`, and for // most "test" scenarios. We need to design an API to handle more general cases. - let spend_info = taproot_spend_info(internal_key, cmr); + let spend_info = taproot_spend_info(internal_key, state, cmr); if spend_info.output_key().as_inner().serialize() != input_utxo.script_pubkey[2..] { // If our guess was wrong, at least error out.. return Err(format!("CMR and internal key imply output key {}, which does not match input scriptPubKey {}", spend_info.output_key().as_inner(), input_utxo.script_pubkey)) diff --git a/src/hal_simplicity.rs b/src/hal_simplicity.rs index e3732d5..c7af324 100644 --- a/src/hal_simplicity.rs +++ b/src/hal_simplicity.rs @@ -112,11 +112,29 @@ fn script_ver(cmr: simplicity::Cmr) -> (elements::Script, elements::taproot::Lea /// for a Taptree with this CMR as its single leaf. pub fn taproot_spend_info( internal_key: secp256k1::XOnlyPublicKey, + state: Option<[u8; 32]>, cmr: simplicity::Cmr, ) -> TaprootSpendInfo { let builder = TaprootBuilder::new(); let (script, version) = script_ver(cmr); - let builder = builder.add_leaf_with_ver(0, script, version).expect("tap tree should be valid"); + let builder = if let Some(state) = state { + use elements::hashes::{sha256, Hash as _, HashEngine as _}; + let tag = sha256::Hash::hash(b"TapData"); + let mut eng = sha256::Hash::engine(); + eng.input(tag.as_byte_array()); + eng.input(tag.as_byte_array()); + eng.input(&state); + let state_hash = sha256::Hash::from_engine(eng); + + builder + .add_leaf_with_ver(1, script, version) + .expect("tap tree should be valid") + .add_hidden(1, state_hash) + .expect("tap tree should be valid") + } else { + builder.add_leaf_with_ver(0, script, version).expect("tap tree should be valid") + }; + builder.finalize(secp256k1::SECP256K1, internal_key).expect("tap tree should be valid") } @@ -125,9 +143,10 @@ pub fn taproot_spend_info( /// internal key and this CMR as its single leaf. pub fn elements_address( cmr: simplicity::Cmr, + state: Option<[u8; 32]>, params: &'static elements::AddressParams, ) -> elements::Address { - let info = taproot_spend_info(unspendable_internal_key(), cmr); + let info = taproot_spend_info(unspendable_internal_key(), state, cmr); let blinder = None; elements::Address::p2tr( secp256k1::SECP256K1, diff --git a/tests/cli.rs b/tests/cli.rs index 3646d18..00a5670 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1077,7 +1077,7 @@ hal-simplicity-simplicity-info 0.1.0 Parse a base64-encoded Simplicity program and decode it USAGE: - hal-simplicity simplicity info [FLAGS] [witness] + hal-simplicity simplicity info [FLAGS] [OPTIONS] [witness] FLAGS: -r, --elementsregtest run in elementsregtest mode @@ -1086,6 +1086,9 @@ FLAGS: -v, --verbose print verbose logging output to stderr -y, --yaml print output in YAML instead of JSON +OPTIONS: + -s, --state 32-byte state commitment to put alongside the program when generating addresess (hex) + ARGS: a Simplicity program in base64 a hex encoding of all the witness data for the program @@ -1103,7 +1106,7 @@ error: The following required arguments were not provided: USAGE: - hal-simplicity simplicity info [FLAGS] [witness] + hal-simplicity simplicity info [FLAGS] [OPTIONS] [witness] For more information try --help ",