iuna

iuna

iuna - experimental devnet protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

ledger_ops.rs (22209B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 
      3 use anyhow::{Context, Result, bail};
      4 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
      5 
      6 use super::blinded::verify_blinded_input_signatures;
      7 use super::hex::hex_hash;
      8 use super::reveal::{RevealBundleSection, canonical_reveal_bundle_hashes};
      9 use super::selection::{TransactionKind, blinded_fee_rate_key, fee_rate_key};
     10 use super::ticket::ticket_is_eligible_for_height;
     11 use super::transaction::{BlindedTransaction, Transaction};
     12 use super::{
     13     Amount, Block, BlockSelection, BurnTicket, FinalizerMode, LeaderProof, LeaderProofPayload,
     14     Ledger, MINE_REWARD, OutPoint, PUBLIC_KEY_BYTES, RECOVERY_BLOCK_DELAY_MS,
     15     REVEAL_COMMITTEE_SIZE, SIGNATURE_BYTES, TxInput, TxOutput, decode_hex_array, validate_address,
     16     validate_hash, validate_protocol_id, validate_signature,
     17 };
     18 
     19 pub(super) fn validate_genesis_allocations(
     20     genesis_allocations: &BTreeMap<String, Amount>,
     21 ) -> Result<()> {
     22     for address in genesis_allocations.keys() {
     23         validate_address(address, "genesis allocation")?;
     24     }
     25     Ok(())
     26 }
     27 
     28 pub(super) fn validate_transaction_inputs(inputs: &[TxInput]) -> Result<()> {
     29     for input in inputs {
     30         validate_protocol_id(&input.outpoint.txid, "input outpoint txid")?;
     31         validate_address(&input.owner, "input owner")?;
     32         validate_signature(&input.signature, "input signature")?;
     33     }
     34     Ok(())
     35 }
     36 
     37 pub(super) fn validate_transaction_outputs(outputs: &[TxOutput]) -> Result<()> {
     38     for output in outputs {
     39         validate_address(&output.address, "output recipient")?;
     40     }
     41     Ok(())
     42 }
     43 
     44 pub(super) fn validate_genesis_burn_transaction(transaction: &Transaction) -> Result<()> {
     45     let Transaction::Burn {
     46         inputs,
     47         change,
     48         fee,
     49         signature,
     50         ..
     51     } = transaction
     52     else {
     53         bail!("genesis only supports burn transactions");
     54     };
     55     if *fee != 0 {
     56         bail!("genesis burn fee must be zero");
     57     }
     58     validate_hash(signature, "genesis burn signature")?;
     59     validate_transaction_outputs(change)?;
     60     for input in inputs {
     61         validate_hash(&input.outpoint.txid, "genesis burn input outpoint txid")?;
     62         validate_address(&input.owner, "genesis burn input owner")?;
     63         if input.signature != "genesis" {
     64             bail!("genesis burn input signature is invalid");
     65         }
     66     }
     67     Ok(())
     68 }
     69 
     70 pub(super) fn estimated_block_selection_size_bytes(
     71     selection: &BlockSelection,
     72     recovery: bool,
     73 ) -> Result<usize> {
     74     let block = Block {
     75         height: u64::MAX,
     76         prev_hash: "f".repeat(64),
     77         timestamp_ms: u64::MAX,
     78         miner: "f".repeat(64),
     79         finalizer_mode: if recovery {
     80             FinalizerMode::Recovery
     81         } else {
     82             FinalizerMode::Ticket
     83         },
     84         finalizer_rank: 0,
     85         reward: u64::MAX,
     86         vdf_rounds: u64::MAX,
     87         vdf_output: "f".repeat(64),
     88         leader_proof: (!recovery).then(|| LeaderProof {
     89             ticket_id: "f".repeat(64),
     90             public_key: "f".repeat(64),
     91             signature: "f".repeat(128),
     92         }),
     93         blinded_transactions: selection.blinded_transactions.clone(),
     94         reveal_bundle_section: RevealBundleSection::default(),
     95         transactions: selection.transactions.clone(),
     96         hash: "f".repeat(64),
     97     };
     98     block.serialized_size_bytes()
     99 }
    100 
    101 pub(super) fn ensure_transaction_fits_empty_block(
    102     transaction: &Transaction,
    103     max_block_bytes: usize,
    104 ) -> Result<()> {
    105     let selection = BlockSelection {
    106         transactions: vec![transaction.clone()],
    107         blinded_transactions: Vec::new(),
    108     };
    109     if estimated_block_selection_size_bytes(&selection, false)? > max_block_bytes {
    110         bail!("transaction exceeds max block size");
    111     }
    112     Ok(())
    113 }
    114 
    115 pub(super) fn ensure_blinded_transaction_fits_empty_block(
    116     transaction: &BlindedTransaction,
    117     max_block_bytes: usize,
    118 ) -> Result<()> {
    119     let selection = BlockSelection {
    120         transactions: Vec::new(),
    121         blinded_transactions: vec![transaction.clone()],
    122     };
    123     if estimated_block_selection_size_bytes(&selection, false)? > max_block_bytes {
    124         bail!("blinded transaction exceeds max block size");
    125     }
    126     Ok(())
    127 }
    128 
    129 pub(super) fn verify_leader_proof(block: &Block, tickets: &[BurnTicket]) -> Result<()> {
    130     let Some(proof) = &block.leader_proof else {
    131         bail!("block is missing leader proof");
    132     };
    133     if proof.public_key != block.miner {
    134         bail!("leader proof public key does not match block finalizer");
    135     }
    136     let ticket = tickets
    137         .iter()
    138         .find(|ticket| {
    139             ticket.id == proof.ticket_id && ticket_is_eligible_for_height(ticket, block.height)
    140         })
    141         .context("leader ticket is not pending for this height")?;
    142     if ticket.owner != block.miner {
    143         bail!("leader ticket owner does not match block finalizer");
    144     }
    145     if ticket.eligible_from_height > block.height {
    146         bail!("leader ticket is not mature");
    147     }
    148 
    149     let payload = LeaderProofPayload {
    150         height: block.height,
    151         prev_hash: block.prev_hash.clone(),
    152         finalizer_rank: block.finalizer_rank,
    153         vdf_output: block.vdf_output.clone(),
    154         ticket_id: ticket.id.clone(),
    155         ticket_amount: ticket.amount,
    156         ticket_owner: ticket.owner.clone(),
    157     };
    158     verify_leader_signature(proof, &payload)?;
    159     Ok(())
    160 }
    161 
    162 pub(super) fn verify_leader_signature(
    163     proof: &LeaderProof,
    164     payload: &LeaderProofPayload,
    165 ) -> Result<()> {
    166     verify_address_signature(
    167         &proof.public_key,
    168         &payload.canonical(),
    169         &proof.signature,
    170         "leader",
    171     )
    172 }
    173 
    174 pub(super) fn verify_address_signature(
    175     address: &str,
    176     payload: &str,
    177     signature: &str,
    178     label: &str,
    179 ) -> Result<()> {
    180     let public_key = decode_hex_array::<PUBLIC_KEY_BYTES>(address)
    181         .with_context(|| format!("invalid {label} public key {address}"))?;
    182     let signature = decode_hex_array::<SIGNATURE_BYTES>(signature)
    183         .with_context(|| format!("invalid {label} signature hex"))?;
    184     let verifying_key = VerifyingKey::from_bytes(&public_key)
    185         .with_context(|| format!("invalid {label} public key"))?;
    186     let signature = Signature::from_bytes(&signature);
    187     verifying_key
    188         .verify(payload.as_bytes(), &signature)
    189         .with_context(|| format!("{label} signature is invalid"))
    190 }
    191 
    192 pub(super) fn vdf_seed_for_child(
    193     prev_hash: &str,
    194     height: u64,
    195     bundle_hashes: &[String; REVEAL_COMMITTEE_SIZE],
    196 ) -> String {
    197     hex_hash(format!(
    198         "iuna-vdf-child:{prev_hash}:{height}:{}",
    199         canonical_reveal_bundle_hashes(bundle_hashes)
    200     ))
    201 }
    202 
    203 pub(super) fn recovery_vdf_seed_for_child(
    204     prev_hash: &str,
    205     height: u64,
    206     timestamp_ms: u64,
    207     bundle_hashes: &[String; REVEAL_COMMITTEE_SIZE],
    208 ) -> String {
    209     hex_hash(format!(
    210         "iuna-recovery-vdf-child:{prev_hash}:{height}:{timestamp_ms}:{}",
    211         canonical_reveal_bundle_hashes(bundle_hashes)
    212     ))
    213 }
    214 
    215 pub(super) fn apply_transaction(
    216     transaction: &Transaction,
    217     utxos: &mut BTreeMap<OutPoint, TxOutput>,
    218 ) -> Result<()> {
    219     transaction.verify_signature()?;
    220     match transaction {
    221         Transaction::Mine { recipient, .. } => {
    222             let output = TxOutput {
    223                 address: recipient.clone(),
    224                 amount: MINE_REWARD,
    225             };
    226             ensure_outputs_do_not_overflow(utxos, std::slice::from_ref(&output))?;
    227             utxos.insert(
    228                 OutPoint {
    229                     txid: transaction.signature().to_string(),
    230                     index: 0,
    231                 },
    232                 output,
    233             );
    234             return Ok(());
    235         }
    236         Transaction::Transfer { .. } | Transaction::Burn { .. } => {}
    237     }
    238     ensure_single_input_owner(transaction)?;
    239     let input_total = spend_inputs(transaction, utxos)?;
    240     let outputs = transaction.outputs();
    241     let output_total = outputs.iter().try_fold(0_u64, |total, output| {
    242         total
    243             .checked_add(output.amount)
    244             .context("transaction outputs overflow")
    245     })?;
    246     let required = output_total
    247         .checked_add(transaction.fee())
    248         .context("transaction outputs plus fee overflow")?
    249         .checked_add(match transaction {
    250             Transaction::Burn { amount, .. } => *amount,
    251             Transaction::Transfer { .. } | Transaction::Mine { .. } => 0,
    252         })
    253         .context("transaction outputs plus burn overflow")?;
    254     if input_total != required {
    255         bail!("transaction inputs do not balance outputs, burn, and fee");
    256     }
    257     ensure_outputs_do_not_overflow(utxos, &outputs)?;
    258     for (index, output) in outputs.iter().enumerate() {
    259         utxos.insert(
    260             OutPoint {
    261                 txid: transaction.signature().to_string(),
    262                 index: index as u32,
    263             },
    264             output.clone(),
    265         );
    266     }
    267     Ok(())
    268 }
    269 
    270 pub(super) fn validate_block_blinded_items(block: &Block, ledger: &Ledger) -> Result<()> {
    271     let mut commitments = BTreeSet::new();
    272     for transaction in &block.blinded_transactions {
    273         if !commitments.insert(transaction.commitment.clone()) {
    274             bail!("duplicate blinded transaction in block");
    275         }
    276         ledger.validate_blinded_transaction(transaction)?;
    277         if transaction.expires_at_height <= block.height {
    278             bail!("blinded transaction is expired for block height");
    279         }
    280         if ledger.active_blinded.contains_key(&transaction.commitment) {
    281             bail!("blinded transaction is already active");
    282         }
    283         if ledger.chain.iter().any(|block| {
    284             block
    285                 .blinded_transactions
    286                 .iter()
    287                 .any(|existing| existing.commitment == transaction.commitment)
    288         }) {
    289             bail!("blinded transaction is already on chain");
    290         }
    291     }
    292 
    293     let mut reveals = BTreeSet::new();
    294     for reveal in block.all_blinded_reveals() {
    295         if !reveals.insert(reveal.commitment.clone()) {
    296             bail!("duplicate blinded reveal in block");
    297         }
    298         if ledger.chain.iter().any(|block| {
    299             block
    300                 .all_blinded_reveals()
    301                 .iter()
    302                 .any(|existing| existing.commitment == reveal.commitment)
    303         }) {
    304             bail!("blinded reveal is already on chain");
    305         }
    306         ledger.pending_reveal_transaction(reveal)?;
    307     }
    308     Ok(())
    309 }
    310 
    311 pub(super) fn fee_reward(transactions: &[Transaction]) -> Result<Amount> {
    312     transactions.iter().try_fold(0_u64, |total, tx| {
    313         total.checked_add(tx.fee()).context("block fees overflow")
    314     })
    315 }
    316 
    317 pub(super) fn block_reward(
    318     transactions: &[Transaction],
    319     aggregated_reveal_finalizer_fees: Amount,
    320 ) -> Result<Amount> {
    321     fee_reward(transactions)?
    322         .checked_add(aggregated_reveal_finalizer_fees)
    323         .context("block reward overflow")
    324 }
    325 
    326 pub(super) fn spend_inputs(
    327     transaction: &Transaction,
    328     utxos: &mut BTreeMap<OutPoint, TxOutput>,
    329 ) -> Result<Amount> {
    330     let mut seen = BTreeSet::new();
    331     let mut total = 0_u64;
    332     for input in transaction.inputs() {
    333         if !seen.insert(input.outpoint.clone()) {
    334             bail!("duplicate input in transaction");
    335         }
    336         let output = utxos.remove(&input.outpoint).with_context(|| {
    337             format!("transaction spends missing output {}", input.outpoint.id())
    338         })?;
    339         if output.address != input.owner {
    340             bail!("transaction input owner does not match spent output");
    341         }
    342         total = total
    343             .checked_add(output.amount)
    344             .context("transaction input total overflows")?;
    345     }
    346     Ok(total)
    347 }
    348 
    349 pub(super) fn apply_spendable_pending_transaction(
    350     transaction: &Transaction,
    351     utxos: &mut BTreeMap<OutPoint, TxOutput>,
    352 ) -> Result<()> {
    353     if matches!(transaction, Transaction::Mine { .. }) {
    354         bail!("pending mine outputs are not spendable");
    355     }
    356     transaction.verify_signature()?;
    357     ensure_single_input_owner(transaction)?;
    358     let input_total = transaction_input_total(transaction, utxos)?;
    359     let outputs = transaction.outputs();
    360     let output_total = outputs.iter().try_fold(0_u64, |total, output| {
    361         total
    362             .checked_add(output.amount)
    363             .context("transaction outputs overflow")
    364     })?;
    365     let required = output_total
    366         .checked_add(transaction.fee())
    367         .context("transaction outputs plus fee overflow")?
    368         .checked_add(match transaction {
    369             Transaction::Burn { amount, .. } => *amount,
    370             Transaction::Transfer { .. } | Transaction::Mine { .. } => 0,
    371         })
    372         .context("transaction outputs plus burn overflow")?;
    373     if input_total != required {
    374         bail!("transaction inputs do not balance outputs, burn, and fee");
    375     }
    376     ensure_outputs_do_not_overflow(utxos, &outputs)?;
    377     for input in transaction.inputs() {
    378         utxos.remove(&input.outpoint);
    379     }
    380     for (index, output) in outputs.iter().enumerate() {
    381         utxos.insert(
    382             OutPoint {
    383                 txid: transaction.signature().to_string(),
    384                 index: index as u32,
    385             },
    386             output.clone(),
    387         );
    388     }
    389     Ok(())
    390 }
    391 
    392 pub(super) fn transaction_input_total(
    393     transaction: &Transaction,
    394     utxos: &BTreeMap<OutPoint, TxOutput>,
    395 ) -> Result<Amount> {
    396     let mut seen = BTreeSet::new();
    397     let mut total = 0_u64;
    398     for input in transaction.inputs() {
    399         if !seen.insert(input.outpoint.clone()) {
    400             bail!("duplicate input in transaction");
    401         }
    402         let output = utxos.get(&input.outpoint).with_context(|| {
    403             format!("transaction spends missing output {}", input.outpoint.id())
    404         })?;
    405         if output.address != input.owner {
    406             bail!("transaction input owner does not match spent output");
    407         }
    408         total = total
    409             .checked_add(output.amount)
    410             .context("transaction input total overflows")?;
    411     }
    412     Ok(total)
    413 }
    414 
    415 pub(super) fn spend_blinded_inputs(
    416     transaction: &BlindedTransaction,
    417     utxos: &mut BTreeMap<OutPoint, TxOutput>,
    418 ) -> Result<Vec<TxOutput>> {
    419     verify_blinded_input_signatures(transaction)?;
    420     if transaction.inputs.is_empty() {
    421         return Ok(Vec::new());
    422     }
    423     let mut seen = BTreeSet::new();
    424     let mut locked = Vec::new();
    425     for input in &transaction.inputs {
    426         if !seen.insert(input.outpoint.clone()) {
    427             bail!("duplicate input in blinded transaction");
    428         }
    429         let output = utxos.remove(&input.outpoint).with_context(|| {
    430             format!(
    431                 "blinded transaction spends missing output {}",
    432                 input.outpoint.id()
    433             )
    434         })?;
    435         if output.address != input.owner {
    436             bail!("blinded transaction input owner does not match spent output");
    437         }
    438         locked.push(output);
    439     }
    440     let locked_total = locked.iter().try_fold(0_u64, |total, output| {
    441         total
    442             .checked_add(output.amount)
    443             .context("blinded transaction locked input total overflows")
    444     })?;
    445     if transaction.fee > locked_total {
    446         bail!("blinded transaction fee exceeds locked inputs");
    447     }
    448     Ok(locked)
    449 }
    450 
    451 pub(super) fn spend_spendable_blinded_inputs(
    452     transaction: &BlindedTransaction,
    453     utxos: &mut BTreeMap<OutPoint, TxOutput>,
    454 ) -> Result<Vec<TxOutput>> {
    455     let locked = blinded_input_outputs(transaction, utxos)?;
    456     for input in &transaction.inputs {
    457         utxos.remove(&input.outpoint);
    458     }
    459     Ok(locked)
    460 }
    461 
    462 pub(super) fn blinded_input_outputs(
    463     transaction: &BlindedTransaction,
    464     utxos: &BTreeMap<OutPoint, TxOutput>,
    465 ) -> Result<Vec<TxOutput>> {
    466     verify_blinded_input_signatures(transaction)?;
    467     if transaction.inputs.is_empty() {
    468         return Ok(Vec::new());
    469     }
    470     let mut seen = BTreeSet::new();
    471     let mut locked = Vec::new();
    472     for input in &transaction.inputs {
    473         if !seen.insert(input.outpoint.clone()) {
    474             bail!("duplicate input in blinded transaction");
    475         }
    476         let output = utxos.get(&input.outpoint).with_context(|| {
    477             format!(
    478                 "blinded transaction spends missing output {}",
    479                 input.outpoint.id()
    480             )
    481         })?;
    482         if output.address != input.owner {
    483             bail!("blinded transaction input owner does not match spent output");
    484         }
    485         locked.push(output.clone());
    486     }
    487     let locked_total = locked.iter().try_fold(0_u64, |total, output| {
    488         total
    489             .checked_add(output.amount)
    490             .context("blinded transaction locked input total overflows")
    491     })?;
    492     if transaction.fee > locked_total {
    493         bail!("blinded transaction fee exceeds locked inputs");
    494     }
    495     Ok(locked)
    496 }
    497 
    498 pub(super) fn transaction_has_missing_inputs(
    499     transaction: &Transaction,
    500     utxos: &BTreeMap<OutPoint, TxOutput>,
    501 ) -> bool {
    502     transaction
    503         .inputs()
    504         .iter()
    505         .any(|input| !utxos.contains_key(&input.outpoint))
    506 }
    507 
    508 pub(super) fn ensure_single_input_owner(transaction: &Transaction) -> Result<()> {
    509     if matches!(transaction, Transaction::Mine { .. }) {
    510         return Ok(());
    511     }
    512     ensure_single_input_owner_for_inputs(transaction.inputs())
    513 }
    514 
    515 pub(super) fn ensure_single_input_owner_for_inputs(inputs: &[TxInput]) -> Result<()> {
    516     let Some(first) = inputs.first() else {
    517         bail!("transaction has no inputs");
    518     };
    519     if inputs.iter().any(|input| input.owner != first.owner) {
    520         bail!("transaction inputs must have one owner");
    521     }
    522     Ok(())
    523 }
    524 
    525 pub(super) fn credit_reward_output(
    526     utxos: &mut BTreeMap<OutPoint, TxOutput>,
    527     block: &Block,
    528 ) -> Result<()> {
    529     if block.reward == 0 {
    530         return Ok(());
    531     }
    532     let output = TxOutput {
    533         address: block.miner.clone(),
    534         amount: block.reward,
    535     };
    536     ensure_outputs_do_not_overflow(utxos, std::slice::from_ref(&output))?;
    537     utxos.insert(reward_outpoint(&block.hash), output);
    538     Ok(())
    539 }
    540 
    541 pub(super) fn ensure_outputs_do_not_overflow(
    542     utxos: &BTreeMap<OutPoint, TxOutput>,
    543     outputs: &[TxOutput],
    544 ) -> Result<()> {
    545     let mut balances = BTreeMap::new();
    546     for output in utxos.values() {
    547         let balance = balances.entry(output.address.clone()).or_insert(0_u64);
    548         *balance = balance
    549             .checked_add(output.amount)
    550             .with_context(|| format!("balance overflow for {}", output.address))?;
    551     }
    552     for output in outputs {
    553         let balance = balances.entry(output.address.clone()).or_insert(0_u64);
    554         *balance = balance
    555             .checked_add(output.amount)
    556             .with_context(|| format!("balance overflow for {}", output.address))?;
    557     }
    558     Ok(())
    559 }
    560 
    561 pub(super) fn ensure_block_has_burn(transactions: &[Transaction]) -> Result<()> {
    562     if !transactions.iter().any(Transaction::is_burn) {
    563         bail!("block must include at least one burn transaction");
    564     }
    565     Ok(())
    566 }
    567 
    568 pub(super) fn ensure_block_has_burn_from(transactions: &[Transaction], miner: &str) -> Result<()> {
    569     if !transactions
    570         .iter()
    571         .any(|transaction| transaction.is_burn() && transaction.sender() == miner)
    572     {
    573         bail!("recovery block must include a burn from the finalizer");
    574     }
    575     Ok(())
    576 }
    577 
    578 pub(super) fn ensure_valid_recovery_block(block: &Block, parent: &Block) -> Result<()> {
    579     if block.finalizer_rank != 0 {
    580         bail!("recovery block finalizer rank must be 0");
    581     }
    582     if block.leader_proof.is_some() {
    583         bail!("recovery block must not carry a leader proof");
    584     }
    585     let min_timestamp = parent.timestamp_ms.saturating_add(RECOVERY_BLOCK_DELAY_MS);
    586     if block.timestamp_ms < min_timestamp {
    587         bail!("recovery block is not available before timestamp {min_timestamp}");
    588     }
    589     ensure_block_has_burn_from(&block.transactions, &block.miner)
    590 }
    591 
    592 pub(super) fn best_selectable_blinded_index(
    593     transactions: &[BlindedTransaction],
    594     utxos: &BTreeMap<OutPoint, TxOutput>,
    595 ) -> Option<usize> {
    596     transactions
    597         .iter()
    598         .enumerate()
    599         .filter(|(_, transaction)| {
    600             let mut utxos = utxos.clone();
    601             spend_blinded_inputs(transaction, &mut utxos).is_ok()
    602         })
    603         .max_by(|(_, left), (_, right)| {
    604             blinded_fee_rate_key(left)
    605                 .cmp(&blinded_fee_rate_key(right))
    606                 .then_with(|| left.fee.cmp(&right.fee))
    607                 .then_with(|| right.commitment.cmp(&left.commitment))
    608         })
    609         .map(|(index, _)| index)
    610 }
    611 
    612 pub(super) fn best_selectable_transaction_index(
    613     transactions: &[Transaction],
    614     utxos: &BTreeMap<OutPoint, TxOutput>,
    615     required_kind: Option<TransactionKind>,
    616 ) -> Option<usize> {
    617     transactions
    618         .iter()
    619         .enumerate()
    620         .filter(|(_, tx)| match required_kind {
    621             Some(TransactionKind::Burn) => tx.is_burn(),
    622             None => true,
    623         })
    624         .filter(|(_, tx)| {
    625             let mut utxos = utxos.clone();
    626             apply_transaction(tx, &mut utxos).is_ok()
    627         })
    628         .max_by(|(_, left), (_, right)| {
    629             fee_rate_key(left)
    630                 .cmp(&fee_rate_key(right))
    631                 .then_with(|| left.fee().cmp(&right.fee()))
    632                 .then_with(|| left.is_burn().cmp(&right.is_burn()))
    633                 .then_with(|| right.signature().cmp(left.signature()))
    634         })
    635         .map(|(index, _)| index)
    636 }
    637 
    638 pub(super) fn best_selectable_burn_from_index(
    639     transactions: &[Transaction],
    640     utxos: &BTreeMap<OutPoint, TxOutput>,
    641     owner: &str,
    642 ) -> Option<usize> {
    643     transactions
    644         .iter()
    645         .enumerate()
    646         .filter(|(_, tx)| tx.is_burn() && tx.sender() == owner)
    647         .filter(|(_, tx)| {
    648             let mut utxos = utxos.clone();
    649             apply_transaction(tx, &mut utxos).is_ok()
    650         })
    651         .max_by(|(_, left), (_, right)| {
    652             fee_rate_key(left)
    653                 .cmp(&fee_rate_key(right))
    654                 .then_with(|| left.fee().cmp(&right.fee()))
    655                 .then_with(|| right.signature().cmp(left.signature()))
    656         })
    657         .map(|(index, _)| index)
    658 }
    659 
    660 pub(super) fn reward_outpoint(block_hash: &str) -> OutPoint {
    661     OutPoint {
    662         txid: block_hash.to_string(),
    663         index: u32::MAX,
    664     }
    665 }