Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/actions/simplicity/pset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use elements::taproot::ControlBlock;
use elements::Script;
use serde::Serialize;

use crate::hal_simplicity::control_block_commits_to;
use crate::simplicity::jet::elements::{ElementsEnv, ElementsUtxo};
use crate::simplicity::Cmr;

Expand All @@ -45,6 +46,12 @@ pub enum PsetError {

#[error("witness_utxo field not populated for input {0}")]
MissingWitnessUtxo(usize),

#[error("control block for CMR {cmr} does not match input scriptPubKey {script_pubkey}")]
ControlBlockMismatch {
cmr: String,
script_pubkey: String,
},
}

#[derive(Serialize)]
Expand Down Expand Up @@ -111,6 +118,14 @@ pub fn execution_environment(
})
.collect::<Result<Vec<_>, _>>()?;

let script_pubkey = &input_utxos[input_idx].script_pubkey;
if !control_block_commits_to(&control_block, cmr, script_pubkey) {
return Err(PsetError::ControlBlockMismatch {
cmr: cmr.to_string(),
script_pubkey: script_pubkey.to_string(),
});
}

let tx_env = ElementsEnv::new(
tx,
input_utxos,
Expand All @@ -123,3 +138,40 @@ pub fn execution_environment(

Ok((tx_env, control_block, tap_leaf))
}

#[cfg(test)]
mod tests {
use super::*;

use elements::{LockTime, Transaction, TxIn, TxOut};

use crate::hal_simplicity::script_ver;
use crate::test_helpers::test_leaf;

#[test]
fn control_block_must_match_witness_utxo() {
let cmr = Cmr::from_byte_array([0; 32]);
let (control_block, script_pubkey) = test_leaf(cmr);
let (_, other_script_pubkey) = test_leaf(Cmr::from_byte_array([1; 32]));

let pset_spending = |script_pubkey: Script| {
let mut pset = PartiallySignedTransaction::from_tx(Transaction {
version: 2,
lock_time: LockTime::ZERO,
input: vec![TxIn::default()],
output: vec![],
});
let input = &mut pset.inputs_mut()[0];
input.witness_utxo = Some(TxOut {
script_pubkey,
..TxOut::default()
});
input.tap_scripts.insert(control_block.clone(), script_ver(cmr));
pset
};

assert!(execution_environment(&pset_spending(script_pubkey), 0, cmr, None).is_ok());
let err = execution_environment(&pset_spending(other_script_pubkey), 0, cmr, None).err();
assert!(matches!(err, Some(PsetError::ControlBlockMismatch { .. })), "{:?}", err);
}
}
108 changes: 99 additions & 9 deletions src/actions/simplicity/sighash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::simplicity::elements::taproot::ControlBlock;
use crate::simplicity::jet::elements::ElementsEnv;

use crate::actions::simplicity::ParseElementsUtxoError;
use crate::hal_simplicity::control_block_commits_to;

#[derive(Debug, thiserror::Error)]
pub enum SimplicitySighashError {
Expand All @@ -41,7 +42,7 @@ pub enum SimplicitySighashError {
#[error("invalid control block decoding: {0}")]
ControlBlockDecoding(elements::taproot::TaprootError),

#[error("input index {index} out-of-range for PSET with {n_inputs} inputs")]
#[error("input index {index} out-of-range for transaction with {n_inputs} inputs")]
InputIndexOutOfRange {
index: u32,
n_inputs: usize,
Expand All @@ -55,6 +56,12 @@ pub enum SimplicitySighashError {
#[error("with a raw transaction, control-block must be provided")]
ControlBlockRequired,

#[error("control block for CMR {cmr} does not match input scriptPubKey {script_pubkey}")]
ControlBlockMismatch {
cmr: String,
script_pubkey: String,
},

#[error("witness UTXO field not populated for input {input}")]
WitnessUtxoMissing {
input: usize,
Expand Down Expand Up @@ -134,6 +141,13 @@ pub fn simplicity_sighash(
}
};
let input_idx: u32 = input_idx.parse().map_err(SimplicitySighashError::InputIndexParsing)?;
// Checked here because the PSET input lookup below is skipped when a control block is given.
if input_idx as usize >= tx.input.len() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 6627744:

This loses the "probably fine" comment on the cast. But we should probably just get rid of the cast and use try_from to convert the u32 to usize. If the try_from fails we can return InputIndexOutOfRange.

Also, rather than doing an explicit check like this then having a panicking [] below, we should just use get here even though the actual input is unused when a control block is given.

return Err(SimplicitySighashError::InputIndexOutOfRange {
index: input_idx,
n_inputs: tx.input.len(),
});
}
let cmr: Cmr = cmr.parse().map_err(SimplicitySighashError::CmrParsing)?;

// If the user specifies a control block, use it. Otherwise query the PSET.
Expand All @@ -143,14 +157,7 @@ pub fn simplicity_sighash(
// value f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2
ControlBlock::from_slice(&cb_bytes).map_err(SimplicitySighashError::ControlBlockDecoding)?
} else if let Some(ref pset) = pset {
let n_inputs = pset.n_inputs();
let input = pset
.inputs()
.get(input_idx as usize) // cast u32->usize probably fine
.ok_or(SimplicitySighashError::InputIndexOutOfRange {
index: input_idx,
n_inputs,
})?;
let input = &pset.inputs()[input_idx as usize]; // bounds-checked above

let mut control_block = None;
for (cb, script_ver) in &input.tap_scripts {
Expand Down Expand Up @@ -203,6 +210,14 @@ pub fn simplicity_sighash(
});
}

let script_pubkey = &input_utxos[input_idx as usize].script_pubkey;
if !control_block_commits_to(&control_block, cmr, script_pubkey) {
return Err(SimplicitySighashError::ControlBlockMismatch {
cmr: cmr.to_string(),
script_pubkey: script_pubkey.to_string(),
});
}

// Default to Bitcoin blockhash.
let genesis_hash = match genesis_hash {
Some(s) => s.parse().map_err(SimplicitySighashError::GenesisHashParsing)?,
Expand Down Expand Up @@ -268,3 +283,78 @@ pub fn simplicity_sighash(
},
})
}

#[cfg(test)]
mod tests {
use super::*;

use elements::encode::serialize_hex;
use elements::{LockTime, Script, Transaction, TxIn};

use crate::test_helpers::test_leaf;

const CMR: &str = "0000000000000000000000000000000000000000000000000000000000000000";

fn two_input_tx() -> Transaction {
Transaction {
version: 2,
lock_time: LockTime::ZERO,
input: vec![TxIn::default(), TxIn::default()],
output: vec![],
}
}

/// Sighash using an explicit control block for `CMR`, with both input UTXOs
/// locked to `script_pubkey`.
fn sighash(
tx: &str,
idx: &str,
script_pubkey: &Script,
) -> Result<SighashInfo, SimplicitySighashError> {
let (control_block, _) = test_leaf(CMR.parse().unwrap());
let utxo = format!("{}:{}:0.0001", hex::encode(script_pubkey.as_bytes()), "00".repeat(32));
simplicity_sighash(
tx,
idx,
CMR,
Some(&hex::encode(control_block.serialize())),
None,
None,
None,
None,
Some(&[&utxo, &utxo]),
)
}

#[test]
fn input_index_out_of_range_with_explicit_control_block() {
let (_, script_pubkey) = test_leaf(CMR.parse().unwrap());
let tx = two_input_tx();
for tx in [serialize_hex(&tx), PartiallySignedTransaction::from_tx(tx).to_string()] {
assert!(sighash(&tx, "1", &script_pubkey).is_ok());
let err = sighash(&tx, "2", &script_pubkey).err();
assert!(
matches!(
err,
Some(SimplicitySighashError::InputIndexOutOfRange {
index: 2,
n_inputs: 2
})
),
"{:?}",
err
);
}
}

#[test]
fn control_block_must_match_input_utxo() {
let (_, other_script_pubkey) = test_leaf(Cmr::from_byte_array([1; 32]));
let err = sighash(&serialize_hex(&two_input_tx()), "0", &other_script_pubkey).err();
assert!(
matches!(err, Some(SimplicitySighashError::ControlBlockMismatch { .. })),
"{:?}",
err
);
}
}
28 changes: 26 additions & 2 deletions src/hal_simplicity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

use std::sync::Arc;

use elements::taproot::{TaprootBuilder, TaprootSpendInfo};
use elements::schnorr::TweakedPublicKey;
use elements::taproot::{ControlBlock, TaprootBuilder, TaprootSpendInfo};
use simplicity::bitcoin::secp256k1;
use simplicity::jet::Jet;
use simplicity::{BitIter, CommitNode, DecodeError, ParseError, RedeemNode};
Expand Down Expand Up @@ -103,7 +104,9 @@ pub fn unspendable_internal_key() -> secp256k1::XOnlyPublicKey {
.expect("key should be valid")
}

fn script_ver(cmr: simplicity::Cmr) -> (elements::Script, elements::taproot::LeafVersion) {
pub(crate) fn script_ver(
cmr: simplicity::Cmr,
) -> (elements::Script, elements::taproot::LeafVersion) {
let script = elements::script::Script::from(cmr.as_ref().to_vec());
(script, simplicity::leaf_version())
}
Expand Down Expand Up @@ -138,6 +141,27 @@ pub fn taproot_spend_info(
builder.finalize(secp256k1::SECP256K1, internal_key).expect("tap tree should be valid")
}

/// Whether `control_block` proves that the Simplicity leaf `cmr` is committed to
/// by `script_pubkey`, for any Taptree shape.
pub(crate) fn control_block_commits_to(
control_block: &ControlBlock,
cmr: simplicity::Cmr,
script_pubkey: &elements::Script,
) -> bool {
let (script, version) = script_ver(cmr);
if control_block.leaf_version != version || !script_pubkey.is_v1_p2tr() {
return false;
}
match secp256k1::XOnlyPublicKey::from_slice(&script_pubkey[2..]) {
Ok(output_key) => control_block.verify_taproot_commitment(
secp256k1::SECP256K1,
&TweakedPublicKey::new(output_key),
&script,
),
Err(_) => false,
}
}

/// Given a Simplicity CMR, computes an unconfidential Elements address
/// (for the given network) corresponding to a Taptree with an unspendable
/// internal key and this CMR as its single leaf.
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ pub mod tx;

pub mod confidential;

#[cfg(test)]
mod test_helpers;

pub use elements::bitcoin;
pub use hal::HexBytes;

Expand Down
13 changes: 13 additions & 0 deletions src/test_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Helpers shared between unit tests in different modules.

use elements::taproot::ControlBlock;
use elements::Script;

use crate::hal_simplicity::{script_ver, taproot_spend_info, unspendable_internal_key};

/// The control block and scriptPubKey for a Taptree with this CMR as its single leaf.
pub fn test_leaf(cmr: simplicity::Cmr) -> (ControlBlock, Script) {
let spend_info = taproot_spend_info(unspendable_internal_key(), None, cmr);
let control_block = spend_info.control_block(&script_ver(cmr)).expect("leaf is in tree");
(control_block, Script::new_v1_p2tr_tweaked(spend_info.output_key()))
}
Loading