From 814f671a225be44602d8f3a01db492e37f7dcebb Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 16 Sep 2023 16:38:03 -0700 Subject: [PATCH] Feature: Count sigops on electrs side --- src/chain.rs | 12 +- src/rest.rs | 8 +- src/util/block.rs | 2 +- src/util/mod.rs | 2 +- src/util/transaction.rs | 242 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+), 8 deletions(-) diff --git a/src/chain.rs b/src/chain.rs index 5b8cabf3..de726186 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -1,15 +1,19 @@ #[cfg(not(feature = "liquid"))] // use regular Bitcoin data structures pub use bitcoin::{ - blockdata::script, consensus::deserialize, util::address, Block, BlockHash, BlockHeader, - OutPoint, Script, Transaction, TxIn, TxOut, Txid, + blockdata::{opcodes, script, witness::Witness}, + consensus::deserialize, + hashes, + util::address, + Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxOut, Txid, }; #[cfg(feature = "liquid")] pub use { crate::elements::asset, elements::{ - address, confidential, encode::deserialize, script, Address, AssetId, Block, BlockHash, - BlockHeader, OutPoint, Script, Transaction, TxIn, TxOut, Txid, + address, confidential, encode::deserialize, hashes, opcodes, script, Address, AssetId, + Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxInWitness as Witness, + TxOut, Txid, }, }; diff --git a/src/rest.rs b/src/rest.rs index d25da2e8..48a8a4d5 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -4,8 +4,8 @@ use crate::errors; use crate::new_index::{compute_script_hash, Query, SpendingInput, Utxo}; use crate::util::{ create_socket, electrum_merkle, extract_tx_prevouts, full_hash, get_innerscripts, get_tx_fee, - has_prevout, is_coinbase, BlockHeaderMeta, BlockId, FullHash, ScriptToAddr, ScriptToAsm, - TransactionStatus, + has_prevout, is_coinbase, transaction_sigop_count, BlockHeaderMeta, BlockId, FullHash, + ScriptToAddr, ScriptToAsm, TransactionStatus, }; #[cfg(not(feature = "liquid"))] @@ -144,6 +144,7 @@ struct TransactionValue { vout: Vec, size: u32, weight: u32, + sigops: u32, fee: u64, #[serde(skip_serializing_if = "Option::is_none")] status: Option, @@ -157,6 +158,8 @@ impl TransactionValue { config: &Config, ) -> Result { let prevouts = extract_tx_prevouts(&tx, txos)?; + let sigops = transaction_sigop_count(&tx, &prevouts) + .map_err(|_| errors::Error::from("Couldn't count sigops"))? as u32; let vins: Vec = tx .input @@ -183,6 +186,7 @@ impl TransactionValue { vout: vouts, size: tx.size() as u32, weight: tx.weight() as u32, + sigops, fee, status: Some(TransactionStatus::from(blockid)), }) diff --git a/src/util/block.rs b/src/util/block.rs index 1f6b9850..7292efcc 100644 --- a/src/util/block.rs +++ b/src/util/block.rs @@ -146,7 +146,7 @@ impl HeaderList { + 1 }; (new_height..) - .zip(hashed_headers.into_iter()) + .zip(hashed_headers) .map(|(height, hashed_header)| HeaderEntry { height, hash: hashed_header.blockhash, diff --git a/src/util/mod.rs b/src/util/mod.rs index 65ab2eed..1225c7a7 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -11,7 +11,7 @@ pub use self::fees::get_tx_fee; pub use self::script::{get_innerscripts, ScriptToAddr, ScriptToAsm}; pub use self::transaction::{ extract_tx_prevouts, has_prevout, is_coinbase, is_spendable, serialize_outpoint, - TransactionStatus, TxInput, + sigops::transaction_sigop_count, TransactionStatus, TxInput, }; use std::collections::HashMap; diff --git a/src/util/transaction.rs b/src/util/transaction.rs index 5a4aacf1..db9ba066 100644 --- a/src/util/transaction.rs +++ b/src/util/transaction.rs @@ -114,3 +114,245 @@ where s.serialize_field("vout", &outpoint.vout)?; s.end() } + +pub(super) mod sigops { + use crate::chain::{ + hashes::hex::FromHex, + opcodes::{ + all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY}, + All, + }, + script::{self, Instruction}, + Transaction, TxOut, Witness, + }; + use std::collections::HashMap; + + /// Get sigop count for transaction. prevout_map must have all the prevouts. + pub fn transaction_sigop_count( + tx: &Transaction, + prevout_map: &HashMap, + ) -> Result { + let input_count = tx.input.len(); + let mut prevouts = Vec::with_capacity(input_count); + + #[cfg(not(feature = "liquid"))] + let is_coinbase = tx.is_coin_base(); + #[cfg(feature = "liquid")] + let is_coinbase = tx.is_coinbase(); + + if !is_coinbase { + for idx in 0..input_count { + prevouts.push( + *prevout_map + .get(&(idx as u32)) + .ok_or(script::Error::EarlyEndOfScript)?, + ); + } + } + + // coinbase tx won't use prevouts so it can be empty. + get_sigop_cost(tx, &prevouts, true, true) + } + + fn decode_pushnum(op: &All) -> Option { + // 81 = OP_1, 96 = OP_16 + // 81 -> 1, so... 81 - 80 -> 1 + let self_u8 = op.into_u8(); + match self_u8 { + 81..=96 => Some(self_u8 - 80), + _ => None, + } + } + + fn count_sigops(script: &script::Script, accurate: bool) -> usize { + let mut n = 0; + let mut pushnum_cache = None; + for inst in script.instructions() { + match inst { + Ok(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 += usize::from(pushnum); + } + _ => { + // 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 = decode_pushnum(&opcode); + } + } + } + // We ignore errors as well as pushdatas + _ => { + pushnum_cache = None; + } + } + } + + n + } + + /// Get the sigop count for legacy transactions + fn get_legacy_sigop_count(tx: &Transaction) -> usize { + let mut n = 0; + for input in &tx.input { + n += count_sigops(&input.script_sig, false); + } + for output in &tx.output { + n += count_sigops(&output.script_pubkey, false); + } + n + } + + fn get_p2sh_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize { + #[cfg(not(feature = "liquid"))] + if tx.is_coin_base() { + return 0; + } + #[cfg(feature = "liquid")] + if tx.is_coinbase() { + return 0; + } + let mut n = 0; + for (input, prevout) in tx.input.iter().zip(previous_outputs.iter()) { + if prevout.script_pubkey.is_p2sh() { + if let Some(Ok(script::Instruction::PushBytes(redeem))) = + input.script_sig.instructions().last() + { + let script = + script::Script::from_byte_iter(redeem.iter().map(|v| Ok(*v))).unwrap(); // I only return Ok, so it won't error + n += count_sigops(&script, true); + } + } + } + n + } + + fn get_witness_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize { + let mut n = 0; + + #[inline] + fn is_push_only(script: &script::Script) -> bool { + for inst in script.instructions() { + match inst { + Err(_) => return false, + Ok(Instruction::Op(_)) => return false, + Ok(Instruction::PushBytes(_)) => {} + } + } + true + } + + #[inline] + fn last_pushdata(script: &script::Script) -> Option<&[u8]> { + match script.instructions().last() { + Some(Ok(Instruction::PushBytes(bytes))) => Some(bytes), + _ => None, + } + } + + #[inline] + fn count_with_prevout( + prevout: &TxOut, + script_sig: &script::Script, + witness: &Witness, + ) -> usize { + let mut n = 0; + + let script = if prevout.script_pubkey.is_witness_program() { + prevout.script_pubkey.clone() + } else if prevout.script_pubkey.is_p2sh() + && is_push_only(script_sig) + && !script_sig.is_empty() + { + script::Script::from_byte_iter( + last_pushdata(script_sig).unwrap().iter().map(|v| Ok(*v)), + ) + .unwrap() + } else { + return 0; + }; + + if script.is_v0_p2wsh() { + let bytes = script.as_bytes(); + n += sig_ops(witness, bytes[0], &bytes[2..]); + } else if script.is_v0_p2wpkh() { + n += 1; + } + n + } + + for (input, prevout) in tx.input.iter().zip(previous_outputs.iter()) { + n += count_with_prevout(prevout, &input.script_sig, &input.witness); + } + n + } + + /// Get the sigop cost for this transaction. + fn get_sigop_cost( + tx: &Transaction, + previous_outputs: &[&TxOut], + verify_p2sh: bool, + verify_witness: bool, + ) -> Result { + let mut n_sigop_cost = get_legacy_sigop_count(tx) * 4; + #[cfg(not(feature = "liquid"))] + if tx.is_coin_base() { + return Ok(n_sigop_cost); + } + #[cfg(feature = "liquid")] + if tx.is_coinbase() { + return Ok(n_sigop_cost); + } + if tx.input.len() != previous_outputs.len() { + return Err(script::Error::EarlyEndOfScript); + } + if verify_witness && !verify_p2sh { + return Err(script::Error::EarlyEndOfScript); + } + if verify_p2sh { + n_sigop_cost += get_p2sh_sigop_count(tx, previous_outputs) * 4; + } + if verify_witness { + n_sigop_cost += get_witness_sigop_count(tx, previous_outputs); + } + + Ok(n_sigop_cost) + } + + /// Get sigops for the Witness + /// + /// witness_version is the raw opcode. OP_0 is 0, OP_1 is 81, etc. + fn sig_ops(witness: &Witness, witness_version: u8, witness_program: &[u8]) -> usize { + #[cfg(feature = "liquid")] + let last_witness = witness.script_witness.last(); + #[cfg(not(feature = "liquid"))] + let last_witness = witness.last(); + match (witness_version, witness_program.len()) { + (0, 20) => 1, + (0, 32) => { + if let Some(n) = last_witness + .map(|sl| sl.iter().map(|v| Ok(*v))) + .map(script::Script::from_byte_iter) + // I only return Ok 2 lines up, so there is no way to error + .map(|s| count_sigops(&s.unwrap(), true)) + { + n + } else { + 0 + } + } + _ => 0, + } + } +}