diff --git a/src/crates/heuristics/src/ast/same_address.rs b/src/crates/heuristics/src/ast/same_address.rs index b8d4fb5..2f4dd52 100644 --- a/src/crates/heuristics/src/ast/same_address.rs +++ b/src/crates/heuristics/src/ast/same_address.rs @@ -68,7 +68,9 @@ mod tests { use tx_indexer_primitives::loose::LooseIndexBuilder; use tx_indexer_primitives::{ loose::{TxId, TxOutId}, - test_utils::{DummyTxData, DummyTxOutData, temp_dir, write_single_block_file}, + test_utils::{ + DummyTxData, DummyTxOutData, SEQUENCE_FINAL, temp_dir, write_single_block_file, + }, traits::abstract_types::AbstractTransaction, unified::AnyOutId, }; @@ -100,6 +102,7 @@ mod tests { DummyTxOutData::new_with_script(5_000, 1, shared_spk), // change (shared with spend2) ], vec![TxOutId::new(TxId(1), 0)], + vec![SEQUENCE_FINAL; 1], 0, ); @@ -110,6 +113,7 @@ mod tests { DummyTxOutData::new_with_script(5_000, 1, shared_spk), // change (shared with spend1) ], vec![TxOutId::new(TxId(2), 0)], + vec![SEQUENCE_FINAL; 1], 0, ); diff --git a/src/crates/heuristics/src/ast/tests.rs b/src/crates/heuristics/src/ast/tests.rs index e3b1b63..52c1291 100644 --- a/src/crates/heuristics/src/ast/tests.rs +++ b/src/crates/heuristics/src/ast/tests.rs @@ -9,7 +9,7 @@ pub(crate) mod ast_tests { UnifiedStorage, loose::LooseIndexBuilder, loose::{TxId, TxOutId}, - test_utils::{DummyTxData, DummyTxOutData}, + test_utils::{DummyTxData, DummyTxOutData, SEQUENCE_FINAL}, traits::abstract_types::AbstractTransaction, unified::{AnyOutId, AnyTxId}, }; @@ -291,6 +291,7 @@ pub(crate) mod ast_tests { DummyTxOutData::new(300, 1), // change ], vec![TxOutId::new(TxId(1), 0)], + vec![SEQUENCE_FINAL; 1], 0, ), DummyTxData::new_with_amounts(vec![300]), diff --git a/src/crates/heuristics/src/change_identification.rs b/src/crates/heuristics/src/change_identification.rs index 5b62333..9065a55 100644 --- a/src/crates/heuristics/src/change_identification.rs +++ b/src/crates/heuristics/src/change_identification.rs @@ -96,7 +96,7 @@ mod tests { UnifiedStorage, loose::LooseIndexBuilder, loose::{TxId, TxOutId}, - test_utils::{DummyTxData, DummyTxOut, DummyTxOutData}, + test_utils::{DummyTxData, DummyTxOut, DummyTxOutData, SEQUENCE_FINAL}, unified::AnyOutId, }; @@ -146,9 +146,9 @@ mod tests { // Same lock time let tx_out = DummyTxOut { vout: 0, - containing_tx: DummyTxData::new(vec![DummyTxOutData::new(100, 0)], vec![], 1), + containing_tx: DummyTxData::new(vec![DummyTxOutData::new(100, 0)], vec![], vec![], 1), }; - let spending_tx = DummyTxData::new(vec![DummyTxOutData::new(100, 0)], vec![], 1); + let spending_tx = DummyTxData::new(vec![DummyTxOutData::new(100, 0)], vec![], vec![], 1); assert_eq!( NLockTimeChangeIdentification::is_change(tx_out, spending_tx), TxOutChangeAnnotation::Change @@ -187,6 +187,7 @@ mod tests { ), ], vec![TxOutId::new(TxId(1), 0), TxOutId::new(TxId(2), 0)], + vec![SEQUENCE_FINAL; 2], 0, ), ]); @@ -236,6 +237,7 @@ mod tests { ), ], vec![TxOutId::new(TxId(1), 0), TxOutId::new(TxId(2), 0)], + vec![SEQUENCE_FINAL; 2], 0, ), ]); @@ -282,6 +284,7 @@ mod tests { ), ], vec![TxOutId::new(TxId(1), 0), TxOutId::new(TxId(2), 0)], + vec![SEQUENCE_FINAL; 2], 0, ), ]); diff --git a/src/crates/primitives/src/handle.rs b/src/crates/primitives/src/handle.rs index 3a90d43..eb4214d 100644 --- a/src/crates/primitives/src/handle.rs +++ b/src/crates/primitives/src/handle.rs @@ -1,10 +1,11 @@ use crate::{ - AnyInId, AnyOutId, AnyTxId, HasWitnessData, OutputType, + AnyInId, AnyOutId, AnyTxId, HasWitnessData, OutputType, ScriptPubkeyHash, traits::{ abstract_types::{ AbstractTransaction, AbstractTxIn, AbstractTxOut, EnumerateInputValueInArbitraryOrder, - EnumerateOutputValueInArbitraryOrder, EnumerateSpentTxOuts, HasNLockTime, - HasScriptPubkey, HasSequence, InputCount, OutputCount, TxConstituent, + EnumerateOutputValueInArbitraryOrder, EnumerateSpentTxOuts, HasBlockHeight, + HasInputPrevOuts, HasNLockTime, HasScriptPubkey, HasSequence, InputCount, OutputCount, + TxConstituent, }, graph_index::IndexedGraph, }, @@ -300,6 +301,36 @@ impl<'a> HasNLockTime for TxHandle<'a> { } } +impl<'a> HasBlockHeight for TxHandle<'a> { + fn block_height(&self) -> Option { + TxHandle::block_height(self) + } +} + +impl<'a> TxHandle<'a> { + fn map_input_prev_outs( + &self, + project: impl Fn(TxOutHandle<'a>) -> R + 'a, + ) -> impl Iterator> + '_ { + let index = self.index; + self.inputs().map(move |i| { + let prev = i.prev_txout()?; + index.tx(&prev.txid())?; + Some(project(prev)) + }) + } +} + +impl<'a> HasInputPrevOuts for TxHandle<'a> { + fn input_prev_types(&self) -> impl Iterator> { + self.map_input_prev_outs(|p| p.output_type()) + } + + fn input_prev_script_hashes(&self) -> impl Iterator> { + self.map_input_prev_outs(|p| p.script_pubkey_hash()) + } +} + impl<'a> TxConstituent for TxOutHandle<'a> { type Handle = TxHandle<'a>; diff --git a/src/crates/primitives/src/loose/mod.rs b/src/crates/primitives/src/loose/mod.rs index 3fc4b6d..93be5bf 100644 --- a/src/crates/primitives/src/loose/mod.rs +++ b/src/crates/primitives/src/loose/mod.rs @@ -284,9 +284,18 @@ impl TxIoIndex for InMemoryIndex { tx.locktime() } - fn input_sequence(&self, _in_id: &AnyInId) -> u32 { - // TODO: loose transactions don't carry sequence data in the abstract model yet. - panic!("input_sequence not supported for loose transactions"); + fn input_sequence(&self, in_id: &AnyInId) -> u32 { + let loose_in = in_id + .loose_id() + .expect("loose storage only supports loose txin ids"); + let tx = self + .txs + .get(&loose_in.txid()) + .expect("loose txid not found in storage"); + tx.inputs() + .nth(loose_in.vin() as usize) + .expect("vin out of range") + .sequence() } fn witness_items(&self, _in_id: &AnyInId) -> Vec> { diff --git a/src/crates/primitives/src/test_utils/mod.rs b/src/crates/primitives/src/test_utils/mod.rs index b8b32cf..5c4d2bd 100644 --- a/src/crates/primitives/src/test_utils/mod.rs +++ b/src/crates/primitives/src/test_utils/mod.rs @@ -3,37 +3,59 @@ use bitcoin::hashes::Hash as _; use bitcoin::hashes::hash160::Hash as Hash160; use crate::{ - AnyOutId, AnyTxId, ScriptPubkeyHash, + AnyOutId, AnyTxId, OutputType, ScriptPubkeyHash, loose::{TxId, TxOutId}, - traits::HasNLockTime, traits::abstract_types::{ AbstractTransaction, AbstractTxIn, AbstractTxOut, EnumerateOutputValueInArbitraryOrder, EnumerateSpentTxOuts, HasScriptPubkey, InputCount, OutputCount, TxConstituent, }, + traits::{HasBlockHeight, HasInputPrevOuts, HasNLockTime, HasSequence}, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DummyTxData { outputs: Vec, - /// The outputs that are spent by this transaction - spent_coins: Vec, + /// Inputs of this transaction; each carries its prev outpoint and sequence. + inputs: Vec, n_locktime: u32, } +pub const SEQUENCE_FINAL: u32 = u32::MAX; + impl DummyTxData { /// Base constructor. - pub fn new(outputs: Vec, spent_coins: Vec, n_locktime: u32) -> Self { + pub fn new( + outputs: Vec, + spent_coins: Vec, + sequences: Vec, + n_locktime: u32, + ) -> Self { + assert_eq!( + spent_coins.len(), + sequences.len(), + "spent_coins and sequences must have matching lengths" + ); + let inputs = spent_coins + .into_iter() + .zip(sequences) + .map(|(coin, sequence)| DummyTxInWrapper { + prev_txid: coin.txid(), + prev_vout: coin.vout(), + sequence, + }) + .collect(); Self { outputs, - spent_coins, + inputs, n_locktime, } } + /// Tx with explicit outputs, no spent coins. pub fn new_with_outputs(outputs: Vec) -> Self { Self { outputs, - spent_coins: vec![], + inputs: vec![], n_locktime: 0, } } @@ -51,11 +73,15 @@ impl DummyTxData { /// Create spending tx from amounts and spent coins. pub fn new_with_spent(amounts: Vec, spent_coins: Vec) -> Self { let base = Self::new_with_amounts(amounts); - Self::new(base.outputs, spent_coins, 0) + let n = spent_coins.len(); + Self::new(base.outputs, spent_coins, vec![SEQUENCE_FINAL; n], 0) } - pub fn spent_coins(&self) -> &[TxOutId] { - &self.spent_coins + pub fn spent_coins(&self) -> Vec { + self.inputs + .iter() + .map(|i| TxOutId::new(i.prev_txid, i.prev_vout)) + .collect() } } @@ -65,10 +91,33 @@ impl HasNLockTime for DummyTxData { } } +impl HasBlockHeight for DummyTxData { + fn block_height(&self) -> Option { + None + } +} + +impl HasInputPrevOuts for DummyTxData { + fn input_prev_types(&self) -> impl Iterator> { + std::iter::repeat_n(None, self.inputs.len()) + } + fn input_prev_script_hashes(&self) -> impl Iterator> { + std::iter::repeat_n(None, self.inputs.len()) + } +} + // Wrapper types for implementing abstract traits on dummy types +#[derive(Debug, Clone, PartialEq, Eq, Hash)] struct DummyTxInWrapper { prev_txid: TxId, prev_vout: u32, + sequence: u32, +} + +impl HasSequence for DummyTxInWrapper { + fn sequence(&self) -> u32 { + self.sequence + } } impl AbstractTxIn for DummyTxInWrapper { @@ -132,14 +181,9 @@ impl AbstractTransaction for DummyTxData { fn inputs(&self) -> Box> + '_> { // Collect into a vector to avoid lifetime issues let inputs: Vec> = self - .spent_coins + .inputs .iter() - .map(|spent| { - Box::new(DummyTxInWrapper { - prev_txid: spent.txid(), - prev_vout: spent.vout(), - }) as Box - }) + .map(|input| Box::new(input.clone()) as Box) .collect(); Box::new(inputs.into_iter()) } @@ -155,7 +199,7 @@ impl AbstractTransaction for DummyTxData { } fn input_len(&self) -> usize { - self.spent_coins.len() + self.inputs.len() } fn output_len(&self) -> usize { @@ -173,7 +217,7 @@ impl AbstractTransaction for DummyTxData { } fn is_coinbase(&self) -> bool { - self.spent_coins.is_empty() + self.inputs.is_empty() } } @@ -185,7 +229,7 @@ impl OutputCount for DummyTxData { impl InputCount for DummyTxData { fn input_count(&self) -> usize { - self.spent_coins.len() + self.inputs.len() } } @@ -222,7 +266,9 @@ pub fn write_single_block_file(dir: &std::path::Path, block: &[u8]) -> std::io:: impl EnumerateSpentTxOuts for DummyTxData { fn spent_coins(&self) -> impl Iterator { - self.spent_coins.iter().copied().map(AnyOutId::from) + self.inputs + .iter() + .map(|i| AnyOutId::from(TxOutId::new(i.prev_txid, i.prev_vout))) } } diff --git a/src/crates/primitives/src/traits/abstract_types.rs b/src/crates/primitives/src/traits/abstract_types.rs index 3e77588..724f603 100644 --- a/src/crates/primitives/src/traits/abstract_types.rs +++ b/src/crates/primitives/src/traits/abstract_types.rs @@ -32,7 +32,7 @@ pub trait EnumerateInputValueInArbitraryOrder: AbstractTransaction { } /// Trait for transaction inputs -pub trait AbstractTxIn { +pub trait AbstractTxIn: HasSequence { /// Returns the transaction ID of the previous output fn prev_txid(&self) -> Option; /// Returns the output index of the previous output @@ -111,6 +111,22 @@ pub trait HasPrevOutput { fn prev_outpoint_vout(&self) -> u32; } +/// Confirmed block height of a transaction. `None` when unconfirmed +/// (loose). +pub trait HasBlockHeight { + fn block_height(&self) -> Option; +} + +/// Per-input prev-out script type and script-pubkey hash, in input order. +/// Each entry is `None` when the prev tx is unavailable (coinbase, missing +/// from the index, or wrapper opted out of populating it). +pub trait HasInputPrevOuts: AbstractTransaction { + /// Per-input prev-out script type, in input order. + fn input_prev_types(&self) -> impl Iterator>; + /// Per-input prev-out script-pubkey hash, in input order. + fn input_prev_script_hashes(&self) -> impl Iterator>; +} + // --- bitcoin type impls --- impl HasSequence for bitcoin::TxIn { diff --git a/src/crates/primitives/src/traits/mod.rs b/src/crates/primitives/src/traits/mod.rs index 7eef286..efc2541 100644 --- a/src/crates/primitives/src/traits/mod.rs +++ b/src/crates/primitives/src/traits/mod.rs @@ -2,8 +2,8 @@ pub mod abstract_types; pub mod graph_index; pub use abstract_types::{ - HasNLockTime, HasPrevOutput, HasScriptPubkey, HasSequence, HasValue, HasVersion, - HasWitnessData, InputCount, + HasBlockHeight, HasInputPrevOuts, HasNLockTime, HasPrevOutput, HasScriptPubkey, HasSequence, + HasValue, HasVersion, HasWitnessData, InputCount, }; use crate::ScriptPubkeyHash; diff --git a/src/crates/primitives/src/unified/mod.rs b/src/crates/primitives/src/unified/mod.rs index 88af874..ee9b655 100644 --- a/src/crates/primitives/src/unified/mod.rs +++ b/src/crates/primitives/src/unified/mod.rs @@ -468,8 +468,7 @@ impl TxIoIndex for UnifiedStorage { fn input_sequence(&self, in_id: &AnyInId) -> u32 { if in_id.is_loose() { - // TODO: loose transactions don't carry sequence data in the abstract model yet. - panic!("input_sequence not supported for loose transactions"); + return self.loose().input_sequence(in_id); } let did = in_id.confirmed_id().expect("must be dense"); let ds = self.dense();