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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 78 additions & 67 deletions crates/sdk/src/signer/core.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use std::collections::HashMap;
#[cfg(feature = "provider")]
use std::collections::HashSet;
use std::ops::Deref;
use std::str::FromStr;

use simplicityhl::elements::pset::PartiallySignedTransaction;
use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, SECP256K1, Secp256k1, ecdsa, schnorr};
use simplicityhl::elements::secp256k1_zkp::{All, Keypair, Message, Secp256k1, ecdsa, schnorr};
use simplicityhl::elements::{Address, Script, Transaction, Witness};
#[cfg(feature = "provider")]
use simplicityhl::elements::{AssetId, OutPoint, Txid};
use simplicityhl::simplicity::bitcoin::XOnlyPublicKey;
use simplicityhl::{WitnessNameToValueMap, WitnessValues};

use bip39::Mnemonic;
use bip39::rand::thread_rng;

use elements_miniscript::{
ConfidentialDescriptor, Descriptor, DescriptorPublicKey,
Expand Down Expand Up @@ -312,8 +312,11 @@ impl Signer {
///
/// # Errors
/// - `SignerError::NotEnoughFunds` if the wallet runs out before the fee is covered.
/// - `SignerError::UnknownInputAmount` if an input is confidential and has no unblinding secrets.
/// - `SignerError::DuplicateInput` if `tx` spends an outpoint twice.
/// - `SignerError::PartialInput` if an input is confidential and has no unblinding secrets, or
/// its secrets don't open the output it spends.
/// - `SignerError::FinalTransaction` with `FinalTransactionError::DuplicateInput` if `tx` spends
/// an outpoint twice, or with the errors of [`FinalTransaction::extract_blinded_pst`] if its
/// outputs can't be blinded.
/// - `SignerError::ConfidentialInputWithoutBlindedOutput` if `tx` spends a confidential input,
/// has an explicit change target, and doesn't have any blinded output.
/// - `SignerError::Unbalanced` if an asset other than the policy asset doesn't balance.
Expand Down Expand Up @@ -350,8 +353,11 @@ impl Signer {
///
/// # Errors
/// - `SignerError::InvalidFeeRate` if `fee_rate` isn't a finite, non-negative number.
/// - `SignerError::UnknownInputAmount` if an input is confidential and has no unblinding secrets.
/// - `SignerError::DuplicateInput` if the transaction spends an outpoint twice.
/// - `SignerError::PartialInput` if an input is confidential and has no unblinding secrets, or
/// its secrets don't open the output it spends.
/// - `SignerError::FinalTransaction` with `FinalTransactionError::DuplicateInput` if the
/// transaction spends an outpoint twice, or with the errors of
/// [`FinalTransaction::extract_blinded_pst`] if its outputs can't be blinded.
/// - `SignerError::DustAmount` if the inputs don't exceed the outputs by at least [`MIN_FEE`] of
/// the policy asset.
/// - `SignerError::NotEnoughFeeAmount` if that surplus can't pay the fee, or the fee and the
Expand Down Expand Up @@ -385,7 +391,7 @@ impl Signer {
/// [`MIN_FEE`] instead: the smallest surplus that pays both the fee and the change.
///
/// # Errors
/// Returns `SignerError::InvalidFeeRate`, `UnknownInputAmount`, `DuplicateInput`,
/// Returns `SignerError::InvalidFeeRate`, `PartialInput`, `FinalTransaction`,
/// `ConfidentialInputWithoutBlindedOutput` or `Unbalanced` as [`Signer::finalize_strict`] does,
/// and `ProgramError::ProgramExecution` if a program rejects the spend.
pub fn estimate_strict(&self, tx: &FinalTransaction, fee_rate: f32) -> Result<u64, SignerError> {
Expand All @@ -404,7 +410,7 @@ impl Signer {
///
/// # Errors
/// - `SignerError::NotEnoughFeeAmount` if the surplus is below the fee at `fee_rate`.
/// - `SignerError::InvalidFeeRate`, `UnknownInputAmount`, `DuplicateInput`,
/// - `SignerError::InvalidFeeRate`, `PartialInput`, `FinalTransaction`,
/// `ConfidentialInputWithoutBlindedOutput` or `Unbalanced` as [`Signer::finalize_strict`]
/// does, and the errors of [`Signer::sign_tx`].
pub fn finalize_strict_fee(&self, tx: &FinalTransaction, fee_rate: f32) -> Result<(Transaction, u64), SignerError> {
Expand Down Expand Up @@ -434,7 +440,7 @@ impl Signer {
/// signed transaction needs.
///
/// # Errors
/// Returns `SignerError::InvalidFeeRate`, `UnknownInputAmount`, `DuplicateInput` or
/// Returns `SignerError::InvalidFeeRate`, `PartialInput`, `FinalTransaction` or
/// `ConfidentialInputWithoutBlindedOutput` as [`Signer::finalize_strict`] does, and
/// `ProgramError::ProgramExecution` if a program rejects the spend.
pub fn estimate_strict_fee(
Expand Down Expand Up @@ -481,29 +487,26 @@ impl Signer {
/// without executing or finalizing the programs. Inputs that require no signature stay unsigned.
///
/// Returns the PSET, with the native inputs finalized, and each program input's witness with
/// its signature inserted, by input index. Blinding is random, so when `tx` has blinded
/// outputs, every call produces different output commitments and so a different
/// `sighash_all`.
/// its signature inserted, by input index. Blinding draws a new ephemeral key for every blinded
/// output, so when `tx` has blinded outputs, every call produces a different `sighash_all`.
///
/// Running a program input with [`ProgramTrait::finalize`] on the result is a dry run: it
/// checks the program with its real signature without finalizing anything.
///
/// # Errors
/// Returns a `SignerError` if blinding, signing or injecting a signature into a program witness fails.
/// Returns `SignerError::PartialInput` as [`Signer::finalize_strict`] does, the errors of
/// [`FinalTransaction::extract_blinded_pst`] as `SignerError::FinalTransaction`, and another
/// `SignerError` if signing or injecting a signature into a program witness fails.
pub fn sign_witnesses(
&self,
tx: &FinalTransaction,
) -> Result<(PartiallySignedTransaction, HashMap<usize, WitnessValues>), SignerError> {
Self::check_known_amounts(tx)?;
tx.check_inputs_assets_and_amounts()?;

let (mut pst, secrets) = tx.extract_pst()?;
let mut pst = tx.extract_blinded_pst()?;
let inputs = tx.inputs();
let mut signed_witnesses = HashMap::new();

if tx.needs_blinding() {
pst.blind_last(&mut thread_rng(), &self.secp, &secrets)?;
}

// Native inputs share one extracted transaction and sighash cache.
let mut sighash_cache = if inputs
.iter()
Expand Down Expand Up @@ -733,8 +736,8 @@ impl Signer {
/// `tx` with wallet inputs, a change output and a fee output added, and its fee.
#[cfg(feature = "provider")]
fn complete(&self, tx: &FinalTransaction) -> Result<(FinalTransaction, u64), SignerError> {
Self::check_known_amounts(tx)?;
Self::check_unique_inputs(tx)?;
tx.check_inputs_assets_and_amounts()?;
tx.check_unique_inputs()?;

let fee_rate = self.get_provider()?.fetch_fee_rate(1)?;
Self::check_fee_rate(fee_rate)?;
Expand Down Expand Up @@ -815,8 +818,8 @@ impl Signer {
/// `tx` with a change output and a fee output added at `fee_rate`, or the fee it needs when the
/// surplus is too small.
fn complete_strict(&self, tx: &FinalTransaction, fee_rate: f32) -> Result<Completion, SignerError> {
Self::check_known_amounts(tx)?;
Self::check_unique_inputs(tx)?;
tx.check_inputs_assets_and_amounts()?;
tx.check_unique_inputs()?;
Self::check_fee_rate(fee_rate)?;

let available_delta = tx.calculate_fee_delta(&self.network)?.max(0).cast_unsigned();
Expand All @@ -831,8 +834,8 @@ impl Signer {
network: &SimplicityNetwork,
fee_rate: f32,
) -> Result<(FinalTransaction, u64), SignerError> {
Self::check_known_amounts(tx)?;
Self::check_unique_inputs(tx)?;
tx.check_inputs_assets_and_amounts()?;
tx.check_unique_inputs()?;
Self::check_fee_rate(fee_rate)?;

if tx.has_confidential_input() && !tx.needs_blinding() {
Expand Down Expand Up @@ -959,11 +962,7 @@ impl Signer {
/// Every program input runs with its signature checks passing, so a placeholder prunes the
/// program as the real signature does.
fn measure_fee(tx: &FinalTransaction, network: &SimplicityNetwork, fee_rate: f32) -> Result<u64, SignerError> {
let (mut pst, secrets) = tx.extract_pst()?;

if tx.needs_blinding() {
pst.blind_last(&mut thread_rng(), SECP256K1, &secrets)?;
}
let mut pst = tx.extract_blinded_pst()?;

for (index, input_i) in tx.inputs().iter().enumerate() {
let stack = if let Some(program_input) = &input_i.program_input {
Expand Down Expand Up @@ -1058,32 +1057,6 @@ impl Signer {
}
}

/// A transaction spending an outpoint twice can never confirm.
fn check_unique_inputs(tx: &FinalTransaction) -> Result<(), SignerError> {
let mut seen = HashSet::new();

match tx
.inputs()
.iter()
.find(|input| !seen.insert(input.partial_input.outpoint))
{
Some(input) => Err(SignerError::DuplicateInput(input.partial_input.outpoint)),
None => Ok(()),
}
}

/// Balancing and signing need the amount of every input.
fn check_known_amounts(tx: &FinalTransaction) -> Result<(), SignerError> {
match tx
.inputs()
.iter()
.find(|input| input.partial_input.asset_and_amount().is_none())
{
Some(input) => Err(SignerError::UnknownInputAmount(input.partial_input.outpoint)),
None => Ok(()),
}
}

/// The account path, `m/84h/1776h/0h` on mainnet and `m/84h/1h/0h` elsewhere.
fn account_path(network: SimplicityNetwork) -> DerivationPath {
let coin_type = if network.is_mainnet() { 1776 } else { 1 };
Expand Down Expand Up @@ -1171,14 +1144,16 @@ impl Signer {

#[cfg(test)]
mod tests {
use bip39::rand::thread_rng;

use simplicityhl::elements::confidential::{Asset, AssetBlindingFactor, Value};
use simplicityhl::elements::{OutPoint, TxOut, Txid};

use simplicityhl::elements::TxOutSecrets;
use simplicityhl::elements::confidential::ValueBlindingFactor;

use crate::provider::{EsploraProvider, ProviderError};
use crate::transaction::{FinalTransactionError, PartialInput, UTXO};
use crate::transaction::{FinalTransactionError, PartialInput, PartialInputError, UTXO};
use crate::utils::random_mnemonic;

use super::*;
Expand Down Expand Up @@ -1293,18 +1268,51 @@ mod tests {
use simplicityhl::elements::confidential::{AssetBlindingFactor, ValueBlindingFactor};
use simplicityhl::elements::{TxOut, TxOutSecrets};

let asset = signer.network.policy_asset();
let asset_bf = AssetBlindingFactor::new(&mut thread_rng());
let value_bf = ValueBlindingFactor::new(&mut thread_rng());

PartialInput::new(UTXO {
outpoint: OutPoint::new(Txid::from_byte_array([0x01; 32]), 0),
txout: TxOut::default(),
secrets: Some(TxOutSecrets::new(
signer.network.policy_asset(),
AssetBlindingFactor::zero(),
value,
ValueBlindingFactor::zero(),
)),
txout: TxOut {
asset: Asset::new_confidential(&signer.secp, asset, asset_bf),
value: Value::new_confidential_from_assetid(&signer.secp, value, asset, value_bf, asset_bf),
..TxOut::default()
},
secrets: Some(TxOutSecrets::new(asset, asset_bf, value, value_bf)),
})
}

#[test]
fn inputs_whose_secrets_do_not_open_them_are_refused() {
let signer = create_signer();
let mut input = confidential_input(&signer, 100_000);

input.secrets.as_mut().unwrap().value = 100_001;

let mut ft = FinalTransaction::new();

ft.add_input(input, Some(NativeSignature::Ecdsa));
ft.add_output(
PartialOutput::new(
signer.get_address().script_pubkey(),
50_000,
signer.network.policy_asset(),
)
.with_blinding_key(signer.get_blinding_public_key()),
);

for result in [
signer.estimate_strict(&ft, 1.0).map(|_| ()),
signer.sign_witnesses(&ft).map(|_| ()),
] {
assert!(matches!(
result,
Err(SignerError::PartialInput(PartialInputError::AmountMismatch(_)))
));
}
}

#[test]
fn explicit_change_target_cannot_balance_a_confidential_input() {
let signer = create_signer();
Expand Down Expand Up @@ -1407,7 +1415,7 @@ mod tests {

assert!(matches!(
signer.sign_witnesses(&ft),
Err(SignerError::UnknownInputAmount(unknown)) if unknown == outpoint
Err(SignerError::PartialInput(PartialInputError::UnknownAmount(unknown))) if unknown == outpoint
));
}

Expand Down Expand Up @@ -1530,7 +1538,10 @@ mod tests {
signer.sign_witnesses(&ft).map(|_| ()),
signer.sign_tx(&ft).map(|_| ()),
] {
assert!(matches!(result, Err(SignerError::UnknownInputAmount(_))));
assert!(matches!(
result,
Err(SignerError::PartialInput(PartialInputError::UnknownAmount(_)))
));
}
}

Expand Down Expand Up @@ -1595,7 +1606,7 @@ mod tests {

assert!(matches!(
signer.finalize_strict(&ft, 100.0),
Err(SignerError::DuplicateInput(_))
Err(SignerError::FinalTransaction(FinalTransactionError::DuplicateInput(_)))
));
}

Expand Down
19 changes: 6 additions & 13 deletions crates/sdk/src/signer/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use elements_miniscript::bitcoin::bip32::DerivationPath;
use crate::program::ProgramError;
use crate::provider::ProviderError;
use crate::transaction::error::ProgramInputError;
use crate::transaction::{FinalTransactionError, UtxoError, WitnessWrappingError};
use crate::transaction::{FinalTransactionError, PartialInputError, UtxoError, WitnessWrappingError};

/// Errors from a [`Signer`](super::Signer).
///
Expand Down Expand Up @@ -31,6 +31,11 @@ pub enum SignerError {
#[error(transparent)]
Utxo(#[from] UtxoError),

/// An input's asset and amount are unknown, or its unblinding secrets don't open the output it
/// spends.
#[error(transparent)]
PartialInput(#[from] PartialInputError),

/// Errors originating from transaction construction or validation.
#[error(transparent)]
FinalTransaction(#[from] FinalTransactionError),
Expand Down Expand Up @@ -60,10 +65,6 @@ pub enum SignerError {
#[error("Failed to unblind txout: {0}")]
Unblind(#[source] Box<simplicityhl::elements::UnblindError>),

/// Error thrown when PSET blinding fails.
#[error("Failed to blind a PST: {0}")]
PsetBlind(#[from] simplicityhl::elements::pset::PsetBlindError),

/// Error indicating failure to construct sighash for input spending.
#[error("Failed to construct a message for the input spending: {0}")]
SighashConstruction(#[from] elements_miniscript::psbt::SighashError),
Expand All @@ -84,10 +85,6 @@ pub enum SignerError {
#[error("Invalid fee rate: {0} sats/kvb")]
InvalidFeeRate(f32),

/// Error indicating the transaction spends the same outpoint more than once.
#[error("The transaction spends {0} more than once")]
DuplicateInput(simplicityhl::elements::OutPoint),

/// Error indicating that the available UTXO funds are not enough to cover total costs.
#[error("Not enough funds on account to cover transaction costs: {0}")]
NotEnoughFunds(u64),
Expand Down Expand Up @@ -135,10 +132,6 @@ pub enum SignerError {
/// Error thrown when WPKH address creation fails.
#[error("Failed to construct a wpkh address: {0}")]
WpkhAddressConstruction(#[from] elements_miniscript::Error),

/// Error indicating an input whose amount the signer cannot know.
#[error("The input spending {0} is confidential and has no unblinding secrets, so its amount is unknown")]
UnknownInputAmount(simplicityhl::elements::OutPoint),
}

impl From<ProgramInputError> for SignerError {
Expand Down
Loading
Loading