1use bitcoin::{Block, OutPoint, TxOut};
17use std::collections::{HashMap, HashSet};
18use subcoin_primitives::runtime::Coin;
19
20pub trait UtxoProvider {
29 fn get(&self, outpoint: &OutPoint) -> Option<Coin>;
33
34 fn batch_get(&self, outpoints: &[OutPoint]) -> HashMap<OutPoint, Coin> {
42 outpoints
43 .iter()
44 .filter_map(|op| self.get(op).map(|coin| (*op, coin)))
45 .collect()
46 }
47}
48
49impl UtxoProvider for HashMap<OutPoint, Coin> {
54 fn get(&self, outpoint: &OutPoint) -> Option<Coin> {
55 HashMap::get(self, outpoint).cloned()
56 }
57
58 fn batch_get(&self, outpoints: &[OutPoint]) -> HashMap<OutPoint, Coin> {
59 outpoints
60 .iter()
61 .filter_map(|op| HashMap::get(self, op).map(|coin| (*op, coin.clone())))
62 .collect()
63 }
64}
65
66#[derive(Debug, Clone)]
74pub struct TxExecutionLevels {
75 levels: Vec<Vec<usize>>,
77}
78
79impl TxExecutionLevels {
80 pub fn build(block: &Block) -> Self {
85 let tx_count = block.txdata.len();
86
87 if tx_count == 0 {
88 return Self { levels: vec![] };
89 }
90
91 let mut output_creators: HashMap<OutPoint, usize> = HashMap::new();
93
94 let mut dependencies: Vec<HashSet<usize>> = vec![HashSet::new(); tx_count];
97
98 for (tx_idx, tx) in block.txdata.iter().enumerate() {
99 let txid = tx.compute_txid();
100
101 for vout in 0..tx.output.len() {
103 let outpoint = OutPoint {
104 txid,
105 vout: vout as u32,
106 };
107 output_creators.insert(outpoint, tx_idx);
108 }
109
110 if tx_idx > 0 {
112 for input in &tx.input {
113 if let Some(&creator_idx) = output_creators.get(&input.previous_output) {
114 dependencies[tx_idx].insert(creator_idx);
115 }
116 }
117 }
118 }
119
120 Self::topological_levels(&dependencies)
122 }
123
124 fn topological_levels(dependencies: &[HashSet<usize>]) -> Self {
126 let n = dependencies.len();
127
128 if n == 0 {
129 return Self { levels: vec![] };
130 }
131
132 let mut in_degree: Vec<usize> = dependencies.iter().map(|deps| deps.len()).collect();
134
135 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
137 for (tx_idx, deps) in dependencies.iter().enumerate() {
138 for &dep in deps {
139 dependents[dep].push(tx_idx);
140 }
141 }
142
143 let mut levels = Vec::new();
144 let mut remaining: HashSet<usize> = (0..n).collect();
145
146 while !remaining.is_empty() {
147 let level: Vec<usize> = remaining
149 .iter()
150 .filter(|&&idx| in_degree[idx] == 0)
151 .copied()
152 .collect();
153
154 if level.is_empty() {
155 panic!("Cycle detected in transaction dependencies - invalid block");
158 }
159
160 for &tx_idx in &level {
162 remaining.remove(&tx_idx);
163 for &dependent in &dependents[tx_idx] {
164 in_degree[dependent] = in_degree[dependent].saturating_sub(1);
165 }
166 }
167
168 levels.push(level);
169 }
170
171 Self { levels }
172 }
173
174 pub fn depth(&self) -> usize {
179 self.levels.len()
180 }
181
182 pub fn is_fully_parallel(&self) -> bool {
184 self.levels.len() <= 1
185 }
186
187 pub fn iter(&self) -> impl Iterator<Item = &[usize]> {
189 self.levels.iter().map(|v| v.as_slice())
190 }
191
192 pub fn get_level(&self, level: usize) -> Option<&[usize]> {
194 self.levels.get(level).map(|v| v.as_slice())
195 }
196
197 pub fn tx_count(&self) -> usize {
199 self.levels.iter().map(|l| l.len()).sum()
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct InBlockOutput {
206 pub txout: TxOut,
208 pub is_coinbase: bool,
210 pub tx_index: usize,
212}
213
214#[derive(Debug)]
221pub struct PreparedBlock {
222 pub block: Block,
224 pub height: u32,
226 pub input_utxos: HashMap<OutPoint, Coin>,
230 pub in_block_outputs: HashMap<OutPoint, InBlockOutput>,
232 pub execution_levels: TxExecutionLevels,
234}
235
236impl PreparedBlock {
237 pub fn prepare<P: UtxoProvider>(block: Block, height: u32, utxo_provider: &P) -> Self {
245 let mut in_block_outputs: HashMap<OutPoint, InBlockOutput> = HashMap::new();
247
248 for (tx_idx, tx) in block.txdata.iter().enumerate() {
249 let txid = tx.compute_txid();
250 let is_coinbase = tx_idx == 0;
251
252 for (vout, txout) in tx.output.iter().enumerate() {
253 let outpoint = OutPoint {
254 txid,
255 vout: vout as u32,
256 };
257 in_block_outputs.insert(
258 outpoint,
259 InBlockOutput {
260 txout: txout.clone(),
261 is_coinbase,
262 tx_index: tx_idx,
263 },
264 );
265 }
266 }
267
268 let mut external_outpoints: Vec<OutPoint> = Vec::new();
270
271 for tx in block.txdata.iter().skip(1) {
272 for input in &tx.input {
274 if !in_block_outputs.contains_key(&input.previous_output) {
275 external_outpoints.push(input.previous_output);
276 }
277 }
278 }
279
280 let input_utxos = utxo_provider.batch_get(&external_outpoints);
282
283 let execution_levels = TxExecutionLevels::build(&block);
285
286 Self {
287 block,
288 height,
289 input_utxos,
290 in_block_outputs,
291 execution_levels,
292 }
293 }
294
295 pub fn get_utxo(&self, outpoint: &OutPoint) -> Option<(TxOut, bool, u32)> {
300 if let Some(in_block) = self.in_block_outputs.get(outpoint) {
302 return Some((in_block.txout.clone(), in_block.is_coinbase, self.height));
303 }
304
305 if let Some(coin) = self.input_utxos.get(outpoint) {
307 let txout = TxOut {
308 value: bitcoin::Amount::from_sat(coin.amount),
309 script_pubkey: bitcoin::ScriptBuf::from_bytes(coin.script_pubkey.clone()),
310 };
311 return Some((txout, coin.is_coinbase, coin.height));
312 }
313
314 None
315 }
316
317 pub fn missing_utxos(&self) -> Vec<OutPoint> {
321 let mut missing = Vec::new();
322
323 for tx in self.block.txdata.iter().skip(1) {
324 for input in &tx.input {
325 let outpoint = &input.previous_output;
326 if !self.in_block_outputs.contains_key(outpoint)
327 && !self.input_utxos.contains_key(outpoint)
328 {
329 missing.push(*outpoint);
330 }
331 }
332 }
333
334 missing
335 }
336
337 pub fn tx_count(&self) -> usize {
339 self.block.txdata.len()
340 }
341
342 pub fn block_hash(&self) -> bitcoin::BlockHash {
344 self.block.block_hash()
345 }
346
347 pub fn into_verified(self, script_verification_duration: std::time::Duration) -> VerifiedBlock {
352 VerifiedBlock {
353 block: self.block,
354 height: self.height,
355 spent_utxos: self.input_utxos,
356 script_verification_duration,
357 }
358 }
359}
360
361#[derive(Debug)]
366pub struct VerifiedBlock {
367 pub block: Block,
369 pub height: u32,
371 pub spent_utxos: HashMap<OutPoint, Coin>,
375 pub script_verification_duration: std::time::Duration,
377}
378
379impl VerifiedBlock {
380 pub fn block_hash(&self) -> bitcoin::BlockHash {
382 self.block.block_hash()
383 }
384
385 pub fn get_spent_utxo(&self, outpoint: &OutPoint) -> Option<&Coin> {
389 self.spent_utxos.get(outpoint)
390 }
391
392 pub fn tx_count(&self) -> usize {
394 self.block.txdata.len()
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use bitcoin::absolute::LockTime;
402 use bitcoin::blockdata::block::{Header, Version};
403 use bitcoin::blockdata::transaction::{Transaction, TxIn, Version as TxVersion};
404 use bitcoin::hashes::Hash;
405 use bitcoin::{Amount, CompactTarget, ScriptBuf, Sequence, Witness};
406
407 fn create_coinbase(height: u32) -> Transaction {
408 let script = vec![0x01, height as u8];
409 Transaction {
410 version: TxVersion::TWO,
411 lock_time: LockTime::ZERO,
412 input: vec![TxIn {
413 previous_output: OutPoint::null(),
414 script_sig: ScriptBuf::from_bytes(script),
415 sequence: Sequence::MAX,
416 witness: Witness::new(),
417 }],
418 output: vec![bitcoin::TxOut {
419 value: Amount::from_sat(5_000_000_000),
420 script_pubkey: ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::all_zeros()),
421 }],
422 }
423 }
424
425 fn create_spending_tx(inputs: &[OutPoint]) -> Transaction {
426 Transaction {
427 version: TxVersion::TWO,
428 lock_time: LockTime::ZERO,
429 input: inputs
430 .iter()
431 .map(|op| TxIn {
432 previous_output: *op,
433 script_sig: ScriptBuf::new(),
434 sequence: Sequence::MAX,
435 witness: Witness::new(),
436 })
437 .collect(),
438 output: vec![bitcoin::TxOut {
439 value: Amount::from_sat(1_000_000),
440 script_pubkey: ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::all_zeros()),
441 }],
442 }
443 }
444
445 fn create_block(txs: Vec<Transaction>) -> Block {
446 Block {
447 header: Header {
448 version: Version::TWO,
449 prev_blockhash: bitcoin::BlockHash::all_zeros(),
450 merkle_root: bitcoin::TxMerkleNode::all_zeros(),
451 time: 0,
452 bits: CompactTarget::from_consensus(0),
453 nonce: 0,
454 },
455 txdata: txs,
456 }
457 }
458
459 fn test_coin(amount: u64) -> Coin {
460 Coin {
461 is_coinbase: false,
462 amount,
463 height: 100,
464 script_pubkey: vec![0x51],
465 }
466 }
467
468 #[test]
469 fn test_hashmap_provider() {
470 let mut utxos = HashMap::new();
471 let outpoint1 = OutPoint {
472 txid: bitcoin::Txid::from_byte_array([1; 32]),
473 vout: 0,
474 };
475 let outpoint2 = OutPoint {
476 txid: bitcoin::Txid::from_byte_array([2; 32]),
477 vout: 0,
478 };
479 utxos.insert(outpoint1, test_coin(1000));
480 utxos.insert(outpoint2, test_coin(2000));
481
482 assert!(utxos.get(&outpoint1).is_some());
484 let missing = OutPoint {
485 txid: bitcoin::Txid::from_byte_array([99; 32]),
486 vout: 0,
487 };
488 assert!(UtxoProvider::get(&utxos, &missing).is_none());
489
490 let result = utxos.batch_get(&[outpoint1, outpoint2, missing]);
492 assert_eq!(result.len(), 2);
493 assert!(result.contains_key(&outpoint1));
494 assert!(result.contains_key(&outpoint2));
495 assert!(!result.contains_key(&missing));
496 }
497
498 #[test]
499 fn test_prepared_block_external_utxos() {
500 let coinbase = create_coinbase(1);
501
502 let external_utxo = OutPoint {
503 txid: bitcoin::Txid::from_byte_array([99; 32]),
504 vout: 0,
505 };
506
507 let tx1 = create_spending_tx(&[external_utxo]);
508 let block = create_block(vec![coinbase, tx1]);
509
510 let mut utxos: HashMap<OutPoint, Coin> = HashMap::new();
512 utxos.insert(
513 external_utxo,
514 Coin {
515 is_coinbase: false,
516 amount: 5_000_000,
517 height: 50,
518 script_pubkey: vec![0x51],
519 },
520 );
521
522 let prepared = PreparedBlock::prepare(block, 100, &utxos);
523
524 assert!(prepared.input_utxos.contains_key(&external_utxo));
526 assert!(prepared.missing_utxos().is_empty());
527
528 let (_, is_coinbase, height) = prepared.get_utxo(&external_utxo).unwrap();
530 assert!(!is_coinbase);
531 assert_eq!(height, 50); }
533
534 #[test]
535 fn test_prepared_block_in_block_spending() {
536 let coinbase = create_coinbase(1);
537 let coinbase_out = OutPoint {
538 txid: coinbase.compute_txid(),
539 vout: 0,
540 };
541
542 let tx1 = create_spending_tx(&[coinbase_out]);
544 let block = create_block(vec![coinbase, tx1]);
545
546 let utxos: HashMap<OutPoint, Coin> = HashMap::new();
548
549 let prepared = PreparedBlock::prepare(block, 100, &utxos);
550
551 assert!(prepared.in_block_outputs.contains_key(&coinbase_out));
553 assert!(prepared.missing_utxos().is_empty());
554
555 let (_, is_coinbase, height) = prepared.get_utxo(&coinbase_out).unwrap();
557 assert!(is_coinbase);
558 assert_eq!(height, 100); }
560
561 #[test]
562 fn test_prepared_block_missing_utxo() {
563 let coinbase = create_coinbase(1);
564
565 let external_utxo = OutPoint {
566 txid: bitcoin::Txid::from_byte_array([99; 32]),
567 vout: 0,
568 };
569
570 let tx1 = create_spending_tx(&[external_utxo]);
571 let block = create_block(vec![coinbase, tx1]);
572
573 let utxos: HashMap<OutPoint, Coin> = HashMap::new();
575
576 let prepared = PreparedBlock::prepare(block, 100, &utxos);
577
578 let missing = prepared.missing_utxos();
580 assert_eq!(missing.len(), 1);
581 assert_eq!(missing[0], external_utxo);
582 }
583
584 #[test]
587 fn test_execution_levels_no_dependencies() {
588 let coinbase = create_coinbase(1);
590
591 let external_utxo1 = OutPoint {
592 txid: bitcoin::Txid::from_byte_array([1; 32]),
593 vout: 0,
594 };
595 let external_utxo2 = OutPoint {
596 txid: bitcoin::Txid::from_byte_array([2; 32]),
597 vout: 0,
598 };
599 let external_utxo3 = OutPoint {
600 txid: bitcoin::Txid::from_byte_array([3; 32]),
601 vout: 0,
602 };
603
604 let tx1 = create_spending_tx(&[external_utxo1]);
605 let tx2 = create_spending_tx(&[external_utxo2]);
606 let tx3 = create_spending_tx(&[external_utxo3]);
607
608 let block = create_block(vec![coinbase, tx1, tx2, tx3]);
609 let levels = TxExecutionLevels::build(&block);
610
611 assert_eq!(levels.depth(), 1);
613 assert!(levels.is_fully_parallel());
614 assert_eq!(levels.tx_count(), 4);
615
616 let level0 = levels.get_level(0).unwrap();
617 assert_eq!(level0.len(), 4);
618 }
619
620 #[test]
621 fn test_execution_levels_chain_dependency() {
622 let coinbase = create_coinbase(1);
624 let coinbase_out = OutPoint {
625 txid: coinbase.compute_txid(),
626 vout: 0,
627 };
628
629 let tx1 = create_spending_tx(&[coinbase_out]);
630 let tx1_out = OutPoint {
631 txid: tx1.compute_txid(),
632 vout: 0,
633 };
634
635 let tx2 = create_spending_tx(&[tx1_out]);
636 let tx2_out = OutPoint {
637 txid: tx2.compute_txid(),
638 vout: 0,
639 };
640
641 let tx3 = create_spending_tx(&[tx2_out]);
642
643 let block = create_block(vec![coinbase, tx1, tx2, tx3]);
644 let levels = TxExecutionLevels::build(&block);
645
646 assert_eq!(levels.depth(), 4);
648 assert!(!levels.is_fully_parallel());
649
650 assert_eq!(levels.get_level(0).unwrap(), &[0]); assert_eq!(levels.get_level(1).unwrap(), &[1]); assert_eq!(levels.get_level(2).unwrap(), &[2]); assert_eq!(levels.get_level(3).unwrap(), &[3]); }
655
656 #[test]
657 fn test_execution_levels_diamond_dependency() {
658 let coinbase = create_coinbase(1);
666
667 let mut coinbase_2out = coinbase.clone();
669 coinbase_2out.output.push(bitcoin::TxOut {
670 value: Amount::from_sat(1_000_000),
671 script_pubkey: ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::all_zeros()),
672 });
673
674 let coinbase_out0 = OutPoint {
675 txid: coinbase_2out.compute_txid(),
676 vout: 0,
677 };
678 let coinbase_out1 = OutPoint {
679 txid: coinbase_2out.compute_txid(),
680 vout: 1,
681 };
682
683 let tx1 = create_spending_tx(&[coinbase_out0]);
684 let tx1_out = OutPoint {
685 txid: tx1.compute_txid(),
686 vout: 0,
687 };
688
689 let tx2 = create_spending_tx(&[coinbase_out1]);
690 let tx2_out = OutPoint {
691 txid: tx2.compute_txid(),
692 vout: 0,
693 };
694
695 let tx3 = create_spending_tx(&[tx1_out, tx2_out]);
696
697 let block = create_block(vec![coinbase_2out, tx1, tx2, tx3]);
698 let levels = TxExecutionLevels::build(&block);
699
700 assert_eq!(levels.depth(), 3);
705
706 assert_eq!(levels.get_level(0).unwrap(), &[0]);
707 let level1 = levels.get_level(1).unwrap();
708 assert_eq!(level1.len(), 2);
709 assert!(level1.contains(&1));
710 assert!(level1.contains(&2));
711 assert_eq!(levels.get_level(2).unwrap(), &[3]);
712 }
713
714 #[test]
715 fn test_empty_block() {
716 let block = create_block(vec![]);
717 let levels = TxExecutionLevels::build(&block);
718
719 assert_eq!(levels.depth(), 0);
720 assert!(levels.is_fully_parallel());
721 assert_eq!(levels.tx_count(), 0);
722 }
723
724 #[test]
725 fn test_coinbase_only_block() {
726 let coinbase = create_coinbase(1);
727 let block = create_block(vec![coinbase]);
728
729 let levels = TxExecutionLevels::build(&block);
730
731 assert_eq!(levels.depth(), 1);
732 assert!(levels.is_fully_parallel());
733 assert_eq!(levels.tx_count(), 1);
734 }
735}