Merge rust-bitcoin/rust-bitcoin#2073: Feature: Count sigops for Transaction
158ba26a8a
Feature: Count sigops for Transaction (junderw) Pull request description: I copied over the sigop counting logic from Bitcoin Core, but I made a few adjustments. 1. I removed 2 consensus flags that checked for P2SH and SegWit activation. This code assumes both are activated. If we were to include that, what would be a good way to go about it? (ie. If I run this method on a transaction from the 1000th block and it just so happened to have a P2SH-like input, Bitcoin Core wouldn't accidentally count those sigops because the consensus flag will stop them from running the P2SH logic. Same goes for SegWit) 3. Since there's no guarantee that we have an index from which we can get the prevout scripts, I made it into a generic closure that looks up the prevout script for us. If the caller doesn't provide it, We can only count sigops directly in the scriptSig and scriptPubkey (no P2SH or SegWit). ## TODO - [x] Write tests for transaction sigop counting ~~Edit: The test changes are just to get the 1.48 tests passing. I'll remove them and replace them with whatever solution that is agreed upon in another PR etc.~~ Edit 2: This is the code I used as a guide:8105bce5b3/src/consensus/tx_verify.cpp (L147-L166)
Edit 3: I found a subtle bug in the implementation of `count_sigops` (https://github.com/rust-bitcoin/rust-bitcoin/pull/2073#issuecomment-1722403687) ACKs for top commit: apoelstra: ACK158ba26a8a
tcharding: ACK158ba26a8a
Tree-SHA512: 2b8a0c50b9390bfb914da1ba687e8599b957c75c511f764a2f3ed3414580150ce3aa2ac7aed97a4f7587d3fbeece269444c65c7449b88f1bdb02e573e6f6febd
This commit is contained in:
commit
141d805ddc
|
@ -9,6 +9,7 @@ use core::ops::{Index, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, Ran
|
|||
use hashes::Hash;
|
||||
use secp256k1::{Secp256k1, Verification};
|
||||
|
||||
use super::PushBytes;
|
||||
use crate::blockdata::opcodes::all::*;
|
||||
use crate::blockdata::opcodes::{self, Opcode};
|
||||
use crate::blockdata::script::witness_version::WitnessVersion;
|
||||
|
@ -207,6 +208,25 @@ impl Script {
|
|||
&& self.0[24] == OP_CHECKSIG.to_u8()
|
||||
}
|
||||
|
||||
/// Checks whether a script is push only.
|
||||
///
|
||||
/// Note: `OP_RESERVED` (`0x50`) and all the OP_PUSHNUM operations
|
||||
/// are considered push operations.
|
||||
#[inline]
|
||||
pub fn is_push_only(&self) -> bool {
|
||||
for inst in self.instructions() {
|
||||
match inst {
|
||||
Err(_) => return false,
|
||||
Ok(Instruction::PushBytes(_)) => {}
|
||||
Ok(Instruction::Op(op)) if op.to_u8() <= 0x60 => {}
|
||||
// From Bitcoin Core
|
||||
// if (opcode > OP_PUSHNUM_16 (0x60)) return false
|
||||
Ok(Instruction::Op(_)) => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Checks whether a script pubkey is a P2PK output.
|
||||
///
|
||||
/// You can obtain the public key, if its valid,
|
||||
|
@ -459,6 +479,7 @@ impl Script {
|
|||
match inst {
|
||||
Ok(Instruction::Op(opcode)) => {
|
||||
match opcode {
|
||||
// p2pk, p2pkh
|
||||
OP_CHECKSIG | OP_CHECKSIGVERIFY => {
|
||||
n += 1;
|
||||
}
|
||||
|
@ -567,6 +588,29 @@ impl Script {
|
|||
}
|
||||
}
|
||||
|
||||
/// Iterates the script to find the last pushdata.
|
||||
///
|
||||
/// Returns `None` if the instruction is an opcode or if the script is empty.
|
||||
pub(crate) fn last_pushdata(&self) -> Option<Push> {
|
||||
match self.instructions().last() {
|
||||
// Handles op codes up to (but excluding) OP_PUSHNUM_NEG.
|
||||
Some(Ok(Instruction::PushBytes(bytes))) => Some(Push::Data(bytes)),
|
||||
// OP_16 (0x60) and lower are considered "pushes" by Bitcoin Core (excl. OP_RESERVED).
|
||||
// By here we know that op is between OP_PUSHNUM_NEG AND OP_PUSHNUM_16 inclusive.
|
||||
Some(Ok(Instruction::Op(op))) if op.to_u8() <= 0x60 => {
|
||||
if op == OP_PUSHNUM_NEG1 {
|
||||
Some(Push::Num(-1))
|
||||
} else if op == OP_RESERVED {
|
||||
Some(Push::Reserved)
|
||||
} else {
|
||||
let num = (op.to_u8() - 0x50) as i8; // cast ok, num is [1, 16].
|
||||
Some(Push::Num(num))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a [`Box<Script>`](Box) into a [`ScriptBuf`] without copying or allocating.
|
||||
#[must_use = "`self` will be dropped if the result is not used"]
|
||||
pub fn into_script_buf(self: Box<Self>) -> ScriptBuf {
|
||||
|
@ -580,6 +624,19 @@ impl Script {
|
|||
}
|
||||
}
|
||||
|
||||
/// Data pushed by "push" opcodes.
|
||||
///
|
||||
/// "push" opcodes are defined by Bitcoin Core as OP_PUSHBYTES_, OP_PUSHDATA, OP_PUSHNUM_, and
|
||||
/// OP_RESERVED i.e., everything less than OP_PUSHNUM_16 (0x60) . (TODO: Add link to core code).
|
||||
pub(crate) enum Push<'a> {
|
||||
/// All the OP_PUSHBYTES_ and OP_PUSHDATA_ opcodes.
|
||||
Data(&'a PushBytes),
|
||||
/// All the OP_PUSHNUM_ opcodes (-1, 1, 2, .., 16)
|
||||
Num(i8),
|
||||
/// OP_RESERVED
|
||||
Reserved,
|
||||
}
|
||||
|
||||
/// Iterator over bytes of a script
|
||||
pub struct Bytes<'a>(core::iter::Copied<core::slice::Iter<'a, u8>>);
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ use internals::write_err;
|
|||
use super::Weight;
|
||||
use crate::blockdata::locktime::absolute::{self, Height, Time};
|
||||
use crate::blockdata::locktime::relative;
|
||||
use crate::blockdata::script::ScriptBuf;
|
||||
use crate::blockdata::script::{Script, ScriptBuf};
|
||||
use crate::blockdata::witness::Witness;
|
||||
#[cfg(feature = "bitcoinconsensus")]
|
||||
pub use crate::consensus::validation::TxVerifyError;
|
||||
|
@ -30,6 +30,7 @@ use crate::hash_types::{Txid, Wtxid};
|
|||
use crate::internal_macros::impl_consensus_encoding;
|
||||
use crate::parse::impl_parse_str_from_int_infallible;
|
||||
use crate::prelude::*;
|
||||
use crate::script::Push;
|
||||
#[cfg(doc)]
|
||||
use crate::sighash::{EcdsaSighashType, TapSighashType};
|
||||
use crate::string::FromHexStr;
|
||||
|
@ -833,6 +834,122 @@ impl Transaction {
|
|||
pub fn script_pubkey_lens(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
self.output.iter().map(|txout| txout.script_pubkey.len())
|
||||
}
|
||||
|
||||
/// Counts the total number of sigops.
|
||||
///
|
||||
/// This value is for pre-taproot transactions only.
|
||||
///
|
||||
/// > In taproot, a different mechanism is used. Instead of having a global per-block limit,
|
||||
/// > there is a per-transaction-input limit, proportional to the size of that input.
|
||||
/// > ref: <https://bitcoin.stackexchange.com/questions/117356/what-is-sigop-signature-operation#117359>
|
||||
///
|
||||
/// The `spent` parameter is a closure/function that looks up the output being spent by each input
|
||||
/// It takes in an [`OutPoint`] and returns a [`TxOut`]. If you can't provide this, a placeholder of
|
||||
/// `|_| None` can be used. Without access to the previous [`TxOut`], any sigops in a redeemScript (P2SH)
|
||||
/// as well as any segwit sigops will not be counted for that input.
|
||||
pub fn total_sigop_cost<S>(&self, mut spent: S) -> usize
|
||||
where
|
||||
S: FnMut(&OutPoint) -> Option<TxOut>,
|
||||
{
|
||||
let mut cost = self.count_p2pk_p2pkh_sigops().saturating_mul(4);
|
||||
|
||||
// coinbase tx is correctly handled because `spent` will always returns None.
|
||||
cost = cost.saturating_add(self.count_p2sh_sigops(&mut spent).saturating_mul(4));
|
||||
cost.saturating_add(self.count_witness_sigops(&mut spent))
|
||||
}
|
||||
|
||||
/// Gets the sigop count.
|
||||
///
|
||||
/// Counts sigops for this transaction's input scriptSigs and output scriptPubkeys i.e., doesn't
|
||||
/// count sigops in the redeemScript for p2sh or the sigops in the witness (use
|
||||
/// `count_p2sh_sigops` and `count_witness_sigops` respectively).
|
||||
fn count_p2pk_p2pkh_sigops(&self) -> usize {
|
||||
let mut count: usize = 0;
|
||||
for input in &self.input {
|
||||
// 0 for p2wpkh, p2wsh, and p2sh (including wrapped segwit).
|
||||
count = count.saturating_add(input.script_sig.count_sigops_legacy());
|
||||
}
|
||||
for output in &self.output {
|
||||
count = count.saturating_add(output.script_pubkey.count_sigops_legacy());
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Does not include wrapped segwit (see `count_witness_sigops`).
|
||||
fn count_p2sh_sigops<S>(&self, spent: &mut S) -> usize
|
||||
where
|
||||
S: FnMut(&OutPoint) -> Option<TxOut>,
|
||||
{
|
||||
fn count_sigops(prevout: &TxOut, input: &TxIn) -> usize {
|
||||
let mut count: usize = 0;
|
||||
if prevout.script_pubkey.is_p2sh() {
|
||||
if let Some(Push::Data(redeem)) = input.script_sig.last_pushdata() {
|
||||
count =
|
||||
count.saturating_add(Script::from_bytes(redeem.as_bytes()).count_sigops());
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
let mut count: usize = 0;
|
||||
for input in &self.input {
|
||||
if let Some(prevout) = spent(&input.previous_output) {
|
||||
count = count.saturating_add(count_sigops(&prevout, input));
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Includes wrapped segwit (returns 0 for taproot spends).
|
||||
fn count_witness_sigops<S>(&self, spent: &mut S) -> usize
|
||||
where
|
||||
S: FnMut(&OutPoint) -> Option<TxOut>,
|
||||
{
|
||||
fn count_sigops_with_witness_program(witness: &Witness, witness_program: &Script) -> usize {
|
||||
if witness_program.is_p2wpkh() {
|
||||
1
|
||||
} else if witness_program.is_p2wsh() {
|
||||
// Treat the last item of the witness as the witnessScript
|
||||
return witness
|
||||
.last()
|
||||
.map(Script::from_bytes)
|
||||
.map(|s| s.count_sigops())
|
||||
.unwrap_or(0);
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn count_sigops(prevout: TxOut, input: &TxIn) -> usize {
|
||||
let script_sig = &input.script_sig;
|
||||
let witness = &input.witness;
|
||||
|
||||
let witness_program = if prevout.script_pubkey.is_witness_program() {
|
||||
&prevout.script_pubkey
|
||||
} else if prevout.script_pubkey.is_p2sh() && script_sig.is_push_only() {
|
||||
// If prevout is P2SH and scriptSig is push only
|
||||
// then we wrap the last push (redeemScript) in a Script
|
||||
if let Some(Push::Data(push_bytes)) = script_sig.last_pushdata() {
|
||||
Script::from_bytes(push_bytes.as_bytes())
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// This will return 0 if the redeemScript wasn't a witness program
|
||||
count_sigops_with_witness_program(witness, witness_program)
|
||||
}
|
||||
|
||||
let mut count: usize = 0;
|
||||
for input in &self.input {
|
||||
if let Some(prevout) = spent(&input.previous_output) {
|
||||
count = count.saturating_add(count_sigops(prevout, input));
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
impl_consensus_encoding!(TxOut, value, script_pubkey);
|
||||
|
@ -1803,6 +1920,154 @@ mod tests {
|
|||
assert_eq!(tx.weight(), *expected_weight);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tx_sigop_count() {
|
||||
let tx_hexes = [
|
||||
// 0 sigops (p2pkh in + p2wpkh out)
|
||||
(
|
||||
"0200000001725aab4d23f76ad10bb569a68f8702ebfb8b076e015179ff9b9425234953\
|
||||
ac63000000006a47304402204cae7dc9bb68b588dd6b8afb8b881b752fd65178c25693e\
|
||||
a6d5d9a08388fd2a2022011c753d522d5c327741a6d922342c86e05c928309d7e566f68\
|
||||
8148432e887028012103f14b11cfb58b113716e0fa277ab4a32e4d3ed64c6b09b1747ef\
|
||||
7c828d5b06a94fdffffff01e5d4830100000000160014e98527b55cae861e5b9c3a6794\
|
||||
86514c012d6fce00000000",
|
||||
0, // Expected (Some)
|
||||
return_none as fn(&OutPoint) -> Option<TxOut>, // spent fn
|
||||
0, // Expected (None)
|
||||
),
|
||||
// 5 sigops (p2wpkh in + p2pkh out (x4))
|
||||
(
|
||||
"020000000001018c47330b1c4d30e7e2244e8ccb56d411b71e10073bb42fa1813f3f01\
|
||||
e144cc4d0100000000fdffffff01f7e30300000000001976a9143b49fd16f7562cfeedc\
|
||||
6a4ba84805f8c2f8e1a2c88ac024830450221009a4dbf077a63f6e4c3628a5fef2a09ec\
|
||||
6f7ca4a4d95bc8bb69195b6b671e9272022074da9ffff5a677fc7b37d66bb4ff1f316c9\
|
||||
dbacb92058291d84cd4b83f7c63c9012103d013e9e53c9ca8dd2ddffab1e9df27811503\
|
||||
feea7eb0700ff058851bbb37d99000000000",
|
||||
5,
|
||||
return_p2wpkh,
|
||||
4,
|
||||
),
|
||||
// 8 sigops (P2WSH 3-of-4 MS (4) in + P2WSH out + P2PKH out (1x4))
|
||||
(
|
||||
"01000000000101e70d7b4d957122909a665070b0c5bbb693982d09e4e66b9e6b7a8390\
|
||||
ce65ef1f0100000000ffffffff02095f2b0000000000220020800a016ea57a08f30c273\
|
||||
ae7624f8f91c505ccbd3043829349533f317168248c52594500000000001976a914607f\
|
||||
643372477c044c6d40b814288e40832a602688ac05004730440220282943649e687b5a3\
|
||||
bda9403c16f363c2ee2be0ec43fb8df40a08b96a4367d47022014e8f36938eef41a09ee\
|
||||
d77a815b0fa120a35f25e3a185310f050959420cee360147304402201e555f894036dd5\
|
||||
78045701e03bf10e093d7e93cd9997e44c1fc65a7b669852302206893f7261e52c9d779\
|
||||
5ba39d99aad30663da43ed675c389542805469fa8eb26a014730440220510fc99bc37d6\
|
||||
dbfa7e8724f4802cebdb17b012aaf70ce625e22e6158b139f40022022e9b811751d491f\
|
||||
bdec7691b697e88ba84315f6739b9e3bd4425ac40563aed2018b5321029ddecf0cc2013\
|
||||
514961550e981a0b8b60e7952f70561a5bb552aa7f075e71e3c2103316195a59c35a3b2\
|
||||
7b6dfcc3192cc10a7a6bbccd5658dfbe98ca62a13d6a02c121034629d906165742def4e\
|
||||
f53c6dade5dcbf88b775774cad151e35ae8285e613b0221035826a29938de2076950811\
|
||||
13c58bcf61fe6adacc3aacceb21c4827765781572d54ae00000000",
|
||||
8,
|
||||
return_p2wsh,
|
||||
4,
|
||||
),
|
||||
// 5 sigops (P2SH-P2WPKH in (1), 2 P2SH outs (0), 1 P2PKH out (1x4))
|
||||
(
|
||||
"010000000001018aec7e0729ba5a2d284303c89b3f397e92d54472a225d28eb0ae2fa6\
|
||||
5a7d1a2e02000000171600145ad5db65f313ab76726eb178c2fd8f21f977838dfdfffff\
|
||||
f03102700000000000017a914dca89e03ba124c2c70e55533f91100f2d9dab04587f2d7\
|
||||
1d00000000001976a91442a34f4b0a65bc81278b665d37fd15910d261ec588ac292c3b0\
|
||||
00000000017a91461978dcebd0db2da0235c1ba3e8087f9fd74c57f8702473044022000\
|
||||
9226f8def30a8ffa53e55ca5d71a72a64cd20ae7f3112562e3413bd0731d2c0220360d2\
|
||||
20435e67eef7f2bf0258d1dded706e3824f06d961ba9eeaed300b16c2cc012103180cff\
|
||||
753d3e4ee1aa72b2b0fd72ce75956d04f4c19400a3daed0b18c3ab831e00000000",
|
||||
5,
|
||||
return_p2sh,
|
||||
4,
|
||||
),
|
||||
// 12 sigops (1 P2SH 2-of-3 MS in (3x4), P2SH outs (0))
|
||||
(
|
||||
"010000000115fe9ec3dc964e41f5267ea26cfe505f202bf3b292627496b04bece84da9\
|
||||
b18903000000fc004730440220442827f1085364bda58c5884cee7b289934083362db6d\
|
||||
fb627dc46f6cdbf5793022078cfa524252c381f2a572f0c41486e2838ca94aa268f2384\
|
||||
d0e515744bf0e1e9014730440220160e49536bb29a49c7626744ee83150174c22fa40d5\
|
||||
8fb4cd554a907a6a7b825022045f6cf148504b334064686795f0968c689e542f475b8ef\
|
||||
5a5fa42383948226a3014c69522103e54bc61efbcb8eeff3a5ab2a92a75272f5f6820e3\
|
||||
8e3d28edb54beb06b86c0862103a553e30733d7a8df6d390d59cc136e2c9d9cf4e808f3\
|
||||
b6ab009beae68dd60822210291c5a54bb8b00b6f72b90af0ac0ecaf78fab026d8eded28\
|
||||
2ad95d4d65db268c953aeffffffff024c4f0d000000000017a9146ebf0484bd5053f727\
|
||||
c755a750aa4c815dfa112887a06b12020000000017a91410065dd50b3a7f299fef3b1c5\
|
||||
3b8216399916ab08700000000",
|
||||
12,
|
||||
return_p2sh,
|
||||
0,
|
||||
),
|
||||
// 3 sigops (1 P2SH-P2WSH 2-of-3 MS in (3), P2SH + P2WSH outs (0))
|
||||
(
|
||||
"0100000000010117a31277a8ba3957be351fe4cffd080e05e07f9ee1594d638f55dd7d\
|
||||
707a983c01000000232200203a33fc9628c29f36a492d9fd811fd20231fbd563f7863e7\
|
||||
9c4dc0ed34ea84b15ffffffff033bed03000000000017a914fb00d9a49663fd8ae84339\
|
||||
8ae81299a1941fb8d287429404000000000017a9148fe08d81882a339cf913281eca8af\
|
||||
39110507c798751ab1300000000002200208819e4bac0109b659de6b9168b83238a050b\
|
||||
ef16278e470083b39d28d2aa5a6904004830450221009faf81f72ec9b14a39f0f0e12f0\
|
||||
1a7175a4fe3239cd9a015ff2085985a9b0e3f022059e1aaf96c9282298bdc9968a46d8a\
|
||||
d28e7299799835cf982b02c35e217caeae0147304402202b1875355ee751e0c8b21990b\
|
||||
7ea73bd84dfd3bd17477b40fc96552acba306ad02204913bc43acf02821a3403132aa0c\
|
||||
33ac1c018d64a119f6cb55dfb8f408d997ef01695221023c15bf3436c0b4089e0ed0428\
|
||||
5101983199d0967bd6682d278821c1e2ac3583621034d924ccabac6d190ce8343829834\
|
||||
cac737aa65a9abe521bcccdcc3882d97481f21035d01d092bb0ebcb793ba3ffa0aeb143\
|
||||
2868f5277d5d3d2a7d2bc1359ec13abbd53aee1560c00",
|
||||
3,
|
||||
return_p2sh,
|
||||
0,
|
||||
),
|
||||
// 80 sigops (1 P2PKH ins (0), 1 BARE MS outs (20x4))
|
||||
(
|
||||
"0100000001628c1726fecd23331ae9ff2872341b82d2c03180aa64f9bceefe457448db\
|
||||
e579020000006a47304402204799581a5b34ae5adca21ef22c55dbfcee58527127c95d0\
|
||||
1413820fe7556ed970220391565b24dc47ce57fe56bf029792f821a392cdb5a3d45ed85\
|
||||
c158997e7421390121037b2fb5b602e51c493acf4bf2d2423bcf63a09b3b99dfb7bd3c8\
|
||||
d74733b5d66f5ffffffff011c0300000000000069512103a29472a1848105b2225f0eca\
|
||||
5c35ada0b0abbc3c538818a53eca177f4f4dcd9621020c8fd41b65ae6b980c072c5a9f3\
|
||||
aec9f82162c92eb4c51d914348f4390ac39122102222222222222222222222222222222\
|
||||
222222222222222222222222222222222253ae00000000",
|
||||
80,
|
||||
return_none,
|
||||
80,
|
||||
),
|
||||
];
|
||||
|
||||
// All we need is to trigger 3 cases for prevout
|
||||
fn return_p2sh(_outpoint: &OutPoint) -> Option<TxOut> {
|
||||
Some(
|
||||
deserialize(&hex!(
|
||||
"cc721b000000000017a91428203c10cc8f18a77412caaa83dabaf62b8fbb0f87"
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
fn return_p2wpkh(_outpoint: &OutPoint) -> Option<TxOut> {
|
||||
Some(
|
||||
deserialize(&hex!(
|
||||
"e695779d000000001600141c6977423aa4b82a0d7f8496cdf3fc2f8b4f580c"
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
fn return_p2wsh(_outpoint: &OutPoint) -> Option<TxOut> {
|
||||
Some(
|
||||
deserialize(&hex!(
|
||||
"66b51e0900000000220020dbd6c9d5141617eff823176aa226eb69153c1e31334ac37469251a2539fc5c2b"
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
fn return_none(_outpoint: &OutPoint) -> Option<TxOut> { None }
|
||||
|
||||
for (hx, expected, spent_fn, expected_none) in tx_hexes.iter() {
|
||||
let tx_bytes = hex!(hx);
|
||||
let tx: Transaction = deserialize(&tx_bytes).unwrap();
|
||||
assert_eq!(tx.total_sigop_cost(spent_fn), *expected);
|
||||
assert_eq!(tx.total_sigop_cost(return_none), *expected_none);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(bench)]
|
||||
|
|
Loading…
Reference in New Issue