2026-01-21 12:34:59 +01:00
|
|
|
// Copyright (C) 2024-2026 Whiterun LLC
|
2024-10-21 15:13:16 +02:00
|
|
|
//
|
|
|
|
|
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
|
|
|
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
|
|
|
|
|
2025-08-15 18:53:34 +02:00
|
|
|
use std::collections::{HashMap, VecDeque};
|
2024-10-21 15:13:16 +02:00
|
|
|
|
|
|
|
|
use bitcoincash::{Transaction, Txid};
|
|
|
|
|
|
2025-08-15 18:53:34 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
|
use std::collections::HashSet;
|
|
|
|
|
|
|
|
|
|
// TTOR sort a list of transactions (old algo; used in tests for comparison)
|
|
|
|
|
#[cfg(test)]
|
2024-10-21 15:13:16 +02:00
|
|
|
pub fn ttor_sorted(txs: Vec<Transaction>) -> Vec<Transaction> {
|
|
|
|
|
let txs = {
|
|
|
|
|
let mut queue: VecDeque<Transaction> = txs.into_iter().collect();
|
|
|
|
|
|
2025-08-15 18:53:34 +02:00
|
|
|
let mut queue_txids: HashSet<Txid> = queue.iter().map(|tx| tx.txid()).collect();
|
2024-10-21 15:13:16 +02:00
|
|
|
|
|
|
|
|
let mut txs: Vec<Transaction> = Vec::with_capacity(queue.len());
|
|
|
|
|
|
|
|
|
|
while let Some(tx) = queue.pop_front() {
|
|
|
|
|
let mut has_parent = false;
|
|
|
|
|
|
|
|
|
|
for i in &tx.input {
|
|
|
|
|
if queue_txids.contains(&i.previous_output.txid) {
|
|
|
|
|
// depends on parent
|
|
|
|
|
has_parent = true;
|
|
|
|
|
break;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if has_parent {
|
|
|
|
|
queue.push_back(tx);
|
|
|
|
|
} else {
|
|
|
|
|
queue_txids.remove(&tx.txid());
|
2025-08-15 18:53:34 +02:00
|
|
|
txs.push(tx);
|
2024-10-21 15:13:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
txs
|
|
|
|
|
};
|
|
|
|
|
txs
|
|
|
|
|
}
|
2025-08-15 18:53:34 +02:00
|
|
|
|
|
|
|
|
/// Alternative implementation using Kahn's algorithm for topological sorting
|
|
|
|
|
/// This is more efficient with O(V + E) complexity and handles cycles gracefully
|
|
|
|
|
///
|
|
|
|
|
/// Note: This implementation may produce different but equally valid topological orderings
|
|
|
|
|
/// compared to the original `ttor_sorted` function. Both implementations respect
|
|
|
|
|
/// transaction dependencies, but may order independent transactions differently.
|
|
|
|
|
pub fn ttor_sorted_kahn(txs: Vec<Transaction>) -> Vec<Transaction> {
|
|
|
|
|
if txs.is_empty() {
|
|
|
|
|
return Vec::new();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build adjacency list and in-degree count
|
|
|
|
|
let mut graph: HashMap<Txid, Vec<Txid>> = HashMap::new();
|
|
|
|
|
let mut in_degree: HashMap<Txid, usize> = HashMap::new();
|
|
|
|
|
let mut tx_map: HashMap<Txid, Transaction> = HashMap::new();
|
|
|
|
|
|
|
|
|
|
// Initialize data structures
|
|
|
|
|
for tx in txs {
|
|
|
|
|
let txid = tx.txid();
|
|
|
|
|
tx_map.insert(txid, tx);
|
|
|
|
|
in_degree.insert(txid, 0);
|
|
|
|
|
graph.insert(txid, Vec::new());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build the dependency graph
|
|
|
|
|
for (txid, tx) in &tx_map {
|
|
|
|
|
for input in &tx.input {
|
|
|
|
|
let parent_txid = input.previous_output.txid;
|
|
|
|
|
|
|
|
|
|
// Only consider dependencies within our transaction set
|
|
|
|
|
if tx_map.contains_key(&parent_txid) {
|
|
|
|
|
// Add edge from parent to current transaction
|
|
|
|
|
graph.entry(parent_txid).or_default().push(*txid);
|
|
|
|
|
|
|
|
|
|
// Increment in-degree of current transaction
|
|
|
|
|
*in_degree.entry(*txid).or_default() += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Kahn's algorithm: find nodes with no incoming edges
|
|
|
|
|
let mut queue: VecDeque<Txid> = VecDeque::new();
|
|
|
|
|
for (txid, °ree) in &in_degree {
|
|
|
|
|
if degree == 0 {
|
|
|
|
|
queue.push_back(*txid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut result: Vec<Transaction> = Vec::with_capacity(tx_map.len());
|
|
|
|
|
|
|
|
|
|
// Process nodes in topological order
|
|
|
|
|
while let Some(txid) = queue.pop_front() {
|
|
|
|
|
result.push(tx_map.remove(&txid).unwrap());
|
|
|
|
|
|
|
|
|
|
// Remove edges from this node and update in-degrees
|
|
|
|
|
if let Some(children) = graph.get(&txid) {
|
|
|
|
|
for &child_txid in children {
|
|
|
|
|
if let Some(degree) = in_degree.get_mut(&child_txid) {
|
|
|
|
|
*degree -= 1;
|
|
|
|
|
if *degree == 0 {
|
|
|
|
|
queue.push_back(child_txid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add any remaining transactions (handles cycles and orphaned transactions)
|
|
|
|
|
// This matches the original implementation's behavior
|
|
|
|
|
for (_, tx) in tx_map {
|
|
|
|
|
result.push(tx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use bitcoin_hashes::{sha256d, Hash};
|
|
|
|
|
use bitcoincash::{OutPoint, PackedLockTime, Script, Sequence, TxIn, TxOut, Witness};
|
|
|
|
|
|
|
|
|
|
fn create_mock_transaction(txid: [u8; 32], inputs: Vec<[u8; 32]>) -> Transaction {
|
|
|
|
|
let tx_inputs: Vec<TxIn> = inputs
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|input_txid| TxIn {
|
|
|
|
|
previous_output: OutPoint {
|
|
|
|
|
txid: Txid::from_hash(sha256d::Hash::from_inner(input_txid)),
|
|
|
|
|
vout: 0,
|
|
|
|
|
},
|
|
|
|
|
script_sig: Script::new(),
|
|
|
|
|
sequence: Sequence(0),
|
|
|
|
|
witness: Witness::new(),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Create a transaction with a unique txid by using the provided txid parameter
|
|
|
|
|
// We need to create a transaction that has the desired txid when txid() is called
|
|
|
|
|
let mut tx = Transaction {
|
|
|
|
|
version: 1,
|
|
|
|
|
lock_time: PackedLockTime(0),
|
|
|
|
|
input: tx_inputs,
|
|
|
|
|
output: vec![TxOut {
|
|
|
|
|
value: 1000,
|
|
|
|
|
script_pubkey: Script::new(),
|
|
|
|
|
token: None,
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// For testing purposes, we'll create transactions with different outputs to ensure different txids
|
|
|
|
|
// The txid is calculated from the transaction content, so we need to make them different
|
|
|
|
|
tx.output[0].value = txid[0] as u64 * 1000; // Use first byte of txid to make value unique
|
|
|
|
|
|
|
|
|
|
tx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a mock transaction with proper dependency relationships
|
|
|
|
|
/// This function creates transactions where input txids actually match the txids of referenced transactions
|
|
|
|
|
fn create_mock_transaction_with_deps(
|
|
|
|
|
base_txid: [u8; 32],
|
|
|
|
|
parent_txs: &[Transaction],
|
|
|
|
|
) -> Transaction {
|
|
|
|
|
let tx_inputs: Vec<TxIn> = parent_txs
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|parent_tx| TxIn {
|
|
|
|
|
previous_output: OutPoint {
|
|
|
|
|
txid: parent_tx.txid(),
|
|
|
|
|
vout: 0,
|
|
|
|
|
},
|
|
|
|
|
script_sig: Script::new(),
|
|
|
|
|
sequence: Sequence(0),
|
|
|
|
|
witness: Witness::new(),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let mut tx = Transaction {
|
|
|
|
|
version: 1,
|
|
|
|
|
lock_time: PackedLockTime(0),
|
|
|
|
|
input: tx_inputs,
|
|
|
|
|
output: vec![TxOut {
|
|
|
|
|
value: 1000,
|
|
|
|
|
script_pubkey: Script::new(),
|
|
|
|
|
token: None,
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Make the transaction unique by using the base_txid
|
|
|
|
|
tx.output[0].value = base_txid[0] as u64 * 1000;
|
|
|
|
|
|
|
|
|
|
tx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_simple_chain() {
|
|
|
|
|
// Create a simple chain: A -> B -> C
|
|
|
|
|
let tx_a = create_mock_transaction([1; 32], vec![]);
|
|
|
|
|
let tx_b = create_mock_transaction_with_deps([2; 32], &[tx_a.clone()]);
|
|
|
|
|
let tx_c = create_mock_transaction_with_deps([3; 32], &[tx_b.clone()]);
|
|
|
|
|
|
|
|
|
|
let txs = vec![tx_c.clone(), tx_a.clone(), tx_b.clone()];
|
|
|
|
|
|
|
|
|
|
let result_original = ttor_sorted(txs.clone());
|
|
|
|
|
let result_kahn = ttor_sorted_kahn(txs);
|
|
|
|
|
|
|
|
|
|
// Both should produce the same number of transactions
|
|
|
|
|
assert_eq!(result_original.len(), 3);
|
|
|
|
|
assert_eq!(result_kahn.len(), 3);
|
|
|
|
|
|
|
|
|
|
// Both should contain all transactions
|
|
|
|
|
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
assert_eq!(original_txids, kahn_txids);
|
|
|
|
|
|
|
|
|
|
// Verify dependencies are respected in both results
|
|
|
|
|
verify_dependencies_respected(&result_original);
|
|
|
|
|
verify_dependencies_respected(&result_kahn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_diamond_dependency() {
|
|
|
|
|
// Create a diamond dependency: A -> B, A -> C, B -> D, C -> D
|
|
|
|
|
let tx_a = create_mock_transaction([1; 32], vec![]);
|
|
|
|
|
let tx_b = create_mock_transaction_with_deps([2; 32], &[tx_a.clone()]);
|
|
|
|
|
let tx_c = create_mock_transaction_with_deps([3; 32], &[tx_a.clone()]);
|
|
|
|
|
let tx_d = create_mock_transaction_with_deps([4; 32], &[tx_b.clone(), tx_c.clone()]);
|
|
|
|
|
|
|
|
|
|
let txs = vec![tx_d.clone(), tx_c.clone(), tx_b.clone(), tx_a.clone()];
|
|
|
|
|
|
|
|
|
|
let result_original = ttor_sorted(txs.clone());
|
|
|
|
|
let result_kahn = ttor_sorted_kahn(txs);
|
|
|
|
|
|
|
|
|
|
// Both should produce the same number of transactions
|
|
|
|
|
assert_eq!(result_original.len(), 4);
|
|
|
|
|
assert_eq!(result_kahn.len(), 4);
|
|
|
|
|
|
|
|
|
|
// Both should contain all transactions
|
|
|
|
|
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
assert_eq!(original_txids, kahn_txids);
|
|
|
|
|
|
|
|
|
|
// Verify dependencies are respected in both results
|
|
|
|
|
verify_dependencies_respected(&result_original);
|
|
|
|
|
verify_dependencies_respected(&result_kahn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_independent_transactions() {
|
|
|
|
|
// Create independent transactions (no dependencies)
|
|
|
|
|
let tx_a = create_mock_transaction([1; 32], vec![]);
|
|
|
|
|
let tx_b = create_mock_transaction([2; 32], vec![]);
|
|
|
|
|
let tx_c = create_mock_transaction([3; 32], vec![]);
|
|
|
|
|
|
|
|
|
|
let txs = vec![tx_c.clone(), tx_a.clone(), tx_b.clone()];
|
|
|
|
|
|
|
|
|
|
let result_original = ttor_sorted(txs.clone());
|
|
|
|
|
let result_kahn = ttor_sorted_kahn(txs);
|
|
|
|
|
|
|
|
|
|
// Both should produce the same number of transactions
|
|
|
|
|
assert_eq!(result_original.len(), 3);
|
|
|
|
|
assert_eq!(result_kahn.len(), 3);
|
|
|
|
|
|
|
|
|
|
// Both should contain all transactions
|
|
|
|
|
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
assert_eq!(original_txids, kahn_txids);
|
|
|
|
|
|
|
|
|
|
// For independent transactions, both should produce valid orderings
|
|
|
|
|
verify_dependencies_respected(&result_original);
|
|
|
|
|
verify_dependencies_respected(&result_kahn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_empty_input() {
|
|
|
|
|
let txs: Vec<Transaction> = vec![];
|
|
|
|
|
|
|
|
|
|
let result_original = ttor_sorted(txs.clone());
|
|
|
|
|
let result_kahn = ttor_sorted_kahn(txs);
|
|
|
|
|
|
|
|
|
|
assert_eq!(result_original.len(), 0);
|
|
|
|
|
assert_eq!(result_kahn.len(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_single_transaction() {
|
|
|
|
|
let tx = create_mock_transaction([1; 32], vec![]);
|
|
|
|
|
let txs = vec![tx.clone()];
|
|
|
|
|
|
|
|
|
|
let result_original = ttor_sorted(txs.clone());
|
|
|
|
|
let result_kahn = ttor_sorted_kahn(txs);
|
|
|
|
|
|
|
|
|
|
assert_eq!(result_original.len(), 1);
|
|
|
|
|
assert_eq!(result_kahn.len(), 1);
|
|
|
|
|
assert_eq!(result_original[0].txid(), tx.txid());
|
|
|
|
|
assert_eq!(result_kahn[0].txid(), tx.txid());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_complex_dependency() {
|
|
|
|
|
// Create a more complex dependency graph
|
|
|
|
|
// A -> B -> D
|
|
|
|
|
// A -> C -> D
|
|
|
|
|
// E -> F -> G
|
|
|
|
|
// H (independent)
|
|
|
|
|
let tx_a = create_mock_transaction([1; 32], vec![]);
|
|
|
|
|
let tx_b = create_mock_transaction_with_deps([2; 32], &[tx_a.clone()]);
|
|
|
|
|
let tx_c = create_mock_transaction_with_deps([3; 32], &[tx_a.clone()]);
|
|
|
|
|
let tx_d = create_mock_transaction_with_deps([4; 32], &[tx_b.clone(), tx_c.clone()]);
|
|
|
|
|
let tx_e = create_mock_transaction([5; 32], vec![]);
|
|
|
|
|
let tx_f = create_mock_transaction_with_deps([6; 32], &[tx_e.clone()]);
|
|
|
|
|
let tx_g = create_mock_transaction_with_deps([7; 32], &[tx_f.clone()]);
|
|
|
|
|
let tx_h = create_mock_transaction([8; 32], vec![]);
|
|
|
|
|
|
|
|
|
|
let txs = vec![
|
|
|
|
|
tx_g.clone(),
|
|
|
|
|
tx_f.clone(),
|
|
|
|
|
tx_e.clone(),
|
|
|
|
|
tx_d.clone(),
|
|
|
|
|
tx_c.clone(),
|
|
|
|
|
tx_b.clone(),
|
|
|
|
|
tx_a.clone(),
|
|
|
|
|
tx_h.clone(),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let result_original = ttor_sorted(txs.clone());
|
|
|
|
|
let result_kahn = ttor_sorted_kahn(txs);
|
|
|
|
|
|
|
|
|
|
// Both should produce the same number of transactions
|
|
|
|
|
assert_eq!(result_original.len(), 8);
|
|
|
|
|
assert_eq!(result_kahn.len(), 8);
|
|
|
|
|
|
|
|
|
|
// Both should contain all transactions
|
|
|
|
|
let original_txids: HashSet<Txid> = result_original.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.txid()).collect();
|
|
|
|
|
assert_eq!(original_txids, kahn_txids);
|
|
|
|
|
|
|
|
|
|
// Verify dependencies are respected in both results
|
|
|
|
|
verify_dependencies_respected(&result_original);
|
|
|
|
|
verify_dependencies_respected(&result_kahn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Helper function to verify that dependencies are respected in a transaction ordering
|
|
|
|
|
fn verify_dependencies_respected(txs: &[Transaction]) {
|
|
|
|
|
let tx_positions: HashMap<Txid, usize> = txs
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(pos, tx)| (tx.txid(), pos))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
for (pos, tx) in txs.iter().enumerate() {
|
|
|
|
|
for input in &tx.input {
|
|
|
|
|
if let Some(&parent_pos) = tx_positions.get(&input.previous_output.txid) {
|
|
|
|
|
// Parent transaction should come before this transaction
|
|
|
|
|
assert!(
|
|
|
|
|
parent_pos < pos,
|
|
|
|
|
"Dependency violation: transaction {} (pos {}) depends on {} (pos {})",
|
|
|
|
|
tx.txid(),
|
|
|
|
|
pos,
|
|
|
|
|
input.previous_output.txid,
|
|
|
|
|
parent_pos
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|