Skip to content

Commit

Permalink
Merge pull request #43 from mempool/junderw/sigops
Browse files Browse the repository at this point in the history
Feature: Count sigops on electrs side
  • Loading branch information
softsimon committed Sep 18, 2023
2 parents 333ad87 + 881ca56 commit 3b16c0a
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 8 deletions.
12 changes: 8 additions & 4 deletions src/chain.rs
Original file line number Diff line number Diff line change
@@ -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,
},
};

Expand Down
8 changes: 6 additions & 2 deletions src/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down Expand Up @@ -144,6 +144,7 @@ struct TransactionValue {
vout: Vec<TxOutValue>,
size: u32,
weight: u32,
sigops: u32,
fee: u64,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<TransactionStatus>,
Expand All @@ -157,6 +158,8 @@ impl TransactionValue {
config: &Config,
) -> Result<Self, errors::Error> {
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<TxInValue> = tx
.input
Expand All @@ -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)),
})
Expand Down
2 changes: 1 addition & 1 deletion src/util/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
242 changes: 242 additions & 0 deletions src/util/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32, &TxOut>,
) -> Result<usize, script::Error> {
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<u8> {
// 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<usize, script::Error> {
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,
}
}
}

0 comments on commit 3b16c0a

Please sign in to comment.