Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another attempt at max_satisfaction_weight fixes #476

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
2 changes: 1 addition & 1 deletion embedded/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fn main() -> ! {
assert!(desc.sanity_check().is_ok());

// Estimate the satisfaction cost
assert_eq!(desc.max_satisfaction_weight().unwrap(), 293);
assert_eq!(desc.max_weight_to_satisfy().unwrap(), 288);
// end miniscript test

// exit QEMU
Expand Down
2 changes: 1 addition & 1 deletion examples/psbt_sign_finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() {
);
println!(
"Weight for witness satisfaction cost {}",
bridge_descriptor.max_satisfaction_weight().unwrap()
bridge_descriptor.max_weight_to_satisfy().unwrap()
);

let master_private_key_str = "cQhdvB3McbBJdx78VSSumqoHQiSXs75qwLptqwxSQBNBMDxafvaw";
Expand Down
6 changes: 3 additions & 3 deletions examples/sign_multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ fn main() {
let descriptor = miniscript::Descriptor::<bitcoin::PublicKey>::from_str(&s).unwrap();

// Check weight for witness satisfaction cost ahead of time.
// 4 (scriptSig length of 0) + 1 (witness stack size) + 106 (serialized witnessScript)
// + 73*2 (signature length + signatures + sighash bytes) + 1 (dummy byte) = 258
assert_eq!(descriptor.max_satisfaction_weight().unwrap(), 258);
// 106 (serialized witnessScript)
// + 73*2 (signature length + signatures + sighash bytes) + 1 (dummy byte) = 253
assert_eq!(descriptor.max_weight_to_satisfy().unwrap(), 253);

// Sometimes it is necessary to have additional information to get the
// `bitcoin::PublicKey` from the `MiniscriptKey` which can be supplied by
Expand Down
10 changes: 5 additions & 5 deletions examples/taproot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ fn main() {

// Max Satisfaction Weight for compilation, corresponding to the script-path spend
// `multi_a(2,PUBKEY_1,PUBKEY_2) at taptree depth 1, having
// Max Witness Size = scriptSig len + witnessStack len + varint(control_block_size) +
// control_block size + varint(script_size) + script_size + max_satisfaction_size
// = 4 + 1 + 1 + 65 + 1 + 70 + 132 = 274
let max_sat_wt = real_desc.max_satisfaction_weight().unwrap();
assert_eq!(max_sat_wt, 274);
// Max Witness Size = varint(control_block_size) + control_block size +
// varint(script_size) + script_size + max_satisfaction_size
// = 1 + 65 + 1 + 70 + 132 = 269
let max_sat_wt = real_desc.max_weight_to_satisfy().unwrap();
assert_eq!(max_sat_wt, 269);

// Compute the bitcoin address and check if it matches
let network = Network::Bitcoin;
Expand Down
42 changes: 42 additions & 0 deletions src/descriptor/bare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@ impl<Pk: MiniscriptKey> Bare<Pk> {
Ok(())
}

/// Computes an upper bound on the difference between a non-satisfied
/// `TxIn`'s `segwit_weight` and a satisfied `TxIn`'s `segwit_weight`
///
/// Since this method uses `segwit_weight` instead of `legacy_weight`,
/// if you want to include only legacy inputs in your transaction,
/// you should remove 1WU from each input's `max_weight_to_satisfy`
/// for a more accurate estimate.
///
/// Assumes all ECDSA signatures are 73 bytes, including push opcode and
/// sighash suffix.
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
pub fn max_weight_to_satisfy(&self) -> Result<usize, Error> {
let scriptsig_size = self.ms.max_satisfaction_size()?;
// scriptSig varint difference between non-satisfied (0) and satisfied
let scriptsig_varint_diff = varint_len(scriptsig_size) - varint_len(0);
Ok(4 * (scriptsig_varint_diff + scriptsig_size))
}

/// Computes an upper bound on the weight of a satisfying witness to the
/// transaction.
///
Expand All @@ -65,6 +85,7 @@ impl<Pk: MiniscriptKey> Bare<Pk> {
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
#[deprecated(note = "use max_weight_to_satisfy instead")]
pub fn max_satisfaction_weight(&self) -> Result<usize, Error> {
let scriptsig_len = self.ms.max_satisfaction_size()?;
Ok(4 * (varint_len(scriptsig_len) + scriptsig_len))
Expand Down Expand Up @@ -202,6 +223,27 @@ impl<Pk: MiniscriptKey> Pkh<Pk> {
self.pk
}

/// Computes an upper bound on the difference between a non-satisfied
/// `TxIn`'s `segwit_weight` and a satisfied `TxIn`'s `segwit_weight`
///
/// Since this method uses `segwit_weight` instead of `legacy_weight`,
/// if you want to include only legacy inputs in your transaction,
/// you should remove 1WU from each input's `max_weight_to_satisfy`
/// for a more accurate estimate.
///
/// Assumes all ECDSA signatures are 73 bytes, including push opcode and
/// sighash suffix.
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
pub fn max_weight_to_satisfy(&self) -> usize {
// OP_72 + <sig(71)+sigHash(1)> + OP_33 + <pubkey>
let scriptsig_size = 73 + BareCtx::pk_len(&self.pk);
// scriptSig varint different between non-satisfied (0) and satisfied
let scriptsig_varint_diff = varint_len(scriptsig_size) - varint_len(0);
4 * (scriptsig_varint_diff + scriptsig_size)
}

/// Computes an upper bound on the weight of a satisfying witness to the
/// transaction.
///
Expand Down
52 changes: 52 additions & 0 deletions src/descriptor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,56 @@ impl<Pk: MiniscriptKey> Descriptor<Pk> {
}
}

/// Computes an upper bound on the difference between a non-satisfied
/// `TxIn`'s `segwit_weight` and a satisfied `TxIn`'s `segwit_weight`
///
/// Since this method uses `segwit_weight` instead of `legacy_weight`,
/// if you want to include only legacy inputs in your transaction,
/// you should remove 1WU from each input's `max_weight_to_satisfy`
/// for a more accurate estimate.
///
/// In other words, for segwit inputs or legacy inputs included in
/// segwit transactions, the following will hold for each input if
/// that input was satisfied with the largest possible witness:
/// ```ignore
/// for i in 0..transaction.input.len() {
/// assert_eq!(
/// descriptor_for_input[i].max_weight_to_satisfy(),
/// transaction.input[i].segwit_weight() - Txin::default().segwit_weight()
/// );
/// }
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you need specify something like this:

/// In other words, for segwit inputs or legacy inputs included in
/// segwit transactions, the following will hold for each input if that input was satisfied with the largest possible witness.

It would be nice to have some tests against random descriptors that checks this does actually hold. Do we have a method of doing proptests like this already?

Copy link
Contributor

@danielabrozzoni danielabrozzoni Dec 18, 2022

Choose a reason for hiding this comment

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

Thanks, updated!

It would be nice to have some tests against random descriptors that checks this does actually hold.

I agree that it would be nice to have some tests, but unfortunately we have to wait for the newest rust-bitcoin to be released and for rust-miniscript to be updated before being able to write any.

Do we have a method of doing proptests like this already?

I'm not entirely sure, I guess I could expand tests/test_desc.rs

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah right. Tests for this equality will have to wait I guess.

Copy link
Member

Choose a reason for hiding this comment

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

Re proptests, we have a foray into Kani in rust-bitcoin rust-bitcoin/rust-bitcoin#1415 but I doubt Kani (which does model-checking using a SMT solver) could handle stuff here, unfortunately.

/// ```
///
/// Instead, for legacy transactions, the following will hold for each input
/// if that input was satisfied with the largest possible witness:
/// ```ignore
/// for i in 0..transaction.input.len() {
/// assert_eq!(
/// descriptor_for_input[i].max_weight_to_satisfy(),
/// transaction.input[i].legacy_weight() - Txin::default().legacy_weight()
/// );
/// }
/// ```
///
/// Assumes all ECDSA signatures are 73 bytes, including push opcode and
/// sighash suffix.
/// Assumes all Schnorr signatures are 66 bytes, including push opcode and
/// sighash suffix.
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
pub fn max_weight_to_satisfy(&self) -> Result<usize, Error> {
let weight = match *self {
Descriptor::Bare(ref bare) => bare.max_weight_to_satisfy()?,
Descriptor::Pkh(ref pkh) => pkh.max_weight_to_satisfy(),
Descriptor::Wpkh(ref wpkh) => wpkh.max_weight_to_satisfy(),
Descriptor::Wsh(ref wsh) => wsh.max_weight_to_satisfy()?,
Descriptor::Sh(ref sh) => sh.max_weight_to_satisfy()?,
Descriptor::Tr(ref tr) => tr.max_weight_to_satisfy()?,
};
Ok(weight)
}

/// Computes an upper bound on the weight of a satisfying witness to the
/// transaction.
///
Expand All @@ -316,6 +366,8 @@ impl<Pk: MiniscriptKey> Descriptor<Pk> {
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
#[deprecated(note = "use max_weight_to_satisfy instead")]
#[allow(deprecated)]
pub fn max_satisfaction_weight(&self) -> Result<usize, Error> {
let weight = match *self {
Descriptor::Bare(ref bare) => bare.max_satisfaction_weight()?,
Expand Down
42 changes: 42 additions & 0 deletions src/descriptor/segwitv0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,34 @@ impl<Pk: MiniscriptKey> Wsh<Pk> {
Ok(())
}

/// Computes an upper bound on the difference between a non-satisfied
/// `TxIn`'s `segwit_weight` and a satisfied `TxIn`'s `segwit_weight`
///
/// Assumes all ECDSA signatures are 73 bytes, including push opcode and
/// sighash suffix.
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
pub fn max_weight_to_satisfy(&self) -> Result<usize, Error> {
let (redeem_script_size, max_sat_elems, max_sat_size) = match self.inner {
WshInner::SortedMulti(ref smv) => (
smv.script_size(),
smv.max_satisfaction_witness_elements(),
smv.max_satisfaction_size(),
),
WshInner::Ms(ref ms) => (
ms.script_size(),
ms.max_satisfaction_witness_elements()?,
ms.max_satisfaction_size()?,
),
};
// stack size varint difference between non-satisfied (0) and satisfied
// `max_sat_elems` is inclusive of the "witness script" (redeem script)
let stack_varint_diff = varint_len(max_sat_elems) - varint_len(0);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If max_satisfaction_witness_elements does not include the "redeem script" item (which I assume it doesn't...

Suggested change
let stack_varint_diff = varint_len(max_sat_elems) - varint_len(0);
let stack_varint_diff = varint_len(max_sat_elems + 1) - varint_len(0);

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like max_satisfaction_witness_elements includes the redeem script, as the docs say "Maximum number of witness elements used to satisfy the Miniscript fragment, including the witness script itself."


Ok(stack_varint_diff + varint_len(redeem_script_size) + redeem_script_size + max_sat_size)
}

/// Computes an upper bound on the weight of a satisfying witness to the
/// transaction.
///
Expand All @@ -81,6 +109,7 @@ impl<Pk: MiniscriptKey> Wsh<Pk> {
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
#[deprecated(note = "use max_weight_to_satisfy instead")]
pub fn max_satisfaction_weight(&self) -> Result<usize, Error> {
let (script_size, max_sat_elems, max_sat_size) = match self.inner {
WshInner::SortedMulti(ref smv) => (
Expand Down Expand Up @@ -315,6 +344,19 @@ impl<Pk: MiniscriptKey> Wpkh<Pk> {
}
}

/// Computes an upper bound on the difference between a non-satisfied
/// `TxIn`'s `segwit_weight` and a satisfied `TxIn`'s `segwit_weight`
///
/// Assumes all ec-signatures are 73 bytes, including push opcode and
/// sighash suffix.
pub fn max_weight_to_satisfy(&self) -> usize {
// stack items: <varint(sig+sigHash)> <sig(71)+sigHash(1)> <varint(pubkey)> <pubkey>
let stack_items_size = 73 + Segwitv0::pk_len(&self.pk);
// stackLen varint difference between non-satisfied (0) and satisfied
let stack_varint_diff = varint_len(2) - varint_len(0);
stack_varint_diff + stack_items_size
}

/// Computes an upper bound on the weight of a satisfying witness to the
/// transaction.
///
Expand Down
53 changes: 52 additions & 1 deletion src/descriptor/sh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,66 @@ impl<Pk: MiniscriptKey> Sh<Pk> {
}
}

/// Computes an upper bound on the difference between a non-satisfied
/// `TxIn`'s `segwit_weight` and a satisfied `TxIn`'s `segwit_weight`
///
/// Since this method uses `segwit_weight` instead of `legacy_weight`,
/// if you want to include only legacy inputs in your transaction,
/// you should remove 1WU from each input's `max_weight_to_satisfy`
/// for a more accurate estimate.
///
/// Assumes all ec-signatures are 73 bytes, including push opcode and
/// sighash suffix.
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
pub fn max_weight_to_satisfy(&self) -> Result<usize, Error> {
let (scriptsig_size, witness_size) = match self.inner {
// add weighted script sig, len byte stays the same
ShInner::Wsh(ref wsh) => {
// scriptSig: OP_34 <OP_0 OP_32 <32-byte-hash>>
let scriptsig_size = 1 + 1 + 1 + 32;
let witness_size = wsh.max_weight_to_satisfy()?;
(scriptsig_size, witness_size)
}
ShInner::SortedMulti(ref smv) => {
let ss = smv.script_size();
let ps = push_opcode_size(ss);
let scriptsig_size = ps + ss + smv.max_satisfaction_size();
(scriptsig_size, 0)
}
// add weighted script sig, len byte stays the same
ShInner::Wpkh(ref wpkh) => {
// scriptSig: OP_22 <OP_0 OP_20 <20-byte-hash>>
let scriptsig_size = 1 + 1 + 1 + 20;
let witness_size = wpkh.max_weight_to_satisfy();
(scriptsig_size, witness_size)
}
ShInner::Ms(ref ms) => {
let ss = ms.script_size();
let ps = push_opcode_size(ss);
let scriptsig_size = ps + ss + ms.max_satisfaction_size()?;
(scriptsig_size, 0)
}
};

// scriptSigLen varint difference between non-satisfied (0) and satisfied
let scriptsig_varint_diff = varint_len(scriptsig_size) - varint_len(0);

Ok(4 * (scriptsig_varint_diff + scriptsig_size) + witness_size)
}

/// Computes an upper bound on the weight of a satisfying witness to the
/// transaction.
///
/// Assumes all ec-signatures are 73 bytes, including push opcode and
/// Assumes all ECDSA signatures are 73 bytes, including push opcode and
/// sighash suffix. Includes the weight of the VarInts encoding the
/// scriptSig and witness stack length.
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
#[deprecated(note = "use max_weight_to_satisfy instead")]
#[allow(deprecated)]
pub fn max_satisfaction_weight(&self) -> Result<usize, Error> {
Ok(match self.inner {
// add weighted script sig, len byte stays the same
Expand Down
50 changes: 50 additions & 0 deletions src/descriptor/tr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,55 @@ impl<Pk: MiniscriptKey> Tr<Pk> {
Ok(())
}

/// Computes an upper bound on the difference between a non-satisfied
/// `TxIn`'s `segwit_weight` and a satisfied `TxIn`'s `segwit_weight`
///
/// Assumes all Schnorr signatures are 66 bytes, including push opcode and
/// sighash suffix.
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
pub fn max_weight_to_satisfy(&self) -> Result<usize, Error> {
let tree = match self.taptree() {
None => {
// key spend path
// item: varint(sig+sigHash) + <sig(64)+sigHash(1)>
let item_sig_size = 1 + 65;
// 1 stack item
let stack_varint_diff = varint_len(1) - varint_len(0);

return Ok(stack_varint_diff + item_sig_size);
}
// script path spend..
Some(tree) => tree,
};

tree.iter()
.filter_map(|(depth, ms)| {
let script_size = ms.script_size();
let max_sat_elems = ms.max_satisfaction_witness_elements().ok()?;
let max_sat_size = ms.max_satisfaction_size().ok()?;
let control_block_size = control_block_len(depth);

// stack varint difference (+1 for ctrl block, witness script already included)
let stack_varint_diff = varint_len(max_sat_elems + 1) - varint_len(0);

Some(
stack_varint_diff +
// size of elements to satisfy script
max_sat_size +
// second to last element: script
varint_len(script_size) +
script_size +
// last element: control block
varint_len(control_block_size) +
control_block_size,
)
})
.max()
.ok_or(Error::ImpossibleSatisfaction)
}

/// Computes an upper bound on the weight of a satisfying witness to the
/// transaction.
///
Expand All @@ -256,6 +305,7 @@ impl<Pk: MiniscriptKey> Tr<Pk> {
///
/// # Errors
/// When the descriptor is impossible to safisfy (ex: sh(OP_FALSE)).
#[deprecated(note = "use max_weight_to_satisfy instead")]
pub fn max_satisfaction_weight(&self) -> Result<usize, Error> {
let tree = match self.taptree() {
// key spend path:
Expand Down
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@
//! assert!(desc.sanity_check().is_ok());
//!
//! // Estimate the satisfaction cost.
//! assert_eq!(desc.max_satisfaction_weight().unwrap(), 293);
//! // scriptSig: OP_PUSH34 <OP_0 OP_32 <32-byte-hash>>
//! // = (1 + 1 + 1 + 32) * 4 = 140 WU
//! // redeemScript: varint <OP_33 <pk1> OP_CHECKSIG OP_IFDUP OP_NOTIF OP_33 <pk2> OP_CHECKSIG OP_ENDIF>
//! // = 1 + (1 + 33 + 1 + 1 + 1 + 1 + 33 + 1 + 1) = 74 WU
//! // stackItem[Sig]: varint <sig+sighash>
//! // = 1 + 73 = 74 WU
//! // Expected satisfaction weight: 140 + 74 + 74 = 288
//! assert_eq!(desc.max_weight_to_satisfy().unwrap(), 288);
//! ```
//!

Expand Down