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

Feature: Count sigops on electrs side #43

Merged
merged 2 commits into from
Sep 18, 2023
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
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);
junderw marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
// 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,
}
}
}