Skip to content

Commit ecbc483

Browse files
committed
feat(chain): Signed txs should displace unsigned txs in TxGraph.
1 parent b707586 commit ecbc483

File tree

2 files changed

+134
-7
lines changed

2 files changed

+134
-7
lines changed

crates/chain/src/tx_graph.rs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -613,22 +613,79 @@ impl<A: Anchor> TxGraph<A> {
613613
changeset
614614
}
615615

616-
/// Inserts the given transaction into [`TxGraph`].
616+
/// Insert the given transaction into [`TxGraph`].
617617
///
618-
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
618+
/// The [`ChangeSet`] returned will be empty if no changes are made to the graph.
619+
///
620+
/// # Updating Existing Transactions
621+
///
622+
/// An unsigned transaction can be inserted first and have it's witness fields updated with
623+
/// further transaction insertions (given that the newly introduced transaction shares the same
624+
/// txid as the original transaction).
625+
///
626+
/// The witnesses of the newly introduced transaction will be merged with the witnesses of the
627+
/// original transaction in a way where:
628+
///
629+
/// * A non-empty witness has precedence over an empty witness.
630+
/// * A smaller witness has precedence over a larger witness.
631+
/// * If the witness sizes are the same, we prioritize the two witnesses with lexicographical
632+
/// order.
619633
pub fn insert_tx<T: Into<Arc<Transaction>>>(&mut self, tx: T) -> ChangeSet<A> {
634+
// This returns `Some` only if the merged tx is different to the `original_tx`.
635+
fn _merge_tx_witnesses(
636+
original_tx: &Arc<Transaction>,
637+
other_tx: &Arc<Transaction>,
638+
) -> Option<Arc<Transaction>> {
639+
debug_assert_eq!(
640+
original_tx.input.len(),
641+
other_tx.input.len(),
642+
"tx input count must be the same"
643+
);
644+
let merged_input = Iterator::zip(original_tx.input.iter(), other_tx.input.iter())
645+
.map(|(original_txin, other_txin)| {
646+
let original_key = core::cmp::Reverse((
647+
original_txin.witness.is_empty(),
648+
original_txin.witness.size(),
649+
&original_txin.witness,
650+
));
651+
let other_key = core::cmp::Reverse((
652+
other_txin.witness.is_empty(),
653+
other_txin.witness.size(),
654+
&other_txin.witness,
655+
));
656+
if original_key > other_key {
657+
original_txin.clone()
658+
} else {
659+
other_txin.clone()
660+
}
661+
})
662+
.collect::<Vec<_>>();
663+
if merged_input == original_tx.input {
664+
return None;
665+
}
666+
if merged_input == other_tx.input {
667+
return Some(other_tx.clone());
668+
}
669+
Some(Arc::new(Transaction {
670+
input: merged_input,
671+
..(**original_tx).clone()
672+
}))
673+
}
674+
620675
let tx: Arc<Transaction> = tx.into();
621676
let txid = tx.compute_txid();
622677
let mut changeset = ChangeSet::<A>::default();
623678

624679
let tx_node = self.txs.entry(txid).or_default();
625680
match tx_node {
626681
TxNodeInternal::Whole(existing_tx) => {
627-
debug_assert_eq!(
628-
existing_tx.as_ref(),
629-
tx.as_ref(),
630-
"tx of same txid should never change"
631-
);
682+
if existing_tx.as_ref() != tx.as_ref() {
683+
// Allowing updating witnesses of txs.
684+
if let Some(merged_tx) = _merge_tx_witnesses(existing_tx, &tx) {
685+
*existing_tx = merged_tx.clone();
686+
changeset.txs.insert(merged_tx);
687+
}
688+
}
632689
}
633690
partial_tx => {
634691
for txin in &tx.input {

crates/chain/tests/test_tx_graph.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use bdk_chain::{
1010
Anchor, ChainOracle, ChainPosition, Merge,
1111
};
1212
use bdk_testenv::{block_id, hash, utils::new_tx};
13+
use bitcoin::hex::FromHex;
14+
use bitcoin::Witness;
1315
use bitcoin::{
1416
absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount,
1517
Transaction, TxIn, TxOut, Txid,
@@ -284,6 +286,74 @@ fn insert_tx_displaces_txouts() {
284286
assert_eq!(tx_graph.get_txout(outpoint), Some(txout));
285287
}
286288

289+
#[test]
290+
fn insert_signed_tx_displaces_unsigned() {
291+
let previous_output = OutPoint::new(hash!("prev"), 2);
292+
let unsigned_tx = Transaction {
293+
version: transaction::Version::ONE,
294+
lock_time: absolute::LockTime::ZERO,
295+
input: vec![TxIn {
296+
previous_output,
297+
script_sig: ScriptBuf::default(),
298+
sequence: transaction::Sequence::ENABLE_RBF_NO_LOCKTIME,
299+
witness: Witness::default(),
300+
}],
301+
output: vec![TxOut {
302+
value: Amount::from_sat(24_000),
303+
script_pubkey: ScriptBuf::default(),
304+
}],
305+
};
306+
let signed_tx = Transaction {
307+
input: vec![TxIn {
308+
previous_output,
309+
script_sig: ScriptBuf::default(),
310+
sequence: transaction::Sequence::ENABLE_RBF_NO_LOCKTIME,
311+
witness: Witness::from_slice(&[
312+
// Random witness from mempool.space
313+
Vec::from_hex("d59118058bf9e8604cec5c0b4a13430b07286482784da313594e932faad074dc4bd27db7cbfff9ad32450db097342d0148ec21c3033b0c27888fd2fd0de2e9b5")
314+
.unwrap(),
315+
]),
316+
}],
317+
..unsigned_tx.clone()
318+
};
319+
320+
// Signed tx must displace unsigned.
321+
{
322+
let mut tx_graph = TxGraph::<ConfirmationBlockTime>::default();
323+
let changeset_insert_unsigned = tx_graph.insert_tx(unsigned_tx.clone());
324+
let changeset_insert_signed = tx_graph.insert_tx(signed_tx.clone());
325+
assert_eq!(
326+
changeset_insert_unsigned,
327+
ChangeSet {
328+
txs: [Arc::new(unsigned_tx.clone())].into(),
329+
..Default::default()
330+
}
331+
);
332+
assert_eq!(
333+
changeset_insert_signed,
334+
ChangeSet {
335+
txs: [Arc::new(signed_tx.clone())].into(),
336+
..Default::default()
337+
}
338+
);
339+
}
340+
341+
// Unsigned tx must not displace signed.
342+
{
343+
let mut tx_graph = TxGraph::<ConfirmationBlockTime>::default();
344+
let changeset_insert_signed = tx_graph.insert_tx(signed_tx.clone());
345+
let changeset_insert_unsigned = tx_graph.insert_tx(unsigned_tx.clone());
346+
assert_eq!(
347+
changeset_insert_signed,
348+
ChangeSet {
349+
txs: [Arc::new(signed_tx)].into(),
350+
..Default::default()
351+
}
352+
);
353+
assert!(changeset_insert_unsigned.is_empty());
354+
}
355+
}
356+
287357
#[test]
288358
fn insert_txout_does_not_displace_tx() {
289359
let mut tx_graph = TxGraph::<ConfirmationBlockTime>::default();

0 commit comments

Comments
 (0)