mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-16 05:28:12 -07:00
global: adding support for safe lengths
This commit is contained in:
@@ -4,7 +4,6 @@ use tracing::error;
|
||||
use vecdb::WritableVec;
|
||||
|
||||
use super::{BlockProcessor, ComputedTx};
|
||||
use crate::IndexesExt;
|
||||
|
||||
impl BlockProcessor<'_> {
|
||||
pub fn process_block_metadata(&mut self) -> Result<()> {
|
||||
@@ -22,7 +21,7 @@ impl BlockProcessor<'_> {
|
||||
return Err(Error::Internal("BlockHash prefix collision"));
|
||||
}
|
||||
|
||||
self.indexes.checked_push(self.vecs)?;
|
||||
self.lengths.checked_push(self.vecs)?;
|
||||
|
||||
self.stores
|
||||
.blockhash_prefix_to_height
|
||||
|
||||
@@ -9,32 +9,23 @@ pub use types::*;
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_types::{
|
||||
AddrHash, Block, Height, OutPoint, SigOps, TxInIndex, TxIndex, TxOutIndex, TypeIndex,
|
||||
};
|
||||
use brk_types::{AddrHash, Block, Height, OutPoint, SigOps, TxInIndex, TypeIndex};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use crate::{Indexes, Readers, Stores, Vecs};
|
||||
use crate::{Lengths, Readers, Stores, Vecs};
|
||||
|
||||
/// Processes a single block, extracting and storing all indexed data.
|
||||
pub struct BlockProcessor<'a> {
|
||||
pub block: &'a Block,
|
||||
pub height: Height,
|
||||
pub check_collisions: bool,
|
||||
pub indexes: &'a mut Indexes,
|
||||
pub lengths: &'a mut Lengths,
|
||||
pub vecs: &'a mut Vecs,
|
||||
pub stores: &'a mut Stores,
|
||||
pub readers: &'a Readers,
|
||||
}
|
||||
|
||||
impl BlockProcessor<'_> {
|
||||
/// Update global indexes after processing a block.
|
||||
pub fn update_indexes(&mut self, tx_count: usize, input_count: usize, output_count: usize) {
|
||||
self.indexes.tx_index += TxIndex::from(tx_count);
|
||||
self.indexes.txin_index += TxInIndex::from(input_count);
|
||||
self.indexes.txout_index += TxOutIndex::from(output_count);
|
||||
}
|
||||
|
||||
/// Finalizes outputs/inputs in parallel with storing tx metadata.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn finalize_and_store_metadata(
|
||||
@@ -47,7 +38,7 @@ impl BlockProcessor<'_> {
|
||||
already_added: &mut ByAddrType<FxHashMap<AddrHash, TypeIndex>>,
|
||||
same_block_info: &mut FxHashMap<OutPoint, SameBlockOutputInfo>,
|
||||
) -> Result<()> {
|
||||
let indexes = &mut *self.indexes;
|
||||
let lengths = &mut *self.lengths;
|
||||
|
||||
// Split transactions vecs: finalize needs first_txout_index/first_txin_index, metadata needs the rest
|
||||
let (first_txout_index, first_txin_index, mut tx_metadata) =
|
||||
@@ -66,7 +57,7 @@ impl BlockProcessor<'_> {
|
||||
let (finalize_result, metadata_result) = rayon::join(
|
||||
|| -> Result<()> {
|
||||
txout::finalize_outputs(
|
||||
indexes,
|
||||
lengths,
|
||||
first_txout_index,
|
||||
outputs,
|
||||
addrs,
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
use bitcoin::{Script, script::Instruction};
|
||||
use brk_types::{OutputType, SigOps, TxInIndex};
|
||||
use rayon::prelude::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::{BlockProcessor, InputSource};
|
||||
use super::{BlockProcessor, InputSource, ProcessedOutput};
|
||||
|
||||
impl BlockProcessor<'_> {
|
||||
/// BIP-141 sigop cost per tx in the block. Uses each input's prevout
|
||||
/// `OutputType` (already resolved by `process_inputs` for the
|
||||
/// previous-block case, looked up from `block.txdata` for the
|
||||
/// same-block case) to feed canonical-shaped synthetic prevouts into
|
||||
/// `bitcoin::Transaction::total_sigop_cost`.
|
||||
pub fn compute_sigops(&self, txins: &[(TxInIndex, InputSource)]) -> Vec<SigOps> {
|
||||
/// BIP-141 sigop cost per tx in the block. Mirrors
|
||||
/// `bitcoin::Transaction::total_sigop_cost` but dispatches on each
|
||||
/// input's prevout `OutputType` and each output's `OutputType`
|
||||
/// (already resolved by `process_inputs`/`process_outputs`) instead
|
||||
/// of round-tripping through bitcoin's closure API with
|
||||
/// synthetic-prevout `ScriptBuf` allocations. The legacy-sigop walk
|
||||
/// is short-circuited by `OutputType` for every script with a
|
||||
/// canonical shape (~99% of outputs and ~95% of inputs on mainnet);
|
||||
/// only `OpReturn`/`Unknown` outputs and non-segwit/non-P2SH inputs
|
||||
/// fall back to a real script walk.
|
||||
pub fn compute_sigops(
|
||||
&self,
|
||||
txins: &[(TxInIndex, InputSource)],
|
||||
txouts: &[ProcessedOutput<'_>],
|
||||
) -> Vec<SigOps> {
|
||||
let txdata = &self.block.txdata;
|
||||
let base_tx_index = u32::from(self.indexes.tx_index);
|
||||
let base_tx_index = u32::from(self.lengths.tx_index);
|
||||
|
||||
let mut tx_input_offsets = Vec::with_capacity(txdata.len());
|
||||
let mut offset = 0usize;
|
||||
let mut tx_output_offsets = Vec::with_capacity(txdata.len());
|
||||
let mut input_offset = 0usize;
|
||||
let mut output_offset = 0usize;
|
||||
for tx in txdata {
|
||||
tx_input_offsets.push(offset);
|
||||
offset += tx.input.len();
|
||||
tx_input_offsets.push(input_offset);
|
||||
input_offset += tx.input.len();
|
||||
tx_output_offsets.push(output_offset);
|
||||
output_offset += tx.output.len();
|
||||
}
|
||||
|
||||
txdata
|
||||
@@ -28,31 +41,144 @@ impl BlockProcessor<'_> {
|
||||
if tx.is_coinbase() {
|
||||
return SigOps::ZERO;
|
||||
}
|
||||
let start = tx_input_offsets[i];
|
||||
let tx_inputs = &txins[start..start + tx.input.len()];
|
||||
let in_start = tx_input_offsets[i];
|
||||
let tx_inputs = &txins[in_start..in_start + tx.input.len()];
|
||||
let out_start = tx_output_offsets[i];
|
||||
let tx_outputs = &txouts[out_start..out_start + tx.output.len()];
|
||||
|
||||
let kinds: SmallVec<[(bitcoin::OutPoint, OutputType); 4]> = tx
|
||||
.input
|
||||
.iter()
|
||||
.zip(tx_inputs.iter())
|
||||
.map(|(txin, (_, source))| {
|
||||
let kind = match source {
|
||||
InputSource::PreviousBlock { output_type, .. } => *output_type,
|
||||
InputSource::SameBlock { outpoint, .. } => {
|
||||
let local =
|
||||
(u32::from(outpoint.tx_index()) - base_tx_index) as usize;
|
||||
let vout = u32::from(outpoint.vout()) as usize;
|
||||
OutputType::from(&txdata[local].output[vout].script_pubkey)
|
||||
let mut legacy: usize = 0;
|
||||
let mut redeem: usize = 0;
|
||||
let mut witness: usize = 0;
|
||||
|
||||
for (input, (_, source)) in tx.input.iter().zip(tx_inputs.iter()) {
|
||||
let prev_kind = match source {
|
||||
InputSource::PreviousBlock { output_type, .. } => *output_type,
|
||||
InputSource::SameBlock { outpoint, .. } => {
|
||||
let local =
|
||||
(u32::from(outpoint.tx_index()) - base_tx_index) as usize;
|
||||
let vout = u32::from(outpoint.vout()) as usize;
|
||||
txouts[tx_output_offsets[local] + vout].output_type
|
||||
}
|
||||
};
|
||||
|
||||
// Single match per input: legacy script_sig sigops AND the
|
||||
// redeem/witness contribution. Consensus enforces a
|
||||
// push-only or empty script_sig in the four cases below
|
||||
// (BIP-16 for P2SH from block 173805 onwards; BIP-141 /
|
||||
// BIP-341 for segwit/taproot from activation), so legacy
|
||||
// sigops are guaranteed 0 there. Everything else falls
|
||||
// through to a real `count_sigops_legacy` walk.
|
||||
match prev_kind {
|
||||
OutputType::P2SH => {
|
||||
// Faithful to bitcoin's count_p2sh_sigops + the
|
||||
// nested-segwit branch of count_witness_sigops in
|
||||
// a single script walk: redeem sigops use
|
||||
// last_pushdata (no push-only check), wrapped
|
||||
// witness sigops require both push-only and
|
||||
// last_pushdata.
|
||||
let (last_push, is_push_only) =
|
||||
last_push_and_push_only(&input.script_sig);
|
||||
let Some(redeem_bytes) = last_push else {
|
||||
continue;
|
||||
};
|
||||
let rs = Script::from_bytes(redeem_bytes);
|
||||
redeem = redeem.saturating_add(rs.count_sigops());
|
||||
if !is_push_only {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
(txin.previous_output, kind)
|
||||
})
|
||||
.collect();
|
||||
if rs.is_p2wpkh() {
|
||||
witness = witness.saturating_add(1);
|
||||
} else if rs.is_p2wsh()
|
||||
&& let Some(last) = input.witness.last()
|
||||
{
|
||||
witness = witness
|
||||
.saturating_add(Script::from_bytes(last).count_sigops());
|
||||
}
|
||||
}
|
||||
OutputType::P2WPKH => {
|
||||
witness = witness.saturating_add(1);
|
||||
}
|
||||
OutputType::P2WSH => {
|
||||
if let Some(last) = input.witness.last() {
|
||||
witness = witness
|
||||
.saturating_add(Script::from_bytes(last).count_sigops());
|
||||
}
|
||||
}
|
||||
OutputType::P2TR => {}
|
||||
_ => {
|
||||
legacy = legacy
|
||||
.saturating_add(input.script_sig.count_sigops_legacy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SigOps::of_bitcoin_tx_with_kinds(tx, |op| {
|
||||
kinds.iter().find(|(o, _)| o == op).map(|(_, k)| *k)
|
||||
})
|
||||
for processed in tx_outputs {
|
||||
legacy = legacy.saturating_add(legacy_sigops_for_output(
|
||||
processed.output_type,
|
||||
&processed.txout.script_pubkey,
|
||||
));
|
||||
}
|
||||
|
||||
SigOps::from(
|
||||
legacy
|
||||
.saturating_mul(4)
|
||||
.saturating_add(redeem.saturating_mul(4))
|
||||
.saturating_add(witness),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy sigop count of a script_pubkey, dispatched on `OutputType`.
|
||||
/// Every variant except `OpReturn` and `Unknown` has a canonical shape
|
||||
/// recognised by `OutputType::from`'s exact byte-pattern matchers, so
|
||||
/// the legacy sigop count is fixed: P2PKH and P2PK both end in a
|
||||
/// single OP_CHECKSIG (1), P2MS contains one OP_CHECKMULTISIG counted
|
||||
/// as 20 in legacy mode, and P2SH/P2WPKH/P2WSH/P2TR/P2A/Empty contain
|
||||
/// no CHECKSIG-class opcodes outside their pushdata. `OpReturn`
|
||||
/// payloads can include 0xac/0xae bytes outside a push, and `Unknown`
|
||||
/// can be anything, so both fall back to a real script walk.
|
||||
#[inline]
|
||||
fn legacy_sigops_for_output(output_type: OutputType, script_pubkey: &Script) -> usize {
|
||||
match output_type {
|
||||
OutputType::P2PKH | OutputType::P2PK33 | OutputType::P2PK65 => 1,
|
||||
OutputType::P2MS => 20,
|
||||
OutputType::P2SH
|
||||
| OutputType::P2WPKH
|
||||
| OutputType::P2WSH
|
||||
| OutputType::P2TR
|
||||
| OutputType::P2A
|
||||
| OutputType::Empty => 0,
|
||||
OutputType::OpReturn | OutputType::Unknown => script_pubkey.count_sigops_legacy(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Single-pass equivalent of bitcoin's private `last_pushdata()` plus the
|
||||
/// public `is_push_only()`: returns the bytes of the script's last
|
||||
/// `Instruction::PushBytes` (only when it is the *last* instruction)
|
||||
/// alongside whether every instruction was a push (per Core,
|
||||
/// `OP_RESERVED` and `OP_PUSHNUM_1..16` count as pushes too).
|
||||
fn last_push_and_push_only(script: &Script) -> (Option<&[u8]>, bool) {
|
||||
let mut last: Option<&[u8]> = None;
|
||||
let mut push_only = true;
|
||||
for inst in script.instructions() {
|
||||
match inst {
|
||||
Ok(Instruction::PushBytes(b)) => {
|
||||
last = Some(b.as_bytes());
|
||||
}
|
||||
Ok(Instruction::Op(op)) => {
|
||||
last = None;
|
||||
if op.to_u8() > 0x60 {
|
||||
push_only = false;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
last = None;
|
||||
push_only = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
(last, push_only)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use super::{BlockProcessor, ComputedTx};
|
||||
impl<'a> BlockProcessor<'a> {
|
||||
pub fn compute_txids(&self) -> Result<Vec<ComputedTx<'a>>> {
|
||||
let will_check_collisions = self.check_collisions;
|
||||
let base_tx_index = self.indexes.tx_index;
|
||||
let base_tx_index = self.lengths.tx_index;
|
||||
|
||||
self.block
|
||||
.txdata
|
||||
|
||||
@@ -22,8 +22,8 @@ impl<'a> BlockProcessor<'a> {
|
||||
txid_prefix_to_tx_index.clear();
|
||||
txid_prefix_to_tx_index.extend(txs.iter().map(|ct| (ct.txid_prefix, ct.tx_index)));
|
||||
|
||||
let base_tx_index = self.indexes.tx_index;
|
||||
let base_txin_index = self.indexes.txin_index;
|
||||
let base_tx_index = self.lengths.tx_index;
|
||||
let base_txin_index = self.lengths.txin_index;
|
||||
|
||||
let total_inputs: usize = self.block.txdata.iter().map(|tx| tx.input.len()).sum();
|
||||
let mut items = Vec::with_capacity(total_inputs);
|
||||
@@ -79,11 +79,11 @@ impl<'a> BlockProcessor<'a> {
|
||||
.map(|v| *v);
|
||||
|
||||
let prev_tx_index = match store_result {
|
||||
Some(tx_index) if tx_index < self.indexes.tx_index => tx_index,
|
||||
Some(tx_index) if tx_index < self.lengths.tx_index => tx_index,
|
||||
_ => {
|
||||
error!(
|
||||
"UnknownTxid: txid={}, prefix={:?}, store_result={:?}, current_tx_index={:?}",
|
||||
txid, txid_prefix, store_result, self.indexes.tx_index
|
||||
txid, txid_prefix, store_result, self.lengths.tx_index
|
||||
);
|
||||
return Err(Error::UnknownTxid);
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ use tracing::error;
|
||||
use vecdb::{BytesVec, WritableVec};
|
||||
|
||||
use super::{BlockProcessor, ProcessedOutput, SameBlockOutputInfo};
|
||||
use crate::{AddrsVecs, Indexes, OutputsVecs, ScriptsVecs};
|
||||
use crate::{AddrsVecs, Lengths, OutputsVecs, ScriptsVecs};
|
||||
|
||||
impl<'a> BlockProcessor<'a> {
|
||||
pub fn process_outputs(&self) -> Result<Vec<ProcessedOutput<'a>>> {
|
||||
let height = self.height;
|
||||
let check_collisions = self.check_collisions;
|
||||
|
||||
let base_tx_index = self.indexes.tx_index;
|
||||
let base_txout_index = self.indexes.txout_index;
|
||||
let base_tx_index = self.lengths.tx_index;
|
||||
let base_txout_index = self.lengths.txout_index;
|
||||
|
||||
let total_outputs: usize = self.block.txdata.iter().map(|tx| tx.output.len()).sum();
|
||||
let mut items = Vec::with_capacity(total_outputs);
|
||||
@@ -63,7 +63,7 @@ impl<'a> BlockProcessor<'a> {
|
||||
.get(&addr_hash)?
|
||||
.map(|v| *v)
|
||||
.and_then(|type_index_local| {
|
||||
(type_index_local < self.indexes.to_type_index(addr_type))
|
||||
(type_index_local < self.lengths.to_type_index(addr_type))
|
||||
.then_some(type_index_local)
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ impl<'a> BlockProcessor<'a> {
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn finalize_outputs(
|
||||
indexes: &mut Indexes,
|
||||
lengths: &mut Lengths,
|
||||
first_txout_index: &mut BytesVec<TxIndex, TxOutIndex>,
|
||||
outputs: &mut OutputsVecs,
|
||||
addrs: &mut AddrsVecs,
|
||||
@@ -150,7 +150,7 @@ pub(super) fn finalize_outputs(
|
||||
{
|
||||
ti
|
||||
} else {
|
||||
let ti = indexes.increment_addr_index(addr_type);
|
||||
let ti = lengths.increment_addr_index(addr_type);
|
||||
|
||||
already_added_addr_hash
|
||||
.get_mut_unwrap(addr_type)
|
||||
@@ -168,29 +168,29 @@ pub(super) fn finalize_outputs(
|
||||
scripts
|
||||
.p2ms
|
||||
.to_tx_index
|
||||
.checked_push(indexes.p2ms_output_index, tx_index)?;
|
||||
indexes.p2ms_output_index.copy_then_increment()
|
||||
.checked_push(lengths.p2ms_output_index, tx_index)?;
|
||||
lengths.p2ms_output_index.copy_then_increment()
|
||||
}
|
||||
OutputType::OpReturn => {
|
||||
scripts
|
||||
.op_return
|
||||
.to_tx_index
|
||||
.checked_push(indexes.op_return_index, tx_index)?;
|
||||
indexes.op_return_index.copy_then_increment()
|
||||
.checked_push(lengths.op_return_index, tx_index)?;
|
||||
lengths.op_return_index.copy_then_increment()
|
||||
}
|
||||
OutputType::Empty => {
|
||||
scripts
|
||||
.empty
|
||||
.to_tx_index
|
||||
.checked_push(indexes.empty_output_index, tx_index)?;
|
||||
indexes.empty_output_index.copy_then_increment()
|
||||
.checked_push(lengths.empty_output_index, tx_index)?;
|
||||
lengths.empty_output_index.copy_then_increment()
|
||||
}
|
||||
OutputType::Unknown => {
|
||||
scripts
|
||||
.unknown
|
||||
.to_tx_index
|
||||
.checked_push(indexes.unknown_output_index, tx_index)?;
|
||||
indexes.unknown_output_index.copy_then_increment()
|
||||
.checked_push(lengths.unknown_output_index, tx_index)?;
|
||||
lengths.unknown_output_index.copy_then_increment()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user