diff --git a/Cargo.lock b/Cargo.lock index 61ca7bb..2e9c912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,7 @@ dependencies = [ "serde_json", "serde_yaml", "simplicity-lang", + "thiserror", ] [[package]] @@ -489,18 +490,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -855,9 +856,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -884,6 +885,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "time" version = "0.1.42" @@ -964,7 +985,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -986,7 +1007,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index aab1f76..4601593 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ hex = "0.3.2" elements = { version = "0.25.2", features = [ "serde", "base64" ] } simplicity = { package = "simplicity-lang", version = "0.5.0", features = [ "base64", "serde" ] } +thiserror = "2.0.17" [lints.clippy] # Exclude lints we don't think are valuable. diff --git a/src/bin/hal-simplicity/cmd/address.rs b/src/bin/hal-simplicity/cmd/address.rs index accc978..6d12109 100644 --- a/src/bin/hal-simplicity/cmd/address.rs +++ b/src/bin/hal-simplicity/cmd/address.rs @@ -2,11 +2,39 @@ use clap; use elements::bitcoin::{secp256k1, PublicKey}; use elements::hashes::Hash; use elements::{Address, WPubkeyHash, WScriptHash}; +use hal_simplicity::address::{AddressInfo, Addresses}; use crate::cmd; use crate::Network; +#[derive(Debug, thiserror::Error)] +pub enum AddressError { + #[error("invalid blinder hex: {0}")] + BlinderHex(hex::FromHexError), + + #[error("invalid blinder: {0}")] + BlinderInvalid(secp256k1::Error), + + #[error("invalid pubkey: {0}")] + PubkeyInvalid(elements::bitcoin::key::ParsePublicKeyError), + + #[error("invalid script hex: {0}")] + ScriptHex(hex::FromHexError), + + #[error("can't create addresses without a pubkey")] + MissingInput, + + #[error("invalid address format: {0}")] + AddressParse(elements::address::AddressError), + + #[error("no address provided")] + NoAddressProvided, + + #[error("addresses always have params")] + AddressesAlwaysHaveParams, +} + pub fn subcommand<'a>() -> clap::App<'a, 'a> { cmd::subcommand_group("address", "work with addresses") .subcommand(cmd_create()) @@ -33,24 +61,36 @@ fn cmd_create<'a>() -> clap::App<'a, 'a> { fn exec_create<'a>(matches: &clap::ArgMatches<'a>) { let network = cmd::network(matches); - let blinder = matches.value_of("blinder").map(|b| { - let bytes = hex::decode(b).expect("invaid blinder hex"); - secp256k1::PublicKey::from_slice(&bytes).expect("invalid blinder") - }); + match exec_create_inner(matches, network) { + Ok(addresses) => cmd::print_output(matches, &addresses), + Err(e) => panic!("{}", e), + } +} + +fn exec_create_inner( + matches: &clap::ArgMatches<'_>, + network: Network, +) -> Result { + let blinder = matches + .value_of("blinder") + .map(|b| { + let bytes = hex::decode(b).map_err(AddressError::BlinderHex)?; + secp256k1::PublicKey::from_slice(&bytes).map_err(AddressError::BlinderInvalid) + }) + .transpose()?; let created = if let Some(pubkey_hex) = matches.value_of("pubkey") { - let pubkey: PublicKey = pubkey_hex.parse().expect("invalid pubkey"); - hal_simplicity::address::Addresses::from_pubkey(&pubkey, blinder, network) + let pubkey: PublicKey = pubkey_hex.parse().map_err(AddressError::PubkeyInvalid)?; + Addresses::from_pubkey(&pubkey, blinder, network) } else if let Some(script_hex) = matches.value_of("script") { - let script_bytes = hex::decode(script_hex).expect("invalid script hex"); + let script_bytes = hex::decode(script_hex).map_err(AddressError::ScriptHex)?; let script = script_bytes.into(); - - hal_simplicity::address::Addresses::from_script(&script, blinder, network) + Addresses::from_script(&script, blinder, network) } else { - panic!("Can't create addresses without a pubkey"); + return Err(AddressError::MissingInput); }; - cmd::print_output(matches, &created) + Ok(created) } fn cmd_inspect<'a>() -> clap::App<'a, 'a> { @@ -59,12 +99,20 @@ fn cmd_inspect<'a>() -> clap::App<'a, 'a> { } fn exec_inspect<'a>(matches: &clap::ArgMatches<'a>) { - let address_str = matches.value_of("address").expect("no address provided"); - let address: Address = address_str.parse().expect("invalid address format"); + match create_inspect_inner(matches) { + Ok(info) => cmd::print_output(matches, &info), + Err(e) => panic!("{}", e), + } +} + +fn create_inspect_inner(matches: &clap::ArgMatches<'_>) -> Result { + let address_str = matches.value_of("address").ok_or(AddressError::NoAddressProvided)?; + let address: Address = address_str.parse().map_err(AddressError::AddressParse)?; let script_pk = address.script_pubkey(); let mut info = hal_simplicity::address::AddressInfo { - network: Network::from_params(address.params).expect("addresses always have params"), + network: Network::from_params(address.params) + .ok_or(AddressError::AddressesAlwaysHaveParams)?, script_pub_key: hal::tx::OutputScriptInfo { hex: Some(script_pk.to_bytes().into()), asm: Some(script_pk.asm()), @@ -124,5 +172,5 @@ fn exec_inspect<'a>(matches: &clap::ArgMatches<'a>) { } } - cmd::print_output(matches, &info) + Ok(info) } diff --git a/src/bin/hal-simplicity/cmd/block.rs b/src/bin/hal-simplicity/cmd/block.rs index e5d39f3..7f4fe53 100644 --- a/src/bin/hal-simplicity/cmd/block.rs +++ b/src/bin/hal-simplicity/cmd/block.rs @@ -8,6 +8,36 @@ use crate::cmd::tx::create_transaction; use hal_simplicity::block::{BlockHeaderInfo, BlockInfo, ParamsInfo, ParamsType}; use log::warn; +#[derive(Debug, thiserror::Error)] +pub enum BlockError { + #[error("can't provide transactions both in JSON and raw.")] + ConflictingTransactions, + + #[error("no transactions provided.")] + NoTransactions, + + #[error("failed to deserialize transaction: {0}")] + TransactionDeserialize(super::tx::TxError), + + #[error("invalid raw transaction: {0}")] + InvalidRawTransaction(elements::encode::Error), + + #[error("invalid block format: {0}")] + BlockDeserialize(elements::encode::Error), + + #[error("could not decode raw block hex: {0}")] + CouldNotDecodeRawBlockHex(hex::FromHexError), + + #[error("invalid json JSON input: {0}")] + InvalidJsonInput(serde_json::Error), + + #[error("{field} missing in {context}")] + MissingField { + field: String, + context: String, + }, +} + pub fn subcommand<'a>() -> clap::App<'a, 'a> { cmd::subcommand_group("block", "manipulate blocks") .subcommand(cmd_create()) @@ -31,40 +61,72 @@ fn cmd_create<'a>() -> clap::App<'a, 'a> { ]) } -fn create_params(info: ParamsInfo) -> dynafed::Params { +fn create_params(info: ParamsInfo) -> Result { match info.params_type { - ParamsType::Null => dynafed::Params::Null, - ParamsType::Compact => dynafed::Params::Compact { + ParamsType::Null => Ok(dynafed::Params::Null), + ParamsType::Compact => Ok(dynafed::Params::Compact { signblockscript: info .signblockscript - .expect("signblockscript missing in compact params") + .ok_or_else(|| BlockError::MissingField { + field: "signblockscript".to_string(), + context: "compact params".to_string(), + })? .0 .into(), - signblock_witness_limit: info - .signblock_witness_limit - .expect("signblock_witness_limit missing in compact params"), - elided_root: info.elided_root.expect("elided_root missing in compact params"), - }, - ParamsType::Full => dynafed::Params::Full(dynafed::FullParams::new( - info.signblockscript.expect("signblockscript missing in full params").0.into(), - info.signblock_witness_limit.expect("signblock_witness_limit missing in full params"), - info.fedpeg_program.expect("fedpeg_program missing in full params").0.into(), - info.fedpeg_script.expect("fedpeg_script missing in full params").0, + signblock_witness_limit: info.signblock_witness_limit.ok_or_else(|| { + BlockError::MissingField { + field: "signblock_witness_limit".to_string(), + context: "compact params".to_string(), + } + })?, + elided_root: info.elided_root.ok_or_else(|| BlockError::MissingField { + field: "elided_root".to_string(), + context: "compact params".to_string(), + })?, + }), + ParamsType::Full => Ok(dynafed::Params::Full(dynafed::FullParams::new( + info.signblockscript + .ok_or_else(|| BlockError::MissingField { + field: "signblockscript".to_string(), + context: "full params".to_string(), + })? + .0 + .into(), + info.signblock_witness_limit.ok_or_else(|| BlockError::MissingField { + field: "signblock_witness_limit".to_string(), + context: "full params".to_string(), + })?, + info.fedpeg_program + .ok_or_else(|| BlockError::MissingField { + field: "fedpeg_program".to_string(), + context: "full params".to_string(), + })? + .0 + .into(), + info.fedpeg_script + .ok_or_else(|| BlockError::MissingField { + field: "fedpeg_script".to_string(), + context: "full params".to_string(), + })? + .0, info.extension_space - .expect("extension space missing in full params") + .ok_or_else(|| BlockError::MissingField { + field: "extension space".to_string(), + context: "full params".to_string(), + })? .into_iter() .map(|b| b.0) .collect(), - )), + ))), } } -fn create_block_header(info: BlockHeaderInfo) -> BlockHeader { +fn create_block_header(info: BlockHeaderInfo) -> Result { if info.block_hash.is_some() { warn!("Field \"block_hash\" is ignored."); } - BlockHeader { + Ok(BlockHeader { version: info.version, prev_blockhash: info.previous_block_hash, merkle_root: info.merkle_root, @@ -72,45 +134,83 @@ fn create_block_header(info: BlockHeaderInfo) -> BlockHeader { height: info.height, ext: if info.dynafed { BlockExtData::Dynafed { - current: create_params(info.dynafed_current.expect("missing current params")), - proposed: create_params(info.dynafed_proposed.expect("missing proposed params")), + current: create_params(info.dynafed_current.ok_or_else(|| { + BlockError::MissingField { + field: "current".to_string(), + context: "dynafed params".to_string(), + } + })?)?, + proposed: create_params(info.dynafed_proposed.ok_or_else(|| { + BlockError::MissingField { + field: "proposed".to_string(), + context: "dynafed params".to_string(), + } + })?)?, signblock_witness: info .dynafed_witness - .expect("missing dynafed witness") + .ok_or_else(|| BlockError::MissingField { + field: "witness".to_string(), + context: "dynafed params".to_string(), + })? .into_iter() .map(|b| b.0) .collect(), } } else { BlockExtData::Proof { - challenge: info.legacy_challenge.expect("missing challenge").0.into(), - solution: info.legacy_solution.expect("missing solution").0.into(), + challenge: info + .legacy_challenge + .ok_or_else(|| BlockError::MissingField { + field: "challenge".to_string(), + context: "proof params".to_string(), + })? + .0 + .into(), + solution: info + .legacy_solution + .ok_or_else(|| BlockError::MissingField { + field: "solution".to_string(), + context: "proof params".to_string(), + })? + .0 + .into(), } }, - } + }) } fn exec_create<'a>(matches: &clap::ArgMatches<'a>) { let info = serde_json::from_str::(&cmd::arg_or_stdin(matches, "block-info")) - .expect("invaid json JSON input"); + .map_err(BlockError::InvalidJsonInput) + .unwrap_or_else(|e| panic!("{}", e)); if info.txids.is_some() { warn!("Field \"txids\" is ignored."); } - let block = Block { - header: create_block_header(info.header), - txdata: match (info.transactions, info.raw_transactions) { - (Some(_), Some(_)) => panic!("Can't provide transactions both in JSON and raw."), - (None, None) => panic!("No transactions provided."), - (Some(infos), None) => infos.into_iter().map(create_transaction).collect(), + let create_block = || -> Result { + let header = create_block_header(info.header)?; + let txdata = match (info.transactions, info.raw_transactions) { + (Some(_), Some(_)) => return Err(BlockError::ConflictingTransactions), + (None, None) => return Err(BlockError::NoTransactions), + (Some(infos), None) => infos + .into_iter() + .map(create_transaction) + .collect::, _>>() + .map_err(BlockError::TransactionDeserialize)?, (None, Some(raws)) => raws .into_iter() - .map(|r| deserialize(&r.0).expect("invalid raw transaction")) - .collect(), - }, + .map(|r| deserialize(&r.0).map_err(BlockError::InvalidRawTransaction)) + .collect::, _>>()?, + }; + Ok(Block { + header, + txdata, + }) }; + let block = create_block().unwrap_or_else(|e| panic!("{}", e)); + let block_bytes = serialize(&block); if matches.is_present("raw-stdout") { ::std::io::stdout().write_all(&block_bytes).unwrap(); @@ -129,10 +229,14 @@ fn cmd_decode<'a>() -> clap::App<'a, 'a> { fn exec_decode<'a>(matches: &clap::ArgMatches<'a>) { let hex_tx = cmd::arg_or_stdin(matches, "raw-block"); - let raw_tx = hex::decode(hex_tx.as_ref()).expect("could not decode raw block hex"); + let raw_tx = hex::decode(hex_tx.as_ref()) + .map_err(BlockError::CouldNotDecodeRawBlockHex) + .unwrap_or_else(|e| panic!("{}", e)); if matches.is_present("txids") { - let block: Block = deserialize(&raw_tx).expect("invalid block format"); + let block: Block = deserialize(&raw_tx) + .map_err(BlockError::BlockDeserialize) + .unwrap_or_else(|e| panic!("{}", e)); let info = BlockInfo { header: crate::GetInfo::get_info(&block.header, cmd::network(matches)), txids: Some(block.txdata.iter().map(|t| t.txid()).collect()), @@ -144,7 +248,9 @@ fn exec_decode<'a>(matches: &clap::ArgMatches<'a>) { let header: BlockHeader = match deserialize(&raw_tx) { Ok(header) => header, Err(_) => { - let block: Block = deserialize(&raw_tx).expect("invalid block format"); + let block: Block = deserialize(&raw_tx) + .map_err(BlockError::BlockDeserialize) + .unwrap_or_else(|e| panic!("{}", e)); block.header } }; diff --git a/src/bin/hal-simplicity/cmd/simplicity/info.rs b/src/bin/hal-simplicity/cmd/simplicity/info.rs index 0f82bcf..1738986 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/info.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/info.rs @@ -3,7 +3,7 @@ use crate::cmd; -use super::{Error, ErrorExt as _}; +use super::Error; use hal_simplicity::hal_simplicity::{elements_address, Program}; use hal_simplicity::simplicity::{jet, Amr, Cmr, Ihr}; @@ -11,6 +11,15 @@ use simplicity::hex::parse::FromHex as _; use serde::Serialize; +#[derive(Debug, thiserror::Error)] +pub enum SimplicityInfoError { + #[error("invalid program: {0}")] + ProgramParse(simplicity::ParseError), + + #[error("invalid state: {0}")] + StateParse(elements::hashes::hex::HexToArrayError), +} + #[derive(Serialize)] struct RedeemInfo { redeem_base64: String, @@ -60,7 +69,12 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { match exec_inner(program, witness, state) { Ok(info) => cmd::print_output(matches, &info), - Err(e) => cmd::print_output(matches, &e), + Err(e) => cmd::print_output( + matches, + &Error { + error: format!("{}", e), + }, + ), } } @@ -68,12 +82,12 @@ fn exec_inner( program: &str, witness: Option<&str>, state: Option<&str>, -) -> Result { +) -> 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. - let program = - Program::::from_str(program, witness).result_context("parsing program")?; + let program = Program::::from_str(program, witness) + .map_err(SimplicityInfoError::ProgramParse)?; let redeem_info = program.redeem_node().map(|node| { let disp = node.display(); @@ -86,10 +100,8 @@ fn exec_inner( x // binding needed for truly stupid borrowck reasons }); - let state = state - .map(<[u8; 32]>::from_hex) - .transpose() - .result_context("parsing 32-byte state commitment as hex")?; + let state = + state.map(<[u8; 32]>::from_hex).transpose().map_err(SimplicityInfoError::StateParse)?; Ok(ProgramInfo { jets: "core", diff --git a/src/bin/hal-simplicity/cmd/simplicity/mod.rs b/src/bin/hal-simplicity/cmd/simplicity/mod.rs index f4d373a..a64ebb8 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/mod.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/mod.rs @@ -15,21 +15,31 @@ use serde::Serialize; #[derive(Serialize)] struct Error { - context: &'static str, error: String, } -trait ErrorExt { - fn result_context(self, context: &'static str) -> Result; -} +#[derive(Debug, thiserror::Error)] +pub enum ParseElementsUtxoError { + #[error("invalid format: expected ::")] + InvalidFormat, -impl ErrorExt for Result { - fn result_context(self, context: &'static str) -> Result { - self.map_err(|e| Error { - context, - error: e.to_string(), - }) - } + #[error("invalid scriptPubKey hex: {0}")] + ScriptPubKeyParsing(elements::hex::Error), + + #[error("invalid asset hex: {0}")] + AssetHexParsing(elements::hashes::hex::HexToArrayError), + + #[error("invalid asset commitment hex: {0}")] + AssetCommitmentHexParsing(elements::hex::Error), + + #[error("invalid asset commitment: {0}")] + AssetCommitmentDecoding(elements::encode::Error), + + #[error("invalid value commitment hex: {0}")] + ValueCommitmentHexParsing(elements::hex::Error), + + #[error("invalid value commitment: {0}")] + ValueCommitmentDecoding(elements::encode::Error), } pub fn subcommand<'a>() -> clap::App<'a, 'a> { @@ -48,29 +58,27 @@ pub fn execute<'a>(matches: &clap::ArgMatches<'a>) { }; } -fn parse_elements_utxo(s: &str) -> Result { +fn parse_elements_utxo(s: &str) -> Result { let parts: Vec<&str> = s.split(':').collect(); if parts.len() != 3 { - return Err(Error { - context: "parsing input UTXO", - error: "expected format ::".to_string(), - }); + return Err(ParseElementsUtxoError::InvalidFormat); } // Parse scriptPubKey let script_pubkey: elements::Script = - parts[0].parse().result_context("parsing scriptPubKey hex")?; + parts[0].parse().map_err(ParseElementsUtxoError::ScriptPubKeyParsing)?; // Parse asset - try as explicit AssetId first, then as confidential commitment let asset = if parts[1].len() == 64 { // 32 bytes = explicit AssetId - let asset_id: elements::AssetId = parts[1].parse().result_context("parsing asset hex")?; + let asset_id: elements::AssetId = + parts[1].parse().map_err(ParseElementsUtxoError::AssetHexParsing)?; confidential::Asset::Explicit(asset_id) } else { // Parse anything except 32 bytes as a confidential commitment (which must be 33 bytes) let commitment_bytes = - Vec::from_hex(parts[1]).result_context("parsing asset commitment hex")?; + Vec::from_hex(parts[1]).map_err(ParseElementsUtxoError::AssetCommitmentHexParsing)?; elements::confidential::Asset::from_commitment(&commitment_bytes) - .result_context("decoding asset commitment")? + .map_err(ParseElementsUtxoError::AssetCommitmentDecoding)? }; // Parse value - try as BTC decimal first, then as confidential commitment @@ -80,9 +88,9 @@ fn parse_elements_utxo(s: &str) -> Result { } else { // 33 bytes = confidential commitment let commitment_bytes = - Vec::from_hex(parts[2]).result_context("parsing value commitment hex")?; + Vec::from_hex(parts[2]).map_err(ParseElementsUtxoError::ValueCommitmentHexParsing)?; elements::confidential::Value::from_commitment(&commitment_bytes) - .result_context("decoding value commitment")? + .map_err(ParseElementsUtxoError::ValueCommitmentDecoding)? }; Ok(ElementsUtxo { diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/create.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/create.rs index 2e17649..6191ba9 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/create.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/create.rs @@ -1,9 +1,10 @@ // Copyright 2025 Andrew Poelstra // SPDX-License-Identifier: CC0-1.0 -use super::super::{Error, ErrorExt as _}; +use super::super::Error; use super::UpdatedPset; use crate::cmd; +use crate::cmd::simplicity::pset::PsetError; use elements::confidential; use elements::pset::PartiallySignedTransaction; @@ -12,6 +13,27 @@ use serde::Deserialize; use std::collections::HashMap; +#[derive(Debug, thiserror::Error)] +pub enum PsetCreateError { + #[error(transparent)] + SharedError(#[from] PsetError), + + #[error("invalid inputs JSON: {0}")] + InputsJsonParse(serde_json::Error), + + #[error("invalid outputs JSON: {0}")] + OutputsJsonParse(serde_json::Error), + + #[error("invalid amount: {0}")] + AmountParse(elements::bitcoin::amount::ParseAmountError), + + #[error("invalid address: {0}")] + AddressParse(elements::address::AddressError), + + #[error("confidential addresses are not yet supported")] + ConfidentialAddressNotSupported, +} + #[derive(Deserialize)] struct InputSpec { txid: Txid, @@ -41,7 +63,7 @@ enum OutputSpec { } impl OutputSpec { - fn flatten(self) -> Box>> { + fn flatten(self) -> Box>> { match self { Self::Map(map) => Box::new(map.into_iter().map(|(address, amount)| { // Use liquid bitcoin asset as default for map format @@ -56,7 +78,7 @@ impl OutputSpec { address, asset: default_asset, amount: elements::bitcoin::Amount::from_btc(amount) - .result_context("parsing amount")?, + .map_err(PsetCreateError::AmountParse)?, }) })), Self::Explicit { @@ -95,18 +117,23 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { match exec_inner(inputs_json, outputs_json) { Ok(info) => cmd::print_output(matches, &info), - Err(e) => cmd::print_output(matches, &e), + Err(e) => cmd::print_output( + matches, + &Error { + error: format!("{}", e), + }, + ), } } -fn exec_inner(inputs_json: &str, outputs_json: &str) -> Result { +fn exec_inner(inputs_json: &str, outputs_json: &str) -> Result { // Parse inputs JSON let input_specs: Vec = - serde_json::from_str(inputs_json).result_context("parsing inputs JSON")?; + serde_json::from_str(inputs_json).map_err(PsetCreateError::InputsJsonParse)?; // Parse outputs JSON - support both array and map formats let output_specs: Vec = - serde_json::from_str(outputs_json).result_context("parsing outputs JSON")?; + serde_json::from_str(outputs_json).map_err(PsetCreateError::OutputsJsonParse)?; // Create transaction inputs let mut inputs = Vec::new(); @@ -132,10 +159,9 @@ fn exec_inner(inputs_json: &str, outputs_json: &str) -> Result elements::Script::new(), x => { - let addr = x.parse::
().result_context("parsing address")?; + let addr = x.parse::
().map_err(PsetCreateError::AddressParse)?; if addr.is_blinded() { - return Err("confidential addresses are not yet supported") - .result_context("output address"); + return Err(PsetCreateError::ConfidentialAddressNotSupported); } addr.script_pubkey() } diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/extract.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/extract.rs index 225767a..3b562b3 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/extract.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/extract.rs @@ -3,8 +3,20 @@ use elements::encode::serialize_hex; -use super::super::{Error, ErrorExt as _}; -use crate::cmd; +use super::super::Error; +use crate::cmd::{self, simplicity::pset::PsetError}; + +#[derive(Debug, thiserror::Error)] +pub enum PsetExtractError { + #[error(transparent)] + SharedError(#[from] PsetError), + + #[error("invalid PSET: {0}")] + PsetDecode(elements::pset::ParseError), + + #[error("ailed to extract transaction: {0}")] + TransactionExtract(elements::pset::Error), +} pub fn cmd<'a>() -> clap::App<'a, 'a> { cmd::subcommand("extract", "extract a raw transaction from a completed PSET") @@ -16,14 +28,19 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { let pset_b64 = matches.value_of("pset").expect("tx mandatory"); match exec_inner(pset_b64) { Ok(info) => cmd::print_output(matches, &info), - Err(e) => cmd::print_output(matches, &e), + Err(e) => cmd::print_output( + matches, + &Error { + error: format!("{}", e), + }, + ), } } -fn exec_inner(pset_b64: &str) -> Result { +fn exec_inner(pset_b64: &str) -> Result { let pset: elements::pset::PartiallySignedTransaction = - pset_b64.parse().result_context("decoding PSET")?; + pset_b64.parse().map_err(PsetExtractError::PsetDecode)?; - let tx = pset.extract_tx().result_context("extracting transaction")?; + let tx = pset.extract_tx().map_err(PsetExtractError::TransactionExtract)?; Ok(serialize_hex(&tx)) } diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs index 53033c2..ffe1b8f 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs @@ -2,13 +2,35 @@ // SPDX-License-Identifier: CC0-1.0 use crate::cmd; +use crate::cmd::simplicity::pset::PsetError; use hal_simplicity::hal_simplicity::Program; use hal_simplicity::simplicity::jet; -use super::super::{Error, ErrorExt as _}; +use super::super::Error; use super::UpdatedPset; +#[derive(Debug, thiserror::Error)] +pub enum PsetFinalizeError { + #[error(transparent)] + SharedError(#[from] PsetError), + + #[error("invalid PSET: {0}")] + PsetDecode(elements::pset::ParseError), + + #[error("invalid input index: {0}")] + InputIndexParse(std::num::ParseIntError), + + #[error("invalid program: {0}")] + ProgramParse(simplicity::ParseError), + + #[error("program does not have a redeem node")] + NoRedeemNode, + + #[error("failed to prune program: {0}")] + ProgramPrune(simplicity::bit_machine::ExecutionError), +} + pub fn cmd<'a>() -> clap::App<'a, 'a> { cmd::subcommand("finalize", "Attach a Simplicity program and witness to a PSET input") .args(&cmd::opts_networks()) @@ -39,7 +61,12 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { match exec_inner(pset_b64, input_idx, program, witness, genesis_hash) { Ok(info) => cmd::print_output(matches, &info), - Err(e) => cmd::print_output(matches, &e), + Err(e) => cmd::print_output( + matches, + &Error { + error: format!("{}", e), + }, + ), } } @@ -50,15 +77,15 @@ fn exec_inner( program: &str, witness: &str, genesis_hash: Option<&str>, -) -> Result { +) -> Result { // 1. Parse everything. let mut pset: elements::pset::PartiallySignedTransaction = - pset_b64.parse().result_context("decoding PSET")?; - let input_idx: u32 = input_idx.parse().result_context("parsing input-idx")?; + pset_b64.parse().map_err(PsetFinalizeError::PsetDecode)?; + let input_idx: u32 = input_idx.parse().map_err(PsetFinalizeError::InputIndexParse)?; let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems let program = Program::::from_str(program, Some(witness)) - .result_context("parsing program")?; + .map_err(PsetFinalizeError::ProgramParse)?; // 2. Extract transaction environment. let (tx_env, control_block, tap_leaf) = @@ -66,8 +93,8 @@ fn exec_inner( let cb_serialized = control_block.serialize(); // 3. Prune program. - let redeem_node = program.redeem_node().expect("populated"); - let pruned = redeem_node.prune(&tx_env).result_context("pruning program")?; + let redeem_node = program.redeem_node().ok_or(PsetFinalizeError::NoRedeemNode)?; + let pruned = redeem_node.prune(&tx_env).map_err(PsetFinalizeError::ProgramPrune)?; let (prog, witness) = pruned.to_vec_with_witness(); // If `execution_environment` above succeeded we are guaranteed that this index is in bounds. diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs index 16d8a21..a39aa4f 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs @@ -9,7 +9,6 @@ mod update_input; use std::sync::Arc; -use super::{Error, ErrorExt as _}; use crate::cmd; use elements::hashes::Hash as _; @@ -21,6 +20,29 @@ use hal_simplicity::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; use hal_simplicity::simplicity::Cmr; use serde::Serialize; +#[derive(Debug, thiserror::Error)] +pub enum PsetError { + #[error("input index {index} out-of-range for PSET with {total} inputs")] + InputIndexOutOfRange { + index: usize, + total: usize, + }, + + #[error("failed to parse genesis hash: {0}")] + GenesisHashParse(elements::hashes::hex::HexToArrayError), + + #[error("could not find Simplicity leaf in PSET taptree with CMR {cmr})")] + MissingSimplicityLeaf { + cmr: String, + }, + + #[error("failed to extract transaction from PSET: {0}")] + PsetExtract(elements::pset::Error), + + #[error("witness_utxo field not populated for input {0}")] + MissingWitnessUtxo(usize), +} + #[derive(Serialize)] struct UpdatedPset { pset: String, @@ -52,19 +74,16 @@ fn execution_environment( input_idx: usize, cmr: Cmr, genesis_hash: Option<&str>, -) -> Result<(ElementsEnv>, ControlBlock, Script), Error> { +) -> Result<(ElementsEnv>, ControlBlock, Script), PsetError> { let n_inputs = pset.n_inputs(); - let input = pset - .inputs() - .get(input_idx) - .ok_or_else(|| { - format!("index {} out-of-range for PSET with {} inputs", input_idx, n_inputs) - }) - .result_context("parsing input index")?; + let input = pset.inputs().get(input_idx).ok_or(PsetError::InputIndexOutOfRange { + index: input_idx, + total: n_inputs, + })?; // Default to Liquid Testnet genesis block let genesis_hash = match genesis_hash { - Some(s) => s.parse().result_context("parsing genesis hash")?, + Some(s) => s.parse().map_err(PsetError::GenesisHashParse)?, None => elements::BlockHash::from_byte_array([ // copied out of simplicity-webide source 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, @@ -82,14 +101,15 @@ fn execution_environment( } } let (control_block, tap_leaf) = match control_block_leaf { - Some((cb, leaf)) => (cb, leaf), - None => { - return Err(format!("could not find Simplicity leaf in PSET taptree with CMR {}; did you forget to run 'simplicity pset update-input'?", cmr)) - .result_context("PSET tap_scripts field") - } + Some((cb, leaf)) => (cb, leaf), + None => { + return Err(PsetError::MissingSimplicityLeaf { + cmr: cmr.to_string(), + }); + } }; - let tx = pset.extract_tx().result_context("extracting transaction from PSET")?; + let tx = pset.extract_tx().map_err(PsetError::PsetExtract)?; let tx = Arc::new(tx); let input_utxos = pset @@ -102,10 +122,9 @@ fn execution_environment( asset: utxo.asset, value: utxo.value, }), - None => Err(format!("witness_utxo field not populated for input {n}")), + None => Err(PsetError::MissingWitnessUtxo(n)), }) - .collect::, _>>() - .result_context("extracting input UTXO information")?; + .collect::, _>>()?; let tx_env = ElementsEnv::new( tx, diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/run.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/run.rs index 4886ef6..64d1672 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/run.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/run.rs @@ -2,13 +2,35 @@ // SPDX-License-Identifier: CC0-1.0 use crate::cmd; +use crate::cmd::simplicity::pset::PsetError; use hal_simplicity::hal_simplicity::Program; use hal_simplicity::simplicity::bit_machine::{BitMachine, ExecTracker}; use hal_simplicity::simplicity::jet; use hal_simplicity::simplicity::{Cmr, Ihr}; -use super::super::{Error, ErrorExt as _}; +use super::super::Error; + +#[derive(Debug, thiserror::Error)] +pub enum PsetRunError { + #[error(transparent)] + SharedError(#[from] PsetError), + + #[error("invalid PSET: {0}")] + PsetDecode(elements::pset::ParseError), + + #[error("invalid input index: {0}")] + InputIndexParse(std::num::ParseIntError), + + #[error("invalid program: {0}")] + ProgramParse(simplicity::ParseError), + + #[error("program does not have a redeem node")] + NoRedeemNode, + + #[error("failed to construct bit machine: {0}")] + BitMachineConstruction(simplicity::bit_machine::LimitError), +} pub fn cmd<'a>() -> clap::App<'a, 'a> { cmd::subcommand("run", "Run a Simplicity program in the context of a PSET input.") @@ -40,7 +62,12 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { match exec_inner(pset_b64, input_idx, program, witness, genesis_hash) { Ok(info) => cmd::print_output(matches, &info), - Err(e) => cmd::print_output(matches, &e), + Err(e) => cmd::print_output( + matches, + &Error { + error: format!("{}", e), + }, + ), } } @@ -69,7 +96,7 @@ fn exec_inner( program: &str, witness: &str, genesis_hash: Option<&str>, -) -> Result { +) -> Result { struct JetTracker(Vec); impl ExecTracker for JetTracker { fn track_left(&mut self, _: Ihr) {} @@ -127,22 +154,22 @@ fn exec_inner( // 1. Parse everything. let pset: elements::pset::PartiallySignedTransaction = - pset_b64.parse().result_context("decoding PSET")?; - let input_idx: u32 = input_idx.parse().result_context("parsing input-idx")?; + pset_b64.parse().map_err(PsetRunError::PsetDecode)?; + let input_idx: u32 = input_idx.parse().map_err(PsetRunError::InputIndexParse)?; let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems let program = Program::::from_str(program, Some(witness)) - .result_context("parsing program")?; + .map_err(PsetRunError::ProgramParse)?; // 2. Extract transaction environment. let (tx_env, _control_block, _tap_leaf) = super::execution_environment(&pset, input_idx_usize, program.cmr(), genesis_hash)?; // 3. Prune program. - let redeem_node = program.redeem_node().expect("populated"); + let redeem_node = program.redeem_node().ok_or(PsetRunError::NoRedeemNode)?; let mut mac = - BitMachine::for_program(redeem_node).result_context("constructing bit machine")?; + BitMachine::for_program(redeem_node).map_err(PsetRunError::BitMachineConstruction)?; let mut tracker = JetTracker(vec![]); // Eat success/failure. FIXME should probably report this to the user. let success = mac.exec_with_tracker(redeem_node, &tx_env, &mut tracker).is_ok(); diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/update_input.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/update_input.rs index 1a9ac3e..279dd8c 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/update_input.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/update_input.rs @@ -2,17 +2,62 @@ // SPDX-License-Identifier: CC0-1.0 use crate::cmd; +use crate::cmd::simplicity::pset::PsetError; +use crate::cmd::simplicity::{parse_elements_utxo, ParseElementsUtxoError}; use core::str::FromStr; use std::collections::BTreeMap; -use super::super::{Error, ErrorExt as _}; +use super::super::Error; use super::UpdatedPset; +use elements::bitcoin::secp256k1; use elements::schnorr::XOnlyPublicKey; use hal_simplicity::hal_simplicity::taproot_spend_info; use simplicity::hex::parse::FromHex as _; +#[derive(Debug, thiserror::Error)] +pub enum PsetUpdateInputError { + #[error(transparent)] + SharedError(#[from] PsetError), + + #[error("invalid PSET: {0}")] + PsetDecode(elements::pset::ParseError), + + #[error("invalid input index: {0}")] + InputIndexParse(std::num::ParseIntError), + + #[error("input index {index} out-of-range for PSET with {total} inputs")] + InputIndexOutOfRange { + index: usize, + total: usize, + }, + + #[error("invalid CMR: {0}")] + CmrParse(elements::hashes::hex::HexToArrayError), + + #[error("invalid internal key: {0}")] + InternalKeyParse(secp256k1::Error), + + #[error("internal key must be present if CMR is; PSET requires a control block for each CMR, which in turn requires the internal key. If you don't know the internal key, good chance it is the BIP-0341 'unspendable key' 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 or the web IDE's 'unspendable key' (highly discouraged for use in production) of f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2")] + MissingInternalKey, + + #[error("input UTXO does not appear to be a Taproot output")] + NotTaprootOutput, + + #[error("invalid state commitment: {0}")] + StateParse(elements::hashes::hex::HexToArrayError), + + #[error("CMR and internal key imply output key {output_key}, which does not match input scriptPubKey {script_pubkey}")] + OutputKeyMismatch { + output_key: String, + script_pubkey: String, + }, + + #[error("invalid elements UTXO: {0}")] + ElementsUtxoParse(ParseElementsUtxoError), +} + pub fn cmd<'a>() -> clap::App<'a, 'a> { cmd::subcommand("update-input", "Attach UTXO data to a PSET input") .args(&cmd::opts_networks()) @@ -55,7 +100,12 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { 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), + Err(e) => cmd::print_output( + matches, + &Error { + error: format!("{}", e), + }, + ), } } @@ -67,43 +117,40 @@ fn exec_inner( internal_key: Option<&str>, cmr: Option<&str>, state: Option<&str>, -) -> Result { +) -> Result { let mut pset: elements::pset::PartiallySignedTransaction = - pset_b64.parse().result_context("decoding PSET")?; - let input_idx: usize = input_idx.parse().result_context("parsing input-idx")?; - let input_utxo = super::super::parse_elements_utxo(input_utxo)?; + pset_b64.parse().map_err(PsetUpdateInputError::PsetDecode)?; + let input_idx: usize = input_idx.parse().map_err(PsetUpdateInputError::InputIndexParse)?; + let input_utxo = + parse_elements_utxo(input_utxo).map_err(PsetUpdateInputError::ElementsUtxoParse)?; let n_inputs = pset.n_inputs(); - let input = pset - .inputs_mut() - .get_mut(input_idx) - .ok_or_else(|| { - format!("index {} out-of-range for PSET with {} inputs", input_idx, n_inputs) - }) - .result_context("parsing input index")?; - - let cmr = cmr.map(simplicity::Cmr::from_str).transpose().result_context("parsing CMR")?; + let input = pset.inputs_mut().get_mut(input_idx).ok_or_else(|| { + PsetUpdateInputError::InputIndexOutOfRange { + index: input_idx, + total: n_inputs, + } + })?; + + let cmr = + cmr.map(simplicity::Cmr::from_str).transpose().map_err(PsetUpdateInputError::CmrParse)?; let internal_key = internal_key .map(XOnlyPublicKey::from_str) .transpose() - .result_context("parsing internal key")?; + .map_err(PsetUpdateInputError::InternalKeyParse)?; if cmr.is_some() && internal_key.is_none() { - return Err("internal key must be present if CMR is; PSET requires a control block for each CMR, which in turn requires the internal key. If you don't know the internal key, good chance it is the BIP-0341 'unspendable key' 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 or the web IDE's 'unspendable key' (highly discouraged for use in production) of f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2") - .result_context("missing internal key"); + return Err(PsetUpdateInputError::MissingInternalKey); } if !input_utxo.script_pubkey.is_v1_p2tr() { - return Err("input UTXO does not appear to be a Taproot output") - .result_context("input UTXO"); + return Err(PsetUpdateInputError::NotTaprootOutput); } // 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 state = + state.map(<[u8; 32]>::from_hex).transpose().map_err(PsetUpdateInputError::StateParse)?; let mut updated_values = vec![]; if let Some(internal_key) = internal_key { @@ -118,8 +165,10 @@ fn exec_inner( 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)) - .result_context("input UTXO"); + return Err(PsetUpdateInputError::OutputKeyMismatch { + output_key: format!("{}", spend_info.output_key().as_inner()), + script_pubkey: format!("{}", input_utxo.script_pubkey), + }); } // FIXME these unwraps and clones should be fixed by a new rust-bitcoin taproot API diff --git a/src/bin/hal-simplicity/cmd/simplicity/sighash.rs b/src/bin/hal-simplicity/cmd/simplicity/sighash.rs index 4e8b461..1a58948 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/sighash.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/sighash.rs @@ -2,13 +2,15 @@ // SPDX-License-Identifier: CC0-1.0 use crate::cmd; +use crate::cmd::simplicity::ParseElementsUtxoError; -use super::{Error, ErrorExt as _}; +use super::Error; +use elements::bitcoin::secp256k1; use elements::hashes::Hash as _; use elements::pset::PartiallySignedTransaction; use hal_simplicity::simplicity::bitcoin::secp256k1::{ - schnorr, Keypair, Message, Secp256k1, SecretKey, + schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey, }; use hal_simplicity::simplicity::elements; use hal_simplicity::simplicity::elements::hashes::sha256; @@ -20,6 +22,82 @@ use hal_simplicity::simplicity::Cmr; use serde::Serialize; +#[derive(Debug, thiserror::Error)] +pub enum SimplicitySighashError { + #[error("failed extracting transaction from PSET: {0}")] + PsetExtraction(elements::pset::Error), + + #[error("invalid transaction hex: {0}")] + TransactionHexParsing(elements::hex::Error), + + #[error("invalid transaction decoding: {0}")] + TransactionDecoding(elements::encode::Error), + + #[error("invalid input index: {0}")] + InputIndexParsing(std::num::ParseIntError), + + #[error("invalid CMR: {0}")] + CmrParsing(elements::hashes::hex::HexToArrayError), + + #[error("invalid control block hex: {0}")] + ControlBlockHexParsing(elements::hex::Error), + + #[error("invalid control block decoding: {0}")] + ControlBlockDecoding(elements::taproot::TaprootError), + + #[error("input index {index} out-of-range for PSET with {n_inputs} inputs")] + InputIndexOutOfRange { + index: u32, + n_inputs: usize, + }, + + #[error("could not find control block in PSET for CMR {cmr}")] + ControlBlockNotFound { + cmr: String, + }, + + #[error("with a raw transaction, control-block must be provided")] + ControlBlockRequired, + + #[error("witness UTXO field not populated for input {input}")] + WitnessUtxoMissing { + input: usize, + }, + + #[error("with a raw transaction, input-utxos must be provided")] + InputUtxosRequired, + + #[error("expected {expected} input UTXOs but got {actual}")] + InputUtxoCountMismatch { + expected: usize, + actual: usize, + }, + + #[error("invalid genesis hash: {0}")] + GenesisHashParsing(elements::hashes::hex::HexToArrayError), + + #[error("invalid secret key: {0}")] + SecretKeyParsing(secp256k1::Error), + + #[error("secret key had public key {derived}, but was passed explicit public key {provided}")] + PublicKeyMismatch { + derived: String, + provided: String, + }, + + #[error("invalid public key: {0}")] + PublicKeyParsing(secp256k1::Error), + + #[error("invalid signature: {0}")] + SignatureParsing(secp256k1::Error), + + #[error("if signature is provided, public-key must be provided as well")] + SignatureWithoutPublicKey, + + #[error("invalid input UTXO: {0}")] + InputUtxoParsing(ParseElementsUtxoError), +} + #[derive(Serialize)] struct SighashInfo { sighash: sha256::Hash, @@ -86,7 +164,12 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { input_utxos.as_deref(), ) { Ok(info) => cmd::print_output(matches, &info), - Err(e) => cmd::print_output(matches, &e), + Err(e) => cmd::print_output( + matches, + &Error { + error: format!("{}", e), + }, + ), } } @@ -101,7 +184,7 @@ fn exec_inner( public_key: Option<&str>, signature: Option<&str>, input_utxos: Option<&[&str]>, -) -> Result { +) -> Result { let secp = Secp256k1::new(); // Attempt to decode transaction as PSET first. If it succeeds, we can extract @@ -113,30 +196,32 @@ fn exec_inner( // Elements fails. May be tricky/annoying in Rust since Program is a // different type from Program. let tx = match pset { - Some(ref pset) => pset.extract_tx().result_context("extracting transaction from PSET")?, + Some(ref pset) => pset.extract_tx().map_err(SimplicitySighashError::PsetExtraction)?, None => { - let tx_bytes = Vec::from_hex(tx_hex).result_context("parsing transaction hex")?; - elements::encode::deserialize(&tx_bytes).result_context("decoding transaction")? + let tx_bytes = + Vec::from_hex(tx_hex).map_err(SimplicitySighashError::TransactionHexParsing)?; + elements::encode::deserialize(&tx_bytes) + .map_err(SimplicitySighashError::TransactionDecoding)? } }; - let input_idx: u32 = input_idx.parse().result_context("parsing input-idx")?; - let cmr: Cmr = cmr.parse().result_context("parsing cmr")?; + let input_idx: u32 = input_idx.parse().map_err(SimplicitySighashError::InputIndexParsing)?; + let cmr: Cmr = cmr.parse().map_err(SimplicitySighashError::CmrParsing)?; // If the user specifies a control block, use it. Otherwise query the PSET. let control_block = if let Some(cb) = control_block { - let cb_bytes = Vec::from_hex(cb).result_context("parsing control block hex")?; + let cb_bytes = Vec::from_hex(cb).map_err(SimplicitySighashError::ControlBlockHexParsing)?; // For txes from webide, the internal key in this control block will be the hardcoded // value f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2 - ControlBlock::from_slice(&cb_bytes).result_context("decoding control block")? + 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_else(|| { - format!("index {} out-of-range for PSET with {} inputs", input_idx, n_inputs) - }) - .result_context("parsing input index")?; + .ok_or(SimplicitySighashError::InputIndexOutOfRange { + index: input_idx, + n_inputs, + })?; let mut control_block = None; for (cb, script_ver) in &input.tap_scripts { @@ -147,20 +232,23 @@ fn exec_inner( match control_block { Some(cb) => cb, None => { - return Err(format!("could not find control block in PSET for CMR {}", cmr)) - .result_context("finding control block")? + return Err(SimplicitySighashError::ControlBlockNotFound { + cmr: cmr.to_string(), + }) } } } else { - return Err("with a raw transaction, control-block must be provided") - .result_context("computing control block"); + return Err(SimplicitySighashError::ControlBlockRequired); }; let input_utxos = if let Some(input_utxos) = input_utxos { input_utxos .iter() - .map(|utxo_str| super::parse_elements_utxo(utxo_str)) - .collect::, Error>>()? + .map(|utxo_str| { + super::parse_elements_utxo(utxo_str) + .map_err(SimplicitySighashError::InputUtxoParsing) + }) + .collect::, SimplicitySighashError>>()? } else if let Some(ref pset) = pset { pset.inputs() .iter() @@ -171,19 +259,24 @@ fn exec_inner( asset: utxo.asset, value: utxo.value, }), - None => Err(format!("witness_utxo field not populated for input {n}")), + None => Err(SimplicitySighashError::WitnessUtxoMissing { + input: n, + }), }) - .collect::, _>>() - .result_context("extracting input UTXO information")? + .collect::, SimplicitySighashError>>()? } else { - return Err("with a raw transaction, input-utxos must be provided") - .result_context("computing control block"); + return Err(SimplicitySighashError::InputUtxosRequired); }; - assert_eq!(input_utxos.len(), tx.input.len()); + if input_utxos.len() != tx.input.len() { + return Err(SimplicitySighashError::InputUtxoCountMismatch { + expected: tx.input.len(), + actual: input_utxos.len(), + }); + } // Default to Bitcoin blockhash. let genesis_hash = match genesis_hash { - Some(s) => s.parse().result_context("parsing genesis hash")?, + Some(s) => s.parse().map_err(SimplicitySighashError::GenesisHashParsing)?, None => elements::BlockHash::from_byte_array([ // copied out of simplicity-webide source 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, @@ -203,17 +296,18 @@ fn exec_inner( ); let (pk, sig) = match (public_key, signature) { - (Some(pk), None) => (Some(pk.parse().result_context("parsing public key")?), None), + (Some(pk), None) => ( + Some(pk.parse::().map_err(SimplicitySighashError::PublicKeyParsing)?), + None, + ), (Some(pk), Some(sig)) => ( - Some(pk.parse().result_context("parsing public key")?), - Some(sig.parse().result_context("parsing signature")?), + Some(pk.parse::().map_err(SimplicitySighashError::PublicKeyParsing)?), + Some( + sig.parse::() + .map_err(SimplicitySighashError::SignatureParsing)?, + ), ), - (None, Some(_)) => { - return Err(Error { - context: "reading cli arguments", - error: "if signature is provided, public-key must be provided as well".to_owned(), - }) - } + (None, Some(_)) => return Err(SimplicitySighashError::SignatureWithoutPublicKey), (None, None) => (None, None), }; @@ -223,18 +317,14 @@ fn exec_inner( sighash, signature: match secret_key { Some(sk) => { - let sk: SecretKey = sk.parse().result_context("parsing secret key hex")?; + let sk: SecretKey = sk.parse().map_err(SimplicitySighashError::SecretKeyParsing)?; let keypair = Keypair::from_secret_key(&secp, &sk); if let Some(ref pk) = pk { if pk != &keypair.x_only_public_key().0 { - return Err(Error { - context: "checking secret key and public key consistency", - error: format!( - "secret key had public key {}, but was passed explicit public key {}", - keypair.x_only_public_key().0, - pk, - ), + return Err(SimplicitySighashError::PublicKeyMismatch { + derived: keypair.x_only_public_key().0.to_string(), + provided: pk.to_string(), }); } } diff --git a/src/bin/hal-simplicity/cmd/tx.rs b/src/bin/hal-simplicity/cmd/tx.rs index 191c256..b2198b6 100644 --- a/src/bin/hal-simplicity/cmd/tx.rs +++ b/src/bin/hal-simplicity/cmd/tx.rs @@ -2,7 +2,7 @@ use std::convert::TryInto; use std::io::Write; use clap; -use elements::bitcoin; +use elements::bitcoin::{self, secp256k1}; use elements::encode::{deserialize, serialize}; use elements::hashes::Hash; use elements::secp256k1_zkp::{ @@ -24,6 +24,89 @@ use hal_simplicity::tx::{ }; use hal_simplicity::Network; +#[derive(Debug, thiserror::Error)] +pub enum TxError { + #[error("invalid JSON provided: {0}")] + JsonParse(serde_json::Error), + + #[error("failed to decode raw transaction hex: {0}")] + TxHex(hex::FromHexError), + + #[error("invalid tx format: {0}")] + TxDeserialize(elements::encode::Error), + + #[error("field \"{field}\" is required.")] + MissingField { + field: String, + }, + + #[error("invalid prevout format: {0}")] + PrevoutParse(bitcoin::blockdata::transaction::ParseOutPointError), + + #[error("txid field given without vout field")] + MissingVout, + + #[error("conflicting prevout information")] + ConflictingPrevout, + + #[error("no previous output provided")] + NoPrevout, + + #[error("invalid confidential commitment: {0}")] + ConfidentialCommitment(elements::secp256k1_zkp::Error), + + #[error("invalid confidential publicKey: {0}")] + ConfidentialCommitmentPublicKey(secp256k1::Error), + + #[error("wrong size of nonce field")] + NonceSize, + + #[error("invalid size of asset_entropy")] + AssetEntropySize, + + #[error("invalid asset_blinding_nonce: {0}")] + AssetBlindingNonce(elements::secp256k1_zkp::Error), + + #[error("decoding script assembly is not yet supported")] + AsmNotSupported, + + #[error("no scriptSig info provided")] + NoScriptSig, + + #[error("no scriptPubKey info provided")] + NoScriptPubKey, + + #[error("invalid outpoint in pegin_data: {0}")] + PeginOutpoint(bitcoin::blockdata::transaction::ParseOutPointError), + + #[error("outpoint in pegin_data does not correspond to input value")] + PeginOutpointMismatch, + + #[error("asset in pegin_data should be explicit")] + PeginAssetNotExplicit, + + #[error("invalid rangeproof: {0}")] + RangeProof(elements::secp256k1_zkp::Error), + + #[error("invalid sequence: {0}")] + Sequence(core::num::TryFromIntError), + + #[error("addresses for different networks are used in the output scripts")] + MixedNetworks, + + #[error("invalid surjection proof: {0}")] + SurjectionProof(elements::secp256k1_zkp::Error), + + #[error("value in pegout_data does not correspond to output value")] + PegoutValueMismatch, + + #[error("explicit value is required for pegout data")] + PegoutValueNotExplicit, + + #[error("asset in pegout_data does not correspond to output value")] + PegoutAssetMismatch, +} + pub fn subcommand<'a>() -> clap::App<'a, 'a> { cmd::subcommand_group("tx", "manipulate transactions") .subcommand(cmd_create()) @@ -47,17 +130,17 @@ fn cmd_create<'a>() -> clap::App<'a, 'a> { ]) } -/// Check both ways to specify the outpoint and panic if conflicting. -fn outpoint_from_input_info(input: &InputInfo) -> OutPoint { +/// Check both ways to specify the outpoint and return error if conflicting. +fn outpoint_from_input_info(input: &InputInfo) -> Result { let op1: Option = - input.prevout.as_ref().map(|op| op.parse().expect("invalid prevout format")); + input.prevout.as_ref().map(|op| op.parse().map_err(TxError::PrevoutParse)).transpose()?; let op2 = match input.txid { Some(txid) => match input.vout { Some(vout) => Some(OutPoint { txid, vout, }), - None => panic!("\"txid\" field given in input without \"vout\" field"), + None => return Err(TxError::MissingVout), }, None => None, }; @@ -65,13 +148,13 @@ fn outpoint_from_input_info(input: &InputInfo) -> OutPoint { match (op1, op2) { (Some(op1), Some(op2)) => { if op1 != op2 { - panic!("Conflicting prevout information in input."); + return Err(TxError::ConflictingPrevout); } - op1 + Ok(op1) } - (Some(op), None) => op, - (None, Some(op)) => op, - (None, None) => panic!("No previous output provided in input."), + (Some(op), None) => Ok(op), + (None, Some(op)) => Ok(op), + (None, None) => Err(TxError::NoPrevout), } } @@ -87,120 +170,130 @@ fn bytes_32(bytes: &[u8]) -> Option<[u8; 32]> { } } -fn create_confidential_value(info: ConfidentialValueInfo) -> confidential::Value { +fn create_confidential_value(info: ConfidentialValueInfo) -> Result { match info.type_ { - ConfidentialType::Null => confidential::Value::Null, - ConfidentialType::Explicit => confidential::Value::Explicit( - info.value.expect("Field \"value\" is required for explicit values."), - ), + ConfidentialType::Null => Ok(confidential::Value::Null), + ConfidentialType::Explicit => { + Ok(confidential::Value::Explicit(info.value.ok_or_else(|| TxError::MissingField { + field: "value".to_string(), + })?)) + } ConfidentialType::Confidential => { - let comm = PedersenCommitment::from_slice( - &info - .commitment - .expect("Field \"commitment\" is required for confidential values.") - .0[..], - ) - .expect("invalid confidential commitment"); - confidential::Value::Confidential(comm) + let commitment_data = info.commitment.ok_or_else(|| TxError::MissingField { + field: "commitment".to_string(), + })?; + let comm = PedersenCommitment::from_slice(&commitment_data.0[..]) + .map_err(TxError::ConfidentialCommitment)?; + Ok(confidential::Value::Confidential(comm)) } } } -fn create_confidential_asset(info: ConfidentialAssetInfo) -> confidential::Asset { +fn create_confidential_asset(info: ConfidentialAssetInfo) -> Result { match info.type_ { - ConfidentialType::Null => confidential::Asset::Null, - ConfidentialType::Explicit => confidential::Asset::Explicit( - info.asset.expect("Field \"asset\" is required for explicit assets."), - ), + ConfidentialType::Null => Ok(confidential::Asset::Null), + ConfidentialType::Explicit => { + Ok(confidential::Asset::Explicit(info.asset.ok_or_else(|| TxError::MissingField { + field: "asset".to_string(), + })?)) + } ConfidentialType::Confidential => { - let gen = Generator::from_slice( - &info - .commitment - .expect("Field \"commitment\" is required for confidential values.") - .0[..], - ) - .expect("invalid confidential commitment"); - confidential::Asset::Confidential(gen) + let commitment_data = info.commitment.ok_or_else(|| TxError::MissingField { + field: "commitment".to_string(), + })?; + let gen = Generator::from_slice(&commitment_data.0[..]) + .map_err(TxError::ConfidentialCommitment)?; + Ok(confidential::Asset::Confidential(gen)) } } } -fn create_confidential_nonce(info: ConfidentialNonceInfo) -> confidential::Nonce { +fn create_confidential_nonce(info: ConfidentialNonceInfo) -> Result { match info.type_ { - ConfidentialType::Null => confidential::Nonce::Null, - ConfidentialType::Explicit => confidential::Nonce::Explicit( - bytes_32(&info.nonce.expect("Field \"nonce\" is required for asset issuances.").0[..]) - .expect("wrong size of \"nonce\" field"), - ), + ConfidentialType::Null => Ok(confidential::Nonce::Null), + ConfidentialType::Explicit => { + let nonce = info.nonce.ok_or_else(|| TxError::MissingField { + field: "nonce".to_string(), + })?; + let bytes = bytes_32(&nonce.0[..]).ok_or(TxError::NonceSize)?; + Ok(confidential::Nonce::Explicit(bytes)) + } ConfidentialType::Confidential => { - let pubkey = PublicKey::from_slice( - &info - .commitment - .expect("Field \"commitment\" is required for confidential values.") - .0[..], - ) - .expect("invalid confidential commitment"); - confidential::Nonce::Confidential(pubkey) + let commitment_data = info.commitment.ok_or_else(|| TxError::MissingField { + field: "commitment".to_string(), + })?; + let pubkey = PublicKey::from_slice(&commitment_data.0[..]) + .map_err(TxError::ConfidentialCommitmentPublicKey)?; + Ok(confidential::Nonce::Confidential(pubkey)) } } } -fn create_asset_issuance(info: AssetIssuanceInfo) -> AssetIssuance { - AssetIssuance { - asset_blinding_nonce: Tweak::from_slice( - &info - .asset_blinding_nonce - .expect("Field \"asset_blinding_nonce\" is required for asset issuances.") - .0[..], - ) - .expect("Invalid \"asset_blinding_nonce\"."), - asset_entropy: bytes_32( - &info - .asset_entropy - .expect("Field \"asset_entropy\" is required for asset issuances.") - .0[..], - ) - .expect("Invalid size of \"asset_entropy\"."), - amount: create_confidential_value( - info.amount.expect("Field \"amount\" is required for asset issuances."), - ), - inflation_keys: create_confidential_value( - info.inflation_keys.expect("Field \"inflation_keys\" is required for asset issuances."), - ), - } +fn create_asset_issuance(info: AssetIssuanceInfo) -> Result { + let asset_blinding_nonce_data = + info.asset_blinding_nonce.ok_or_else(|| TxError::MissingField { + field: "asset_blinding_nonce".to_string(), + })?; + let asset_blinding_nonce = + Tweak::from_slice(&asset_blinding_nonce_data.0[..]).map_err(TxError::AssetBlindingNonce)?; + + let asset_entropy_data = info.asset_entropy.ok_or_else(|| TxError::MissingField { + field: "asset_entropy".to_string(), + })?; + let asset_entropy = bytes_32(&asset_entropy_data.0[..]).ok_or(TxError::AssetEntropySize)?; + + let amount_info = info.amount.ok_or_else(|| TxError::MissingField { + field: "amount".to_string(), + })?; + let amount = create_confidential_value(amount_info)?; + + let inflation_keys_info = info.inflation_keys.ok_or_else(|| TxError::MissingField { + field: "inflation_keys".to_string(), + })?; + let inflation_keys = create_confidential_value(inflation_keys_info)?; + + Ok(AssetIssuance { + asset_blinding_nonce, + asset_entropy, + amount, + inflation_keys, + }) } -fn create_script_sig(ss: InputScriptInfo) -> Script { +fn create_script_sig(ss: InputScriptInfo) -> Result { if let Some(hex) = ss.hex { if ss.asm.is_some() { warn!("Field \"asm\" of input is ignored."); } - - hex.0.into() + Ok(hex.0.into()) } else if ss.asm.is_some() { - panic!("Decoding script assembly is not yet supported."); + Err(TxError::AsmNotSupported) } else { - panic!("No scriptSig info provided."); + Err(TxError::NoScriptSig) } } -fn create_pegin_witness(pd: PeginDataInfo, prevout: bitcoin::OutPoint) -> Vec> { - if prevout != pd.outpoint.parse().expect("Invalid outpoint in field \"pegin_data\".") { - panic!("Outpoint in \"pegin_data\" does not correspond to input value."); +fn create_pegin_witness( + pd: PeginDataInfo, + prevout: bitcoin::OutPoint, +) -> Result>, TxError> { + let parsed_outpoint = pd.outpoint.parse().map_err(TxError::PeginOutpoint)?; + if prevout != parsed_outpoint { + return Err(TxError::PeginOutpointMismatch); } - let asset = match create_confidential_asset(pd.asset) { + let asset = match create_confidential_asset(pd.asset)? { confidential::Asset::Explicit(asset) => asset, - _ => panic!("Asset in \"pegin_data\" should be explicit."), + _ => return Err(TxError::PeginAssetNotExplicit), }; - vec![ + Ok(vec![ serialize(&pd.value), serialize(&asset), pd.genesis_hash.to_byte_array().to_vec(), serialize(&pd.claim_script.0), serialize(&pd.mainchain_tx_hex.0), serialize(&pd.merkle_proof.0), - ] + ]) } fn convert_outpoint_to_btc(p: elements::OutPoint) -> bitcoin::OutPoint { @@ -214,7 +307,7 @@ fn create_input_witness( info: Option, pd: Option, prevout: OutPoint, -) -> TxInWitness { +) -> Result { let pegin_witness = if let Some(info_wit) = info.as_ref().and_then(|info| info.pegin_witness.as_ref()) { if pd.is_some() { @@ -222,58 +315,74 @@ fn create_input_witness( } info_wit.iter().map(|h| h.clone().0).collect() } else if let Some(pd) = pd { - create_pegin_witness(pd, convert_outpoint_to_btc(prevout)) + create_pegin_witness(pd, convert_outpoint_to_btc(prevout))? } else { Default::default() }; if let Some(wi) = info { - TxInWitness { - amount_rangeproof: wi - .amount_rangeproof - .map(|b| Box::new(RangeProof::from_slice(&b.0).expect("invalid rangeproof"))), - inflation_keys_rangeproof: wi - .inflation_keys_rangeproof - .map(|b| Box::new(RangeProof::from_slice(&b.0).expect("invalid rangeproof"))), + let amount_rangeproof = wi + .amount_rangeproof + .map(|b| RangeProof::from_slice(&b.0).map_err(TxError::RangeProof).map(Box::new)) + .transpose()?; + let inflation_keys_rangeproof = wi + .inflation_keys_rangeproof + .map(|b| RangeProof::from_slice(&b.0).map_err(TxError::RangeProof).map(Box::new)) + .transpose()?; + + Ok(TxInWitness { + amount_rangeproof, + inflation_keys_rangeproof, script_witness: match wi.script_witness { Some(ref w) => w.iter().map(|h| h.clone().0).collect(), None => Vec::new(), }, pegin_witness, - } + }) } else { - TxInWitness { + Ok(TxInWitness { pegin_witness, ..Default::default() - } + }) } } -fn create_input(input: InputInfo) -> TxIn { +fn create_input(input: InputInfo) -> Result { let has_issuance = input.has_issuance.unwrap_or(input.asset_issuance.is_some()); let is_pegin = input.is_pegin.unwrap_or(input.pegin_data.is_some()); - let prevout = outpoint_from_input_info(&input); + let prevout = outpoint_from_input_info(&input)?; - TxIn { + let script_sig = input.script_sig.map(create_script_sig).transpose()?.unwrap_or_default(); + + let sequence = elements::Sequence::from_height( + input.sequence.unwrap_or_default().try_into().map_err(TxError::Sequence)?, + ); + + let asset_issuance = if has_issuance { + input.asset_issuance.map(create_asset_issuance).transpose()?.unwrap_or_default() + } else { + if input.asset_issuance.is_some() { + warn!("Field \"asset_issuance\" of input is ignored."); + } + Default::default() + }; + + let witness = create_input_witness(input.witness, input.pegin_data, prevout)?; + + Ok(TxIn { previous_output: prevout, - script_sig: input.script_sig.map(create_script_sig).unwrap_or_default(), - sequence: elements::Sequence::from_height( - input.sequence.unwrap_or_default().try_into().unwrap(), - ), + script_sig, + sequence, is_pegin, - asset_issuance: if has_issuance { - input.asset_issuance.map(create_asset_issuance).unwrap_or_default() - } else { - if input.asset_issuance.is_some() { - warn!("Field \"asset_issuance\" of input is ignored."); - } - Default::default() - }, - witness: create_input_witness(input.witness, input.pegin_data, prevout), - } + asset_issuance, + witness, + }) } -fn create_script_pubkey(spk: OutputScriptInfo, used_network: &mut Option) -> Script { +fn create_script_pubkey( + spk: OutputScriptInfo, + used_network: &mut Option, +) -> Result { if spk.type_.is_some() { warn!("Field \"type\" of output is ignored."); } @@ -287,28 +396,28 @@ fn create_script_pubkey(spk: OutputScriptInfo, used_network: &mut Option bitcoin::ScriptBuf { +fn create_bitcoin_script_pubkey( + spk: hal::tx::OutputScriptInfo, +) -> Result { if spk.type_.is_some() { warn!("Field \"type\" of output is ignored."); } @@ -322,85 +431,104 @@ fn create_bitcoin_script_pubkey(spk: hal::tx::OutputScriptInfo) -> bitcoin::Scri } //TODO(stevenroose) do script sanity check to avoid blackhole? - hex.0.into() + Ok(hex.0.into()) } else if spk.asm.is_some() { if spk.address.is_some() { warn!("Field \"address\" of output is ignored."); } - - panic!("Decoding script assembly is not yet supported."); + Err(TxError::AsmNotSupported) } else if let Some(address) = spk.address { - address.assume_checked().script_pubkey() + Ok(address.assume_checked().script_pubkey()) } else { - panic!("No scriptPubKey info provided."); + Err(TxError::NoScriptPubKey) } } -fn create_output_witness(w: OutputWitnessInfo) -> TxOutWitness { - TxOutWitness { - surjection_proof: w.surjection_proof.map(|b| { - Box::new(SurjectionProof::from_slice(&b.0[..]).expect("invalid surjection proof")) - }), - rangeproof: w - .rangeproof - .map(|b| Box::new(RangeProof::from_slice(&b.0[..]).expect("invalid rangeproof"))), - } +fn create_output_witness(w: OutputWitnessInfo) -> Result { + let surjection_proof = w + .surjection_proof + .map(|b| { + SurjectionProof::from_slice(&b.0[..]).map_err(TxError::SurjectionProof).map(Box::new) + }) + .transpose()?; + let rangeproof = w + .rangeproof + .map(|b| RangeProof::from_slice(&b.0[..]).map_err(TxError::RangeProof).map(Box::new)) + .transpose()?; + + Ok(TxOutWitness { + surjection_proof, + rangeproof, + }) } -fn create_script_pubkey_from_pegout_data(pd: PegoutDataInfo) -> Script { +fn create_script_pubkey_from_pegout_data(pd: PegoutDataInfo) -> Result { + let script_pubkey = create_bitcoin_script_pubkey(pd.script_pub_key)?; let mut builder = elements::script::Builder::new() .push_opcode(elements::opcodes::all::OP_RETURN) .push_slice(&pd.genesis_hash.to_byte_array()) - .push_slice(create_bitcoin_script_pubkey(pd.script_pub_key).as_bytes()); + .push_slice(script_pubkey.as_bytes()); for d in pd.extra_data { builder = builder.push_slice(&d.0); } - builder.into_script() + Ok(builder.into_script()) } -fn create_output(output: OutputInfo) -> TxOut { +fn create_output(output: OutputInfo) -> Result { // Keep track of which network has been used in addresses and error if two different networks // are used. let mut used_network = None; - let value = output - .value - .map(create_confidential_value) - .expect("Field \"value\" is required for outputs."); - let asset = output - .asset - .map(create_confidential_asset) - .expect("Field \"asset\" is required for outputs."); - - TxOut { - asset, - value, - nonce: output.nonce.map(create_confidential_nonce).unwrap_or(confidential::Nonce::Null), - script_pubkey: if let Some(spk) = output.script_pub_key { - if output.pegout_data.is_some() { - warn!("Field \"pegout_data\" of output is ignored."); - } - create_script_pubkey(spk, &mut used_network) - } else if let Some(pd) = output.pegout_data { - match value { - confidential::Value::Explicit(v) => { - if v != pd.value { - panic!("Value in \"pegout_data\" does not correspond to output value."); - } + let value_info = output.value.ok_or_else(|| TxError::MissingField { + field: "value".to_string(), + })?; + let value = create_confidential_value(value_info)?; + + let asset_info = output.asset.ok_or_else(|| TxError::MissingField { + field: "asset".to_string(), + })?; + let asset = create_confidential_asset(asset_info)?; + + let nonce = output + .nonce + .map(create_confidential_nonce) + .transpose()? + .unwrap_or(confidential::Nonce::Null); + + let script_pubkey = if let Some(spk) = output.script_pub_key { + if output.pegout_data.is_some() { + warn!("Field \"pegout_data\" of output is ignored."); + } + create_script_pubkey(spk, &mut used_network)? + } else if let Some(pd) = output.pegout_data { + match value { + confidential::Value::Explicit(v) => { + if v != pd.value { + return Err(TxError::PegoutValueMismatch); } - _ => panic!("Explicit value is required for pegout data."), } - if asset != create_confidential_asset(pd.asset.clone()) { - panic!("Asset in \"pegout_data\" does not correspond to output value."); - } - create_script_pubkey_from_pegout_data(pd) - } else { - Default::default() - }, - witness: output.witness.map(create_output_witness).unwrap_or_default(), - } + _ => return Err(TxError::PegoutValueNotExplicit), + } + let pd_asset = create_confidential_asset(pd.asset.clone())?; + if asset != pd_asset { + return Err(TxError::PegoutAssetMismatch); + } + create_script_pubkey_from_pegout_data(pd)? + } else { + Default::default() + }; + + let witness = output.witness.map(create_output_witness).transpose()?.unwrap_or_default(); + + Ok(TxOut { + asset, + value, + nonce, + script_pubkey, + witness, + }) } -pub fn create_transaction(info: TransactionInfo) -> Transaction { +pub fn create_transaction(info: TransactionInfo) -> Result { // Fields that are ignored. if info.txid.is_some() { warn!("Field \"txid\" is ignored."); @@ -418,28 +546,44 @@ pub fn create_transaction(info: TransactionInfo) -> Transaction { warn!("Field \"vsize\" is ignored."); } - Transaction { - version: info.version.expect("Field \"version\" is required."), - lock_time: info.locktime.expect("Field \"locktime\" is required."), - input: info - .inputs - .expect("Field \"inputs\" is required.") - .into_iter() - .map(create_input) - .collect(), - output: info - .outputs - .expect("Field \"outputs\" is required.") - .into_iter() - .map(create_output) - .collect(), - } + let version = info.version.ok_or_else(|| TxError::MissingField { + field: "version".to_string(), + })?; + let lock_time = info.locktime.ok_or_else(|| TxError::MissingField { + field: "locktime".to_string(), + })?; + + let inputs = info + .inputs + .ok_or_else(|| TxError::MissingField { + field: "inputs".to_string(), + })? + .into_iter() + .map(create_input) + .collect::, _>>()?; + + let outputs = info + .outputs + .ok_or_else(|| TxError::MissingField { + field: "outputs".to_string(), + })? + .into_iter() + .map(create_output) + .collect::, _>>()?; + + Ok(Transaction { + version, + lock_time, + input: inputs, + output: outputs, + }) } fn exec_create<'a>(matches: &clap::ArgMatches<'a>) { let info = serde_json::from_str::(&cmd::arg_or_stdin(matches, "tx-info")) - .expect("invalid JSON provided"); - let tx = create_transaction(info); + .map_err(TxError::JsonParse) + .unwrap_or_else(|e| panic!("{}", e)); + let tx = create_transaction(info).unwrap_or_else(|e| panic!("{}", e)); let tx_bytes = serialize(&tx); if matches.is_present("raw-stdout") { @@ -457,8 +601,10 @@ fn cmd_decode<'a>() -> clap::App<'a, 'a> { fn exec_decode<'a>(matches: &clap::ArgMatches<'a>) { let hex_tx = cmd::arg_or_stdin(matches, "raw-tx"); - let raw_tx = hex::decode(hex_tx.as_ref()).expect("could not decode raw tx"); - let tx: Transaction = deserialize(&raw_tx).expect("invalid tx format"); + let raw_tx = + hex::decode(hex_tx.as_ref()).map_err(TxError::TxHex).unwrap_or_else(|e| panic!("{}", e)); + let tx: Transaction = + deserialize(&raw_tx).map_err(TxError::TxDeserialize).unwrap_or_else(|e| panic!("{}", e)); let info = crate::GetInfo::get_info(&tx, cmd::network(matches)); cmd::print_output(matches, &info) diff --git a/tests/cli.rs b/tests/cli.rs index 00a5670..da7f0fd 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -172,7 +172,7 @@ OPTIONS: // length-prefixed, which is a little surprising and should be documented assert_cmd( &["address", "create"], - "Execution failed: Can't create addresses without a pubkey\n", + "Execution failed: can't create addresses without a pubkey\n", "", ); assert_cmd(&["address", "create", "-h"], expected_help, ""); @@ -194,7 +194,7 @@ For more information try --help // FIXME stdout instead of stderr assert_cmd( &["address", "create", "--pubkey", ""], - "Execution failed: invalid pubkey: InvalidHexLength(0)\n", + "Execution failed: invalid pubkey: pubkey string should be 66 or 130 digits long, got: 0\n", "", ); // x-only keys not supported @@ -205,7 +205,7 @@ For more information try --help "--pubkey", "abababababababababababababababababababababababababababababababab", ], - "Execution failed: invalid pubkey: InvalidHexLength(64)\n", + "Execution failed: invalid pubkey: pubkey string should be 66 or 130 digits long, got: 64\n", "", ); assert_cmd( @@ -215,7 +215,7 @@ For more information try --help "--pubkey", "020000000000000000000000000000000000000000000000000000000000000000", ], - "Execution failed: invalid pubkey: Encoding(Secp256k1(InvalidPublicKey))\n", + "Execution failed: invalid pubkey: string error\n", "", ); // uncompressed keys ok (though FIXME we should not produce p2wpkh or p2shwpkh addresses which are unspendable!!) @@ -231,7 +231,7 @@ For more information try --help // hybrid keys are not assert_cmd( &["address", "create", "--pubkey", "0700000000000000000000003b78ce563f89a0ed9414f5aa28ad0d96d6795f9c633f3979bf72ae8202983dc989aec7f2ff2ed91bdd69ce02fc0700ca100e59ddf3"], - "Execution failed: invalid pubkey: Encoding(InvalidKeyPrefix(7))\n", + "Execution failed: invalid pubkey: string error\n", "", ); // compressed keys are ok, and the output is NOT the same as for uncompressed keys @@ -258,18 +258,18 @@ For more information try --help "--blinder", "0200000000000000000000003b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63", ], - "Execution failed: Can't create addresses without a pubkey\n", + "Execution failed: can't create addresses without a pubkey\n", "", ); // Invalid blinders all get the same generic message, and we don't even check for a pubkey assert_cmd( &["address", "create", "--blinder", ""], - "Execution failed: invalid blinder: InvalidPublicKey\n", + "Execution failed: invalid blinder: malformed public key\n", "", ); assert_cmd( &["address", "create", "--blinder", "02abcd"], - "Execution failed: invalid blinder: InvalidPublicKey\n", + "Execution failed: invalid blinder: malformed public key\n", "", ); assert_cmd( @@ -279,7 +279,7 @@ For more information try --help "--blinder", "abababababababababababababababababababababababababababababababab", ], - "Execution failed: invalid blinder: InvalidPublicKey\n", + "Execution failed: invalid blinder: malformed public key\n", "", ); assert_cmd( @@ -289,7 +289,7 @@ For more information try --help "--blinder", "020000000000000000000000000000000000000000000000000000000000000000", ], - "Execution failed: invalid blinder: InvalidPublicKey\n", + "Execution failed: invalid blinder: malformed public key\n", "", ); // good pubkey, blinder @@ -623,19 +623,19 @@ For more information try --help // FIXME stdout instead of stderr assert_cmd( &["address", "inspect", ""], - "Execution failed: invalid address format: Base58(TooShort(TooShortError { length: 0 }))\n", + "Execution failed: invalid address format: base58 error: too short\n", "", ); // FIXME this error is absolutely terrible assert_cmd( &["address", "inspect", "bc1q7z3dshje7e4tftag5c3w7e85pr00r6cq34khh8"], - "Execution failed: invalid address format: Base58(Decode(InvalidCharacterError { invalid: 48 }))\n", + "Execution failed: invalid address format: base58 error: decode\n", "", ); // FIXME this one is possibly even worse assert_cmd( &["address", "inspect", "1Au8w4fejHaJBbrZCMrfg6v2hwJNr3go1N"], - "Execution failed: invalid address format: InvalidAddress(\"1Au8w4fejHaJBbrZCMrfg6v2hwJNr3go1N\")\n", + "Execution failed: invalid address format: was unable to parse the address: 1Au8w4fejHaJBbrZCMrfg6v2hwJNr3go1N\n", "", ); // liquid addresses ok @@ -796,8 +796,16 @@ ARGS: assert_cmd(&["block", "create", "--help", "xyz"], expected_help, ""); // TODO this was as far as I got trying to find a valid input - assert_cmd(&["block", "create", ""], "Execution failed: invaid json JSON input: Error(\"EOF while parsing a value\", line: 1, column: 0)\n", ""); - assert_cmd(&["block", "create", "{}"], "Execution failed: invaid json JSON input: Error(\"missing field `header`\", line: 1, column: 2)\n", ""); + assert_cmd( + &["block", "create", ""], + "Execution failed: invalid json JSON input: EOF while parsing a value at line 1 column 0\n", + "", + ); + assert_cmd( + &["block", "create", "{}"], + "Execution failed: invalid json JSON input: missing field `header` at line 1 column 2\n", + "", + ); assert_cmd( &[ "block", @@ -813,10 +821,14 @@ ARGS: } }"#, ], - "Execution failed: missing challenge\n", + "Execution failed: challenge missing in proof params\n", + "", + ); + assert_cmd( + &["block", "create", "{}"], + "Execution failed: invalid json JSON input: missing field `header` at line 1 column 2\n", "", ); - assert_cmd(&["block", "create", "{}"], "Execution failed: invaid json JSON input: Error(\"missing field `header`\", line: 1, column: 2)\n", ""); // FIXME this error is awful; the actual field it wants is called `dynafed_current` assert_cmd( &[ @@ -833,7 +845,7 @@ ARGS: } }"#, ], - "Execution failed: missing current params\n", + "Execution failed: current missing in dynafed params\n", "", ); @@ -869,7 +881,7 @@ ARGS: // Also, as always, these errors show up on stdout instead of stderr.. assert_cmd( &["block", "create", &header_json.replace("%TRANSACTIONS%", "")], - "Execution failed: No transactions provided.\n", + "Execution failed: no transactions provided.\n", "", ); assert_cmd( @@ -897,7 +909,7 @@ ARGS: &header_json .replace("%TRANSACTIONS%", ", \"transactions\": [], \"raw_transactions\": []"), ], - "Execution failed: Can't provide transactions both in JSON and raw.\n", + "Execution failed: can't provide transactions both in JSON and raw.\n", "", ); @@ -943,16 +955,22 @@ ARGS: assert_cmd(&["block", "decode", "--help", "xyz"], expected_help, ""); // FIXME this error message is awful, and it's on stdout - assert_cmd(&["block", "decode", ""], "Execution failed: invalid block format: Io(Error { kind: UnexpectedEof, message: \"failed to fill whole buffer\" })\n", ""); + assert_cmd( + &["block", "decode", ""], + "Execution failed: invalid block format: I/O error: failed to fill whole buffer\n", + "", + ); // This is a hex-encoded block header, not a full block assert_cmd(&["block", "decode", BLOCK_HEADER_1585319], HEADER_DECODE_1585319, ""); // This is the same hex-encoded block header, with --txids. FIXME this is awful. - assert_cmd(&["block", "decode", "--txids", BLOCK_HEADER_1585319], - "Execution failed: invalid block format: Io(Error { kind: UnexpectedEof, message: \"failed to fill whole buffer\" })\n", -""); + assert_cmd( + &["block", "decode", "--txids", BLOCK_HEADER_1585319], + "Execution failed: invalid block format: I/O error: failed to fill whole buffer\n", + "", + ); // Here is the header plus some arbitrary junk assert_cmd(&["block", "decode", &(BLOCK_HEADER_1585319.to_owned() + "0000")], - "Execution failed: invalid block format: ParseFailed(\"data not consumed entirely when explicitly deserializing\")\n", + "Execution failed: invalid block format: parse failed: data not consumed entirely when explicitly deserializing\n", ""); // Here is the whole block. assert_cmd(&["block", "decode", FULL_BLOCK_1585319], HEADER_DECODE_1585319, ""); @@ -1161,13 +1179,17 @@ ARGS: assert_cmd(&["tx", "create", "--help"], expected_help, ""); assert_cmd(&["tx", "create", "--help", "xyz"], expected_help, ""); - assert_cmd(&["tx", "create", ""], "Execution failed: invalid JSON provided: Error(\"EOF while parsing a value\", line: 1, column: 0)\n", ""); - assert_cmd(&["tx", "create", "{ }"], "Execution failed: Field \"version\" is required.\n", ""); + assert_cmd( + &["tx", "create", ""], + "Execution failed: invalid JSON provided: EOF while parsing a value at line 1 column 0\n", + "", + ); + assert_cmd(&["tx", "create", "{ }"], "Execution failed: field \"version\" is required.\n", ""); // FIXME I have no idea what is wrong here. But putting a test in to track fixing // whatever is causing this nonsense error. assert_cmd( &["tx", "create", "{ \"version\": 10, \"locktime\": 10 }"], - "Execution failed: invalid JSON provided: Error(\"expected value\", line: 1, column: 30)\n", + "Execution failed: invalid JSON provided: expected value at line 1 column 30\n", "", ); // FIXME: lol, replace this locktime format with something sane @@ -1220,9 +1242,13 @@ ARGS: assert_cmd(&["tx", "decode", "--help"], expected_help, ""); assert_cmd(&["tx", "decode", "--help", "xyz"], expected_help, ""); - assert_cmd(&["tx", "decode", ""], "Execution failed: invalid tx format: Io(Error { kind: UnexpectedEof, message: \"failed to fill whole buffer\" })\n", ""); + assert_cmd( + &["tx", "decode", ""], + "Execution failed: invalid tx format: I/O error: failed to fill whole buffer\n", + "", + ); // A bitcoin transaction - assert_cmd(&["tx", "decode", "02000000000101cd5d8addc8ed0d91d9338a1e524a87185b8bb3c1760e0a19c4ad576b217fd7ca0100000000fdffffff02f50100000000000016001468647ece9c25ab162c72dbedfe7de63db1913e39e50d00000000000016001413aac2fc1cef3dacc656bfe8fe342a03a5feac6302473044022059e6f5ccc1d89bf31a3847a464cce1fcf0e56e43633787d03ebb2ebc1899e28c02207f3f05a16a87f07fe82bfa35c509e7d969243c6215080a6775877bef113c9e7b012103b303769299ca63c9076fc8f91d6e27152a81fc884f9fe95f47fd2a262c987256b7c50d00"], "Execution failed: invalid tx format: NonMinimalVarInt\n", ""); + assert_cmd(&["tx", "decode", "02000000000101cd5d8addc8ed0d91d9338a1e524a87185b8bb3c1760e0a19c4ad576b217fd7ca0100000000fdffffff02f50100000000000016001468647ece9c25ab162c72dbedfe7de63db1913e39e50d00000000000016001413aac2fc1cef3dacc656bfe8fe342a03a5feac6302473044022059e6f5ccc1d89bf31a3847a464cce1fcf0e56e43633787d03ebb2ebc1899e28c02207f3f05a16a87f07fe82bfa35c509e7d969243c6215080a6775877bef113c9e7b012103b303769299ca63c9076fc8f91d6e27152a81fc884f9fe95f47fd2a262c987256b7c50d00"], "Execution failed: invalid tx format: non-minimal varint\n", ""); // A Liquid transaction let tx_decode = r#"{ "txid": "9523d75b48b3411a3f4ebd31b6005898deebbe748875aa6ee084b94aa8422ba6",