Merge rust-bitcoin/rust-bitcoin#1890: [script] Add method get_sigop_count

638445f8a9 Feature: Add opcodes::All::decode_pushnum and Script::count_sigops (junderw)

Pull request description:

  Planning to also add methods for the various parts of Transaction etc. to eventually allow for easier sigops calculation.

  Bare multisig is making a comeback, which is causing a large amount of transactions' effective vSizes (for fee calculation) to be dependent on the sigop count.

  This is a first step at making those transactions easier to estimate fees for / template blocks for etc.

ACKs for top commit:
  Kixunil:
    ACK 638445f8a9
  tcharding:
    ACK 638445f8a9

Tree-SHA512: 5e87d0f5ab58ed22ed50e43eac392b9b84ebccab5086553a6234d825766842057ab89bd0753f3c9de50d9ab17536182b8f64a57e8d5632a55494180f2cc26bbd
This commit is contained in:
Andrew Poelstra 2023-06-04 18:26:04 +00:00
commit 1a1fe0e313
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
3 changed files with 169 additions and 0 deletions

View File

@ -411,6 +411,30 @@ impl All {
/// Encodes [`All`] as a byte.
#[inline]
pub const fn to_u8(self) -> u8 { self.code }
/// Encodes PUSHNUM [`All`] as a `u8` representing its number (1-16).
///
/// Does not convert `OP_FALSE` to 0. Only `1` to `OP_PUSHNUM_16` are covered.
///
/// # Returns
///
/// Returns `None` if `self` is not a PUSHNUM.
///
/// # Examples
///
/// ```
/// use bitcoin::opcodes::all::*;
/// assert_eq!(OP_PUSHNUM_5.decode_pushnum().expect("pushnum"), 5)
/// ```
#[inline]
pub const fn decode_pushnum(self) -> Option<u8> {
const START: u8 = OP_PUSHNUM_1.code;
const END: u8 = OP_PUSHNUM_16.code;
match self.code {
START..=END => Some(self.code - START + 1),
_ => None,
}
}
}
impl From<u8> for All {

View File

@ -318,6 +318,75 @@ impl Script {
crate::Amount::from_sat(sats)
}
/// Count the sigops for this Script using accurate counting.
///
/// In Bitcoin Core, there are two ways to count sigops, "accurate" and "legacy".
/// This method uses "accurate" counting. This means that OP_CHECKMULTISIG and its
/// verify variant count for N sigops where N is the number of pubkeys used in the
/// multisig. However, it will count for 20 sigops if CHECKMULTISIG is not preceeded by an
/// OP_PUSHNUM from 1 - 16 (this would be an invalid script)
///
/// Bitcoin Core uses accurate counting for sigops contained within redeemScripts (P2SH)
/// and witnessScripts (P2WSH) only. It uses legacy for sigops in scriptSigs and scriptPubkeys.
///
/// (Note: taproot scripts don't count toward the sigop count of the block,
/// nor do they have CHECKMULTISIG operations. This function does not count OP_CHECKSIGADD,
/// so do not use this to try and estimate if a taproot script goes over the sigop budget.)
pub fn count_sigops(&self) -> Result<usize, super::Error> { self.count_sigops_internal(true) }
/// Count the sigops for this Script using legacy counting.
///
/// In Bitcoin Core, there are two ways to count sigops, "accurate" and "legacy".
/// This method uses "legacy" counting. This means that OP_CHECKMULTISIG and its
/// verify variant count for 20 sigops.
///
/// Bitcoin Core uses legacy counting for sigops contained within scriptSigs and
/// scriptPubkeys. It uses accurate for redeemScripts (P2SH) and witnessScripts (P2WSH).
///
/// (Note: taproot scripts don't count toward the sigop count of the block,
/// nor do they have CHECKMULTISIG operations. This function does not count OP_CHECKSIGADD,
/// so do not use this to try and estimate if a taproot script goes over the sigop budget.)
pub fn count_sigops_legacy(&self) -> Result<usize, super::Error> {
self.count_sigops_internal(false)
}
fn count_sigops_internal(&self, accurate: bool) -> Result<usize, super::Error> {
let mut n = 0;
let mut pushnum_cache = None;
for inst in self.instructions() {
match inst? {
Instruction::Op(opcode) => {
match opcode {
OP_CHECKSIG | OP_CHECKSIGVERIFY => {
n += 1;
}
OP_CHECKMULTISIG | OP_CHECKMULTISIGVERIFY => {
match (accurate, pushnum_cache) {
(true, Some(pushnum)) => {
// Add the number of pubkeys in the multisig as sigop count
n += pushnum as usize;
}
_ => {
// MAX_PUBKEYS_PER_MULTISIG from Bitcoin Core
// https://github.com/bitcoin/bitcoin/blob/v25.0/src/script/script.h#L29-L30
n += 20;
}
}
}
_ => {
pushnum_cache = opcode.decode_pushnum();
}
}
}
Instruction::PushBytes(_) => {
pushnum_cache = None;
}
}
}
Ok(n)
}
/// Iterates over the script instructions.
///
/// Each returned item is a nested enum covering opcodes, datapushes and errors.

View File

@ -607,6 +607,82 @@ fn defult_dust_value_tests() {
assert_eq!(script_p2pkh.dust_value(), crate::Amount::from_sat(546));
}
#[test]
fn test_script_get_sigop_count() {
assert_eq!(
Builder::new()
.push_opcode(OP_DUP)
.push_opcode(OP_HASH160)
.push_slice([42; 20])
.push_opcode(OP_EQUAL)
.into_script()
.count_sigops(),
Ok(0)
);
assert_eq!(
Builder::new()
.push_opcode(OP_DUP)
.push_opcode(OP_HASH160)
.push_slice([42; 20])
.push_opcode(OP_EQUALVERIFY)
.push_opcode(OP_CHECKSIG)
.into_script()
.count_sigops(),
Ok(1)
);
assert_eq!(
Builder::new()
.push_opcode(OP_DUP)
.push_opcode(OP_HASH160)
.push_slice([42; 20])
.push_opcode(OP_EQUALVERIFY)
.push_opcode(OP_CHECKSIGVERIFY)
.push_opcode(OP_PUSHNUM_1)
.into_script()
.count_sigops(),
Ok(1)
);
let multi = Builder::new()
.push_opcode(OP_PUSHNUM_1)
.push_slice([3; 33])
.push_slice([3; 33])
.push_slice([3; 33])
.push_opcode(OP_PUSHNUM_3)
.push_opcode(OP_CHECKMULTISIG)
.into_script();
assert_eq!(multi.count_sigops(), Ok(3));
assert_eq!(multi.count_sigops_legacy(), Ok(20));
let multi_verify = Builder::new()
.push_opcode(OP_PUSHNUM_1)
.push_slice([3; 33])
.push_slice([3; 33])
.push_slice([3; 33])
.push_opcode(OP_PUSHNUM_3)
.push_opcode(OP_CHECKMULTISIGVERIFY)
.push_opcode(OP_PUSHNUM_1)
.into_script();
assert_eq!(multi_verify.count_sigops(), Ok(3));
assert_eq!(multi_verify.count_sigops_legacy(), Ok(20));
let multi_nopushnum_pushdata = Builder::new()
.push_opcode(OP_PUSHNUM_1)
.push_slice([3; 33])
.push_slice([3; 33])
.push_slice([3; 33])
.push_opcode(OP_CHECKMULTISIG)
.into_script();
assert_eq!(multi_nopushnum_pushdata.count_sigops(), Ok(20));
assert_eq!(multi_nopushnum_pushdata.count_sigops_legacy(), Ok(20));
let multi_nopushnum_op = Builder::new()
.push_opcode(OP_PUSHNUM_1)
.push_slice([3; 33])
.push_slice([3; 33])
.push_opcode(OP_DROP)
.push_opcode(OP_CHECKMULTISIG)
.into_script();
assert_eq!(multi_nopushnum_op.count_sigops(), Ok(20));
assert_eq!(multi_nopushnum_op.count_sigops_legacy(), Ok(20));
}
#[test]
#[cfg(feature = "serde")]
fn test_script_serde_human_and_not() {