riftenlabs-indexer/src/utiltx.rs
Dagur Valberg Johannsson 1614b84674 Upgrade bitcoincash to 0.32 and add TokenToken AMM pool indexing
Migrate off the deprecated bitcoincash 0.29 API (Amount/Version types,
FromHex -> FromStr/parse, non-exhaustive Network) across the indexer and
tests.

Add indexing of native token-A <-> token-B (TokenToken) AMM pools:

- tokentoken_pool / tokentoken_pool_history_entry tables in cauldron.db,
  created via an always-run idempotent migration (no DB_VERSION bump, so
  existing databases upgrade in place)
- block-path indexing sharing the cauldron write transaction and reorg
  undo, with creation/swap/withdrawal state tracking and reserve deltas
- mempool indexing: electrum mempool.get filters on the tokentoken
  contract code (spends) and the CONJURE op_return hint (creations);
  first_seen_timestamp reconciles with mtp on confirmation
- RPC endpoints /tokentoken/pool/active (pair lookup, order-insensitive)
  and /tokentoken/tokens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:18:35 +02:00

377 lines
14 KiB
Rust

// Copyright (C) 2024-2026 Whiterun LLC
//
// 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
use std::collections::{HashMap, VecDeque};
use bitcoincash::{Transaction, Txid};
#[cfg(test)]
use std::collections::HashSet;
// TTOR sort a list of transactions (old algo; used in tests for comparison)
#[cfg(test)]
pub fn ttor_sorted(txs: Vec<Transaction>) -> Vec<Transaction> {
let txs = {
let mut queue: VecDeque<Transaction> = txs.into_iter().collect();
let mut queue_txids: HashSet<Txid> = queue.iter().map(|tx| tx.compute_txid()).collect();
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.compute_txid());
txs.push(tx);
}
}
txs
};
txs
}
/// 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.compute_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, &degree) 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::{
locktime::absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptBuf, 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_raw_hash(sha256d::Hash::from_byte_array(input_txid)),
vout: 0,
},
script_sig: ScriptBuf::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: Version::ONE,
lock_time: LockTime::ZERO,
input: tx_inputs,
output: vec![TxOut {
value: Amount::from_sat(1000),
script_pubkey: ScriptBuf::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 = Amount::from_sat(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.compute_txid(),
vout: 0,
},
script_sig: ScriptBuf::new(),
sequence: Sequence(0),
witness: Witness::new(),
})
.collect();
let mut tx = Transaction {
version: Version::ONE,
lock_time: LockTime::ZERO,
input: tx_inputs,
output: vec![TxOut {
value: Amount::from_sat(1000),
script_pubkey: ScriptBuf::new(),
token: None,
}],
};
// Make the transaction unique by using the base_txid
tx.output[0].value = Amount::from_sat(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.compute_txid()).collect();
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_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.compute_txid()).collect();
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_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.compute_txid()).collect();
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_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].compute_txid(), tx.compute_txid());
assert_eq!(result_kahn[0].compute_txid(), tx.compute_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.compute_txid()).collect();
let kahn_txids: HashSet<Txid> = result_kahn.iter().map(|tx| tx.compute_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.compute_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.compute_txid(),
pos,
input.previous_output.txid,
parent_pos
);
}
}
}
}
}