diff --git a/doc/extension_spec.md b/doc/extension_spec.md index efa42870..be0c86ff 100644 --- a/doc/extension_spec.md +++ b/doc/extension_spec.md @@ -67,6 +67,7 @@ Name | Script curr_inp_asset | `PUSHCURRENTINPUTINDEX INPSECTINPUTASSET` inp_asset(i) | `i INPSECTINPUTASSET` out_asset(i) | `i INPSECTOUTPUTASSET` +curr_out_asset | `PUSHCURRENTINPUTINDEX INPSECTOUTPUTASSET` ### ValueExpr @@ -80,6 +81,7 @@ Name | Script curr_inp_value | `PUSHCURRENTINPUTINDEX INPSECTINPUTVALUE` inp_value(i) | `i INPSECTINPUTVALUE` out_value(i) | `i INPSECTOUTPUTVALUE` +curr_out_value | `PUSHCURRENTINPUTINDEX INPSECTOUTPUTVALUE` ### SpkExpr: Script PubKey Expression @@ -94,6 +96,7 @@ Name | Script curr_inp_spk | `PUSHCURRENTINPUTINDEX INPSECTINPUTSCRIPTPUBKEY` inp_spk(i) | `i INPSECTINPUTSCRIPTPUBKEY` out_spk(i) | `i INPSECTOUTPUTASSETSCRIPTPUBKEY` +curr_out_spk | `PUSHCURRENTINPUTINDEX INPSECTOUTPUTASSETSCRIPTPUBKEY` ## Introspection Operations diff --git a/examples/apo.rs b/examples/apo.rs new file mode 100644 index 00000000..b442e2b0 --- /dev/null +++ b/examples/apo.rs @@ -0,0 +1,348 @@ +//! Sighash any-prevout emulation using the new opcodes. + +use bitcoin; +use elements::confidential::{Asset, Value}; +use elements::encode::{self, deserialize}; +use elements::hashes::hex::{FromHex, ToHex}; +use elements::{confidential, opcodes, AddressParams, AssetId, TxOut}; +use miniscript::descriptor::Tr; +use miniscript::extensions::{ + AssetExpr, CovExtArgs, CovOps, ParseableExt, Spk, SpkExpr, ValueExpr, +}; +use miniscript::miniscript::satisfy::{Satisfaction, Witness}; +use miniscript::miniscript::types::extra_props::{OpLimits, TimelockInfo}; +use miniscript::miniscript::types::{Correctness, ExtData, Malleability}; +use miniscript::{expression, Extension, TxEnv}; +extern crate elements_miniscript as miniscript; + +use std::fmt; + +/// The data that needs to be signed in apo + all. +/// We can decompose this into into individual parts for fixing version, amt, script pubkey +/// +/// This structure is onyl an example of how one might implement extension. We do pay any +/// special attention the serialization format, order of serialization etc. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct SighashAllAPO { + /// The outputs of transaction + outputs: Vec, + /// The input script pubkey + in_asset: elements::confidential::Asset, + /// Input value + in_value: elements::confidential::Value, + /// Input script pubkey + in_spk: elements::Script, + /// The tx version + version: u32, + /// The tx locktime + locktime: u32, + /// The tx sequence + sequence: u32, +} + +impl SighashAllAPO { + /// Evaluate the sighash_all_apo + pub fn eval(&self, env: &TxEnv) -> Result { + let tx_inp = env + .tx() + .input + .get(env.idx()) + .ok_or(miniscript::interpreter::Error::IncorrectCovenantWitness)?; + let spent_utxo = env + .spent_utxos() + .get(env.idx()) + .ok_or(miniscript::interpreter::Error::IncorrectCovenantWitness)?; + if tx_inp.sequence != self.sequence + || env.tx().version != self.version + || env.tx().lock_time != self.locktime + || spent_utxo.asset != self.in_asset + || spent_utxo.value != self.in_value + || spent_utxo.script_pubkey != self.in_spk + || env.tx().output != self.outputs + { + return Ok(false); + } + Ok(true) + } +} + +impl PartialOrd for SighashAllAPO { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SighashAllAPO { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // HACKY implementation that allocates a string + self.to_string().cmp(&other.to_string()) + } +} + +impl fmt::Display for SighashAllAPO { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "all_apo({},{},{},{},{},{},{})", + encode::serialize_hex(&self.outputs), + encode::serialize_hex(&self.in_asset), + encode::serialize_hex(&self.in_value), + encode::serialize_hex(&self.in_spk), + self.version, + self.locktime, + self.sequence, + ) + } +} + +impl Extension for SighashAllAPO { + fn corr_prop(&self) -> miniscript::miniscript::types::Correctness { + Correctness { + base: miniscript::miniscript::types::Base::B, + input: miniscript::miniscript::types::Input::Zero, + dissatisfiable: true, + unit: true, + } + } + + fn mall_prop(&self) -> miniscript::miniscript::types::Malleability { + Malleability { + dissat: miniscript::miniscript::types::Dissat::Unknown, + safe: false, + non_malleable: true, + } + } + + fn extra_prop(&self) -> miniscript::miniscript::types::ExtData { + ExtData { + pk_cost: 500, // dummy size, check real size later + has_free_verify: true, + stack_elem_count_sat: Some(0), + stack_elem_count_dissat: Some(0), + max_sat_size: Some((0, 0)), + max_dissat_size: Some((0, 0)), + timelock_info: TimelockInfo::default(), + exec_stack_elem_count_sat: Some(2), // max stack size during execution = 2 elements + exec_stack_elem_count_dissat: Some(2), + ops: OpLimits { + // Opcodes are really not relevant in tapscript as BIP342 removes all rules on them + count: 1, + sat: Some(0), + nsat: Some(0), + }, + } + } + + fn script_size(&self) -> usize { + todo!() + } + + fn from_name_tree( + name: &str, + children: &[miniscript::expression::Tree<'_>], + ) -> Result { + if children.len() == 7 && name == "all_apo" { + if children.iter().any(|x| !x.args.is_empty()) { + return Err(()); + } + let outputs = deser_hex::>(children[0].name)?; + let in_asset = deser_hex::(children[1].name)?; + let in_value = deser_hex::(children[2].name)?; + let in_spk = deser_hex::(children[3].name)?; + let version = expression::parse_num(children[4].name).map_err(|_e| ())?; + let locktime = expression::parse_num(children[5].name).map_err(|_e| ())?; + let sequence = expression::parse_num(children[6].name).map_err(|_e| ())?; + Ok(SighashAllAPO { + outputs, + in_asset, + in_value, + in_spk, + version, + locktime, + sequence, + }) + } else { + // Correct error handling while parsing fromtree + Err(()) + } + } +} + +impl ParseableExt for SighashAllAPO { + fn from_token_iter( + _tokens: &mut miniscript::miniscript::lex::TokenIter<'_>, + ) -> Result { + // Parsing back from script is currently not implemented + Err(()) + } + + fn evaluate<'intp, 'txin>( + &'intp self, + _stack: &mut miniscript::interpreter::Stack<'txin>, + txenv: Option<&miniscript::TxEnv>, + ) -> Result { + let env = txenv.ok_or(miniscript::interpreter::Error::IncorrectCovenantWitness)?; + self.eval(env) + } + + #[rustfmt::skip] + fn push_to_builder(&self, builder: elements::script::Builder) -> elements::script::Builder { + let mut builder = builder; + for (i, out) in self.outputs.iter().enumerate() { + let asset_eq = CovOps::::AssetEq( + AssetExpr::Const(out.asset.into()), + AssetExpr::Output(i), + ); + let value_eq = CovOps::::ValueEq( + ValueExpr::Const(out.value.into()), + ValueExpr::Output(i), + ); + let spk_eq = CovOps::::SpkEq( + SpkExpr::Const(Spk(out.script_pubkey.clone()).into()), + SpkExpr::Output(i), + ); + builder = asset_eq.push_to_builder(builder).push_opcode(opcodes::all::OP_VERIFY); + builder = value_eq.push_to_builder(builder).push_opcode(opcodes::all::OP_VERIFY); + builder = spk_eq.push_to_builder(builder).push_opcode(opcodes::all::OP_VERIFY); + } + + use opcodes::all::*; + builder = builder + .push_opcode(OP_INSPECTVERSION).push_slice(&self.version.to_le_bytes()).push_opcode(OP_EQUALVERIFY) + .push_opcode(OP_INSPECTLOCKTIME).push_slice(&self.locktime.to_le_bytes()).push_opcode(OP_EQUALVERIFY); + builder = builder + .push_opcode(OP_PUSHCURRENTINPUTINDEX) + .push_opcode(OP_INSPECTINPUTSEQUENCE) + .push_slice(&self.sequence.to_le_bytes()) + .push_opcode(OP_EQUALVERIFY); + let in_asset_eq = CovOps::::AssetEq( + AssetExpr::Const(self.in_asset.into()), + AssetExpr::CurrInputAsset, + ); + let in_value_eq = CovOps::::ValueEq( + ValueExpr::Const(self.in_value.into()), + ValueExpr::CurrInputValue, + ); + let in_spk_eq = CovOps::::SpkEq( + SpkExpr::Const(Spk(self.in_spk.clone()).into()), + SpkExpr::CurrInputSpk, + ); + builder = in_asset_eq.push_to_builder(builder).push_opcode(opcodes::all::OP_VERIFY); + builder = in_value_eq.push_to_builder(builder).push_opcode(opcodes::all::OP_VERIFY); + in_spk_eq.push_to_builder(builder) + } + + fn satisfy(&self, sat: &S) -> miniscript::miniscript::satisfy::Satisfaction + where + Pk: miniscript::ToPublicKey, + S: miniscript::Satisfier, + { + let env = match ( + sat.lookup_tx(), + sat.lookup_spent_utxos(), + sat.lookup_curr_inp(), + ) { + (Some(tx), Some(spent_utxos), Some(idx)) => TxEnv::new(tx, spent_utxos, idx), + _ => { + return Satisfaction { + stack: Witness::Impossible, + has_sig: false, + } + } + }; + let env = match env { + Some(env) => env, + None => { + return Satisfaction { + stack: Witness::Impossible, + has_sig: false, + } + } + }; + let wit = match self.eval(&env) { + Ok(false) => Witness::Unavailable, + Ok(true) => Witness::empty(), + Err(_e) => Witness::Impossible, + }; + Satisfaction { + stack: wit, + has_sig: false, + } + } + + fn dissatisfy(&self, _sat: &S) -> miniscript::miniscript::satisfy::Satisfaction + where + Pk: miniscript::ToPublicKey, + S: miniscript::Satisfier, + { + // This is incorrect!!!!, but done for testing purposes + Satisfaction { + stack: Witness::Impossible, + has_sig: false, + } + } +} + +fn deser_hex(hex: &str) -> Result +where + T: encode::Decodable, +{ + let bytes = Vec::::from_hex(hex).map_err(|_| ())?; + deserialize(&bytes).map_err(|_| ()) +} + +fn main() { + let tap_script = elements::script::Builder::default() + .push_opcode(opcodes::all::OP_PUSHNUM_1) + .push_slice( + &Vec::::from_hex( + "052ef9779ac3955ef438bbcde77411f31bf0e05fbe1b2b563fb5f0475c8a8e71", + ) + .unwrap(), + ) + .into_script(); + let conf_asset = Asset::from_commitment( + &Vec::::from_hex("0bdabc318c05dfc1f911bd7e4608ad75c75b3cc25b2fe98fa3966597ab9a0a473f") + .unwrap(), + ) + .unwrap(); + let conf_value = Value::from_commitment( + &Vec::::from_hex("08fb70255ab047990780c71fed7b874ca4ece6583ade26b37bf7d7f1c9284f4c84") + .unwrap(), + ) + .unwrap(); + let mut apo = SighashAllAPO { + outputs: vec![elements::TxOut::default(); 2], + in_asset: confidential::Asset::Explicit( + AssetId::from_hex("5a62ef74ac90662be6a115855853c1a9d4407d955d28446c67c1568e532e61e9") + .unwrap(), + ), + in_value: confidential::Value::Explicit(1000), + in_spk: tap_script.clone(), + version: 3, + locktime: 1_000_000, + sequence: 0xfffffffe, + }; + apo.outputs[0].asset = conf_asset; + apo.outputs[0].value = conf_value; + apo.outputs[0].script_pubkey = tap_script.clone(); + apo.outputs[1].asset = conf_asset; + apo.outputs[1].value = conf_value; + apo.outputs[1].script_pubkey = tap_script.clone(); + + let internal_pk = "02052ef9779ac3955ef438bbcde77411f31bf0e05fbe1b2b563fb5f0475c8a8e71"; + + let desc = Tr::::from_str_insane(&format!( + "eltr({},{})", + internal_pk, &apo, + )) + .unwrap(); + println!( + "desc addr: {}", + desc.address(None, &AddressParams::ELEMENTS) + ); + + let tap_script = desc.iter_scripts().next().unwrap().1; + println!("{}", tap_script.encode().to_hex()); + println!("{:?}", tap_script.encode()); +} diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 7d2866da..b501b96c 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -1022,7 +1022,8 @@ impl_from_str!( Ok(tr) => Ok(Descriptor::Tr(tr)), Err(_) => { // Try parsing with extensions - let tr = Tr::::from_str(s)?; + // descriptors are always parsed insane. This will improve once we use upstream ExtParams Api. + let tr = Tr::::from_str_insane(s)?; Ok(Descriptor::TrExt(tr)) } } diff --git a/src/descriptor/tr.rs b/src/descriptor/tr.rs index ac5c67af..ecbade37 100644 --- a/src/descriptor/tr.rs +++ b/src/descriptor/tr.rs @@ -1,7 +1,6 @@ // Tapscript use std::cmp::{self, max}; -use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::{fmt, hash}; @@ -417,7 +416,7 @@ impl_block_str!( fn parse_tr_script_spend(tree: &expression::Tree,) -> Result, Error> { match tree { expression::Tree { name, args } if !name.is_empty() && args.is_empty() => { - let script = Miniscript::::from_str(name)?; + let script = Miniscript::::from_str_insane(name)?; Ok(TapTree::Leaf(Arc::new(script))) } expression::Tree { name, args } if name.is_empty() && args.len() == 2 => { @@ -484,6 +483,20 @@ impl_from_str!( => Ext; Extension, type Err = Error;, fn from_str(s: &str) -> Result { + let res = Self::from_str_insane(s)?; + if res.iter_scripts().any(|(_, ms)| ms.sanity_check().is_err()) { + return Err(Error::BadDescriptor("Sanity check failed".to_string())); + } + Ok(res) + } +); + +#[rustfmt::skip] +impl_block_str!( + Tr, + => Ext; Extension, + /// Parse taproot descriptors without any sanity checks + pub fn from_str_insane(s: &str,) -> Result, Error> { let desc_str = verify_checksum(s)?; let top = parse_tr_tree(desc_str)?; Self::from_tree(&top) @@ -742,6 +755,7 @@ where mod tests { use super::*; use crate::{ForEachKey, NoExt}; + use core::str::FromStr; #[test] fn test_for_each() { diff --git a/src/extensions/introspect_ops.rs b/src/extensions/introspect_ops.rs index 2e11afa3..626b15d7 100644 --- a/src/extensions/introspect_ops.rs +++ b/src/extensions/introspect_ops.rs @@ -44,6 +44,9 @@ pub enum AssetExpr { /// Explicit asset at the given output index /// i INPSECTOUTPUTASSET Output(usize), + /// Explicit asset at the output index corresponding to the input index + /// INSPECTCURRENTINPUTINDEX INPSECTOUTPUTASSET + CurrOutputAsset, } /// Enum representing operations with transaction values. @@ -65,6 +68,10 @@ pub enum ValueExpr { /// Value(possibly confidential) at the given output index /// i INPSECTOUTPUTVALUE Output(usize), + /// Value in the corresponding output of the current input index + /// Required for sighash single emulation + /// INSPECTCURRENTINPUTINDEX INPSECTINPUTVALUE + CurrOutputValue, } /// Enum representing operations with transaction script pubkeys. @@ -89,6 +96,9 @@ pub enum SpkExpr { /// Explicit asset at the given output index /// i INPSECTOUTPUTSCRIPTPUBKEY Output(usize), + /// Output spk matching the current executing index. Required for sighash single emulation + /// INSPECTCURRENTINPUTINDEX INPSECTOUTPUTSCRIPTPUBKEY + CurrOutputSpk, } /// Miniscript Fragment containing arith expressions @@ -128,6 +138,7 @@ impl AssetExpr { AssetExpr::CurrInputAsset => 2, AssetExpr::Input(i) => script_num_size(*i) + 1, AssetExpr::Output(i) => script_num_size(*i) + 1, + AssetExpr::CurrOutputAsset => 2, } } @@ -142,6 +153,7 @@ impl AssetExpr { AssetExpr::CurrInputAsset => AssetExpr::CurrInputAsset, AssetExpr::Input(i) => AssetExpr::Input(*i), AssetExpr::Output(i) => AssetExpr::Output(*i), + AssetExpr::CurrOutputAsset => AssetExpr::CurrOutputAsset, }; Ok(res) } @@ -154,6 +166,7 @@ impl fmt::Display for AssetExpr { AssetExpr::CurrInputAsset => write!(f, "curr_inp_asset"), AssetExpr::Input(i) => write!(f, "inp_asset({})", i), AssetExpr::Output(i) => write!(f, "out_asset({})", i), + AssetExpr::CurrOutputAsset => write!(f, "curr_out_asset"), } } } @@ -165,6 +178,7 @@ impl fmt::Debug for AssetExpr { AssetExpr::CurrInputAsset => write!(f, "curr_inp_asset"), AssetExpr::Input(i) => write!(f, "inp_asset({:?})", i), AssetExpr::Output(i) => write!(f, "out_asset({:?})", i), + AssetExpr::CurrOutputAsset => write!(f, "curr_out_asset"), } } } @@ -184,6 +198,7 @@ impl AssetExpr { .map(AssetExpr::Input), ("out_asset", 1) => expression::terminal(&top.args[0], expression::parse_num::) .map(AssetExpr::Output), + ("curr_out_asset", 0) => Ok(AssetExpr::CurrOutputAsset), (asset, 0) => Ok(AssetExpr::Const(T::arg_from_str(asset, parent, pos)?)), _ => Err(Error::Unexpected(format!( "{}({} args) while parsing Extension", @@ -202,6 +217,7 @@ impl ValueExpr { ValueExpr::CurrInputValue => 2, ValueExpr::Input(i) => script_num_size(*i) + 1, ValueExpr::Output(i) => script_num_size(*i) + 1, + ValueExpr::CurrOutputValue => 2, } } @@ -216,6 +232,7 @@ impl ValueExpr { ValueExpr::CurrInputValue => ValueExpr::CurrInputValue, ValueExpr::Input(i) => ValueExpr::Input(*i), ValueExpr::Output(i) => ValueExpr::Output(*i), + ValueExpr::CurrOutputValue => ValueExpr::CurrOutputValue, }; Ok(res) } @@ -228,6 +245,7 @@ impl fmt::Display for ValueExpr { ValueExpr::CurrInputValue => write!(f, "curr_inp_value"), ValueExpr::Input(i) => write!(f, "inp_value({})", i), ValueExpr::Output(i) => write!(f, "out_value({})", i), + ValueExpr::CurrOutputValue => write!(f, "curr_out_value"), } } } @@ -239,6 +257,7 @@ impl fmt::Debug for ValueExpr { ValueExpr::CurrInputValue => write!(f, "curr_inp_value"), ValueExpr::Input(i) => write!(f, "inp_value({:?})", i), ValueExpr::Output(i) => write!(f, "out_value({:?})", i), + ValueExpr::CurrOutputValue => write!(f, "curr_out_value"), } } } @@ -258,6 +277,7 @@ impl ValueExpr { .map(ValueExpr::Input), ("out_value", 1) => expression::terminal(&top.args[0], expression::parse_num::) .map(ValueExpr::Output), + ("curr_out_value", 0) => Ok(ValueExpr::CurrOutputValue), (value, 0) => Ok(ValueExpr::Const(T::arg_from_str(value, parent, pos)?)), _ => Err(Error::Unexpected(format!( "{}({} args) while parsing Extension", @@ -276,6 +296,7 @@ impl SpkExpr { SpkExpr::CurrInputSpk => 2, SpkExpr::Input(i) => script_num_size(*i) + 1, SpkExpr::Output(i) => script_num_size(*i) + 1, + SpkExpr::CurrOutputSpk => 2, } } @@ -290,6 +311,7 @@ impl SpkExpr { SpkExpr::CurrInputSpk => SpkExpr::CurrInputSpk, SpkExpr::Input(i) => SpkExpr::Input(*i), SpkExpr::Output(i) => SpkExpr::Output(*i), + SpkExpr::CurrOutputSpk => SpkExpr::CurrOutputSpk, }; Ok(res) } @@ -302,6 +324,7 @@ impl fmt::Display for SpkExpr { SpkExpr::CurrInputSpk => write!(f, "curr_inp_spk"), SpkExpr::Input(i) => write!(f, "inp_spk({})", i), SpkExpr::Output(i) => write!(f, "out_spk({})", i), + SpkExpr::CurrOutputSpk => write!(f, "curr_out_spk"), } } } @@ -313,6 +336,7 @@ impl fmt::Debug for SpkExpr { SpkExpr::CurrInputSpk => write!(f, "curr_inp_spk"), SpkExpr::Input(i) => write!(f, "inp_spk({:?})", i), SpkExpr::Output(i) => write!(f, "out_spk({:?})", i), + SpkExpr::CurrOutputSpk => write!(f, "curr_out_spk"), } } } @@ -332,6 +356,7 @@ impl SpkExpr { .map(SpkExpr::Input), ("out_spk", 1) => expression::terminal(&top.args[0], expression::parse_num::) .map(SpkExpr::Output), + ("curr_out_spk", 0) => Ok(SpkExpr::CurrOutputSpk), (asset, 0) => Ok(SpkExpr::Const(T::arg_from_str(asset, parent, pos)?)), _ => Err(Error::Unexpected(format!( "{}({} args) while parsing Extension", @@ -653,6 +678,9 @@ impl AssetExpr { AssetExpr::CurrInputAsset => builder .push_opcode(OP_PUSHCURRENTINPUTINDEX) .push_opcode(OP_INSPECTINPUTASSET), + AssetExpr::CurrOutputAsset => builder + .push_opcode(OP_PUSHCURRENTINPUTINDEX) + .push_opcode(OP_INSPECTOUTPUTASSET), AssetExpr::Input(i) => builder .push_int(*i as i64) .push_opcode(OP_INSPECTINPUTASSET), @@ -679,6 +707,15 @@ impl AssetExpr { } Ok(env.spent_utxos()[env.idx()].asset) } + AssetExpr::CurrOutputAsset => { + if env.idx() >= env.tx().output.len() { + return Err(EvalError::OutputIndexOutOfBounds( + env.idx(), + env.tx().output.len(), + )); + } + Ok(env.tx().output[env.idx()].asset) + } AssetExpr::Input(i) => { if *i >= env.spent_utxos().len() { return Err(EvalError::UtxoIndexOutOfBounds(*i, env.spent_utxos().len())); @@ -704,6 +741,8 @@ impl AssetExpr { Some((AssetExpr::Const(CovExtArgs::Asset(asset)), e - 2)) } else if let Some(&[Tk::CurrInp, Tk::InpAsset]) = tks.get(e.checked_sub(2)?..e) { Some((AssetExpr::CurrInputAsset, e - 2)) + } else if let Some(&[Tk::CurrInp, Tk::OutAsset]) = tks.get(e.checked_sub(2)?..e) { + Some((AssetExpr::CurrOutputAsset, e - 2)) } else if let Some(&[Tk::Num(i), Tk::InpAsset]) = tks.get(e.checked_sub(2)?..e) { Some((AssetExpr::Input(i as usize), e - 2)) } else if let Some(&[Tk::Num(i), Tk::OutAsset]) = tks.get(e.checked_sub(2)?..e) { @@ -739,6 +778,9 @@ impl ValueExpr { ValueExpr::CurrInputValue => builder .push_opcode(OP_PUSHCURRENTINPUTINDEX) .push_opcode(OP_INSPECTINPUTVALUE), + ValueExpr::CurrOutputValue => builder + .push_opcode(OP_PUSHCURRENTINPUTINDEX) + .push_opcode(OP_INSPECTOUTPUTVALUE), ValueExpr::Input(i) => builder .push_int(*i as i64) .push_opcode(OP_INSPECTINPUTVALUE), @@ -765,6 +807,15 @@ impl ValueExpr { } Ok(env.spent_utxos()[env.idx()].value) } + ValueExpr::CurrOutputValue => { + if env.idx() >= env.tx().output.len() { + return Err(EvalError::OutputIndexOutOfBounds( + env.idx(), + env.tx().output.len(), + )); + } + Ok(env.tx().output[env.idx()].value) + } ValueExpr::Input(i) => { if *i >= env.spent_utxos().len() { return Err(EvalError::UtxoIndexOutOfBounds(*i, env.spent_utxos().len())); @@ -793,6 +844,8 @@ impl ValueExpr { Some((ValueExpr::Const(CovExtArgs::Value(value)), e - 2)) } else if let Some(&[Tk::CurrInp, Tk::InpValue]) = tks.get(e.checked_sub(2)?..e) { Some((ValueExpr::CurrInputValue, e - 2)) + } else if let Some(&[Tk::CurrInp, Tk::OutValue]) = tks.get(e.checked_sub(2)?..e) { + Some((ValueExpr::CurrOutputValue, e - 2)) } else if let Some(&[Tk::Num(i), Tk::InpValue]) = tks.get(e.checked_sub(2)?..e) { Some((ValueExpr::Input(i as usize), e - 2)) } else if let Some(&[Tk::Num(i), Tk::OutValue]) = tks.get(e.checked_sub(2)?..e) { @@ -818,6 +871,9 @@ impl SpkExpr { SpkExpr::CurrInputSpk => builder .push_opcode(OP_PUSHCURRENTINPUTINDEX) .push_opcode(OP_INSPECTINPUTSCRIPTPUBKEY), + SpkExpr::CurrOutputSpk => builder + .push_opcode(OP_PUSHCURRENTINPUTINDEX) + .push_opcode(OP_INSPECTOUTPUTSCRIPTPUBKEY), SpkExpr::Input(i) => builder .push_int(*i as i64) .push_opcode(OP_INSPECTINPUTSCRIPTPUBKEY), @@ -844,6 +900,15 @@ impl SpkExpr { } spk_to_components(&env.spent_utxos()[env.idx()].script_pubkey) } + SpkExpr::CurrOutputSpk => { + if env.idx() >= env.tx().output.len() { + return Err(EvalError::OutputIndexOutOfBounds( + env.idx(), + env.tx().output.len(), + )); + } + spk_to_components(&env.tx().output[env.idx()].script_pubkey) + } SpkExpr::Input(i) => { if *i >= env.spent_utxos().len() { return Err(EvalError::UtxoIndexOutOfBounds(*i, env.spent_utxos().len())); @@ -873,6 +938,8 @@ impl SpkExpr { Some((SpkExpr::Const(CovExtArgs::Script(Spk(script))), e - 2)) } else if let Some(&[Tk::CurrInp, Tk::InpSpk]) = tks.get(e.checked_sub(2)?..e) { Some((SpkExpr::CurrInputSpk, e - 2)) + } else if let Some(&[Tk::CurrInp, Tk::OutSpk]) = tks.get(e.checked_sub(2)?..e) { + Some((SpkExpr::CurrOutputSpk, e - 2)) } else if let Some(&[Tk::Num(i), Tk::InpSpk]) = tks.get(e.checked_sub(2)?..e) { Some((SpkExpr::Input(i as usize), e - 2)) } else if let Some(&[Tk::Num(i), Tk::OutSpk]) = tks.get(e.checked_sub(2)?..e) { @@ -1157,6 +1224,7 @@ mod tests { _test_parse("is_exp_asset(out_asset(9))"); _test_parse("asset_eq(ConfAst,ExpAst)"); _test_parse("asset_eq(curr_inp_asset,out_asset(1))"); + _test_parse("asset_eq(curr_inp_asset,curr_out_asset)"); _test_parse("asset_eq(inp_asset(3),out_asset(1))"); // same tests for values @@ -1168,11 +1236,13 @@ mod tests { _test_parse("value_eq(ConfVal,ExpVal)"); _test_parse("value_eq(curr_inp_value,out_value(1))"); _test_parse("value_eq(inp_value(3),out_value(1))"); + _test_parse("value_eq(curr_out_value,out_value(1))"); // same tests for spks _test_parse("spk_eq(V0Spk,out_spk(1))"); _test_parse("spk_eq(V1Spk,inp_spk(1))"); _test_parse("spk_eq(curr_inp_spk,out_spk(1))"); + _test_parse("spk_eq(curr_out_spk,out_spk(1))"); _test_parse("spk_eq(inp_spk(3),out_spk(1))"); // Testing the current input index @@ -1184,6 +1254,10 @@ mod tests { "and_v(v:pk(K),and_v(v:is_exp_value(out_value(1)),is_exp_asset(out_asset(1))))", ); _test_parse("and_v(v:pk(K),and_v(v:value_eq(ConfVal,ConfVal),spk_eq(V1Spk,V1Spk)))"); + _test_parse("and_v(v:pk(K),and_v(v:value_eq(ConfVal,ConfVal),spk_eq(V1Spk,curr_out_spk)))"); + _test_parse( + "and_v(v:pk(K),and_v(v:value_eq(curr_out_value,ConfVal),spk_eq(V1Spk,curr_out_spk)))", + ); _test_parse("and_v(v:pk(K),and_v(v:value_eq(ConfVal,ConfVal),and_v(v:spk_eq(V1Spk,V1Spk),curr_idx_eq(1))))"); } diff --git a/tests/test_introspect.rs b/tests/test_introspect.rs index 31290fdb..e7c7e3ed 100644 --- a/tests/test_introspect.rs +++ b/tests/test_introspect.rs @@ -204,6 +204,7 @@ fn test_descs(cl: &ElementsD, testdata: &mut TestData) { test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_asset(out_asset(1))))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),asset_eq(curr_inp_asset,inp_asset(0))))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),asset_eq(curr_inp_asset,out_asset(0))))"); + test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),asset_eq(curr_out_asset,out_asset(0))))"); // same tests for values test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_value(exp_value)))"); @@ -211,6 +212,7 @@ fn test_descs(cl: &ElementsD, testdata: &mut TestData) { test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_value(inp_value(0))))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),is_exp_value(out_value(1))))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),value_eq(curr_inp_value,inp_value(0))))"); + test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),value_eq(curr_out_value,out_value(0))))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),value_eq(out_value(0),out_value(0))))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),value_eq(out_value(1),out_value(1))))"); @@ -218,6 +220,7 @@ fn test_descs(cl: &ElementsD, testdata: &mut TestData) { test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),spk_eq(out_spk(1),out_spk(1))))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),spk_eq(spk_v1,spk_v1)))"); test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),spk_eq(curr_inp_spk,inp_spk(0))))"); + test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),spk_eq(curr_out_spk,out_spk(0))))"); // Testing the current input index test_desc_satisfy(cl, testdata,"tr(X!,and_v(v:pk(X2),curr_idx_eq(0)))");