iuna

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

wallet.rs (21522B)


      1 use anyhow::{Context, Result, bail};
      2 
      3 use crate::domain::{
      4     Amount, BlindedTransaction, Block, BuiltBlindedTransaction, DEFAULT_TRANSACTION_FEE, Ledger,
      5     OutPoint, PreparedBlock, RevealBundle, StratumMineShare, StratumMineTemplate, Transaction,
      6     Wallet, run_vdf,
      7 };
      8 
      9 use super::{
     10     ExternalMineJob, FeeEstimate, GossipEnvelope, NodeCore, helpers::converge_fee_by_byte, now_ms,
     11 };
     12 
     13 #[derive(Clone, Debug)]
     14 pub(super) enum NodeWallet {
     15     Unlocked(Wallet),
     16     Locked { address: String },
     17 }
     18 
     19 impl NodeWallet {
     20     pub(super) fn address(&self) -> &str {
     21         match self {
     22             Self::Unlocked(wallet) => wallet.address(),
     23             Self::Locked { address } => address,
     24         }
     25     }
     26 
     27     pub(super) fn unlocked(&self) -> Result<&Wallet> {
     28         match self {
     29             Self::Unlocked(wallet) => Ok(wallet),
     30             Self::Locked { .. } => bail!("wallet is locked"),
     31         }
     32     }
     33 
     34     pub(super) fn is_locked(&self) -> bool {
     35         matches!(self, Self::Locked { .. })
     36     }
     37 }
     38 
     39 impl NodeCore {
     40     pub fn burn(&mut self, amount: Amount) -> Result<Transaction> {
     41         self.burn_with_fee(amount, 0)
     42     }
     43 
     44     pub fn burn_with_fee(&mut self, amount: Amount, fee: Amount) -> Result<Transaction> {
     45         let tx = self
     46             .wallet_build_ledger()?
     47             .build_burn(self.wallet.unlocked()?, amount, fee)?;
     48         self.submit_transaction_as_owned_blinded(tx)
     49     }
     50 
     51     pub fn burn_with_fee_rate(
     52         &mut self,
     53         amount: Amount,
     54         fee_per_byte: Amount,
     55     ) -> Result<(Transaction, FeeEstimate)> {
     56         let (built, estimate) = self.build_blinded_burn_with_fee_rate(amount, fee_per_byte)?;
     57         let tx = built.payload.clone();
     58         self.submit_owned_blinded_transaction(built)?;
     59         Ok((tx, estimate))
     60     }
     61 
     62     pub fn blinded_burn_with_fee(
     63         &mut self,
     64         amount: Amount,
     65         fee: Amount,
     66         expires_at_height: u64,
     67     ) -> Result<BlindedTransaction> {
     68         let built = self.wallet_build_ledger()?.build_blinded_burn(
     69             self.wallet.unlocked()?,
     70             amount,
     71             fee,
     72             expires_at_height,
     73         )?;
     74         self.submit_owned_blinded_transaction(built)
     75     }
     76 
     77     pub fn estimate_burn_fee(&self, amount: Amount, fee_per_byte: Amount) -> Result<FeeEstimate> {
     78         self.build_burn_with_fee_rate(amount, fee_per_byte)
     79             .map(|(_, estimate)| estimate)
     80     }
     81 
     82     pub fn transfer(&mut self, to: impl Into<String>, amount: Amount) -> Result<Transaction> {
     83         self.transfer_with_fee(to, amount, DEFAULT_TRANSACTION_FEE)
     84     }
     85 
     86     pub fn transfer_with_fee(
     87         &mut self,
     88         to: impl Into<String>,
     89         amount: Amount,
     90         fee: Amount,
     91     ) -> Result<Transaction> {
     92         let tx =
     93             self.wallet_build_ledger()?
     94                 .build_transfer(self.wallet.unlocked()?, to, amount, fee)?;
     95         self.submit_transaction_as_owned_blinded(tx)
     96     }
     97 
     98     pub fn transfer_with_fee_spending(
     99         &mut self,
    100         to: impl Into<String>,
    101         amount: Amount,
    102         fee: Amount,
    103         outpoints: &[OutPoint],
    104     ) -> Result<Transaction> {
    105         let tx = self.wallet_build_ledger()?.build_transfer_with_inputs(
    106             self.wallet.unlocked()?,
    107             to,
    108             amount,
    109             fee,
    110             outpoints,
    111         )?;
    112         self.submit_transaction_as_owned_blinded(tx)
    113     }
    114 
    115     pub fn blinded_transfer_with_fee(
    116         &mut self,
    117         to: impl Into<String>,
    118         amount: Amount,
    119         fee: Amount,
    120         expires_at_height: u64,
    121     ) -> Result<BlindedTransaction> {
    122         let built = self.wallet_build_ledger()?.build_blinded_transfer(
    123             self.wallet.unlocked()?,
    124             to,
    125             amount,
    126             fee,
    127             expires_at_height,
    128         )?;
    129         self.submit_owned_blinded_transaction(built)
    130     }
    131 
    132     pub fn transfer_with_fee_rate(
    133         &mut self,
    134         to: impl Into<String>,
    135         amount: Amount,
    136         fee_per_byte: Amount,
    137         outpoints: &[OutPoint],
    138     ) -> Result<(Transaction, FeeEstimate)> {
    139         let (built, estimate) =
    140             self.build_blinded_transfer_with_fee_rate(to, amount, fee_per_byte, outpoints)?;
    141         let tx = built.payload.clone();
    142         self.submit_owned_blinded_transaction(built)?;
    143         Ok((tx, estimate))
    144     }
    145 
    146     pub fn estimate_transfer_fee(
    147         &self,
    148         to: impl Into<String>,
    149         amount: Amount,
    150         fee_per_byte: Amount,
    151         outpoints: &[OutPoint],
    152     ) -> Result<FeeEstimate> {
    153         self.build_transfer_with_fee_rate(to, amount, fee_per_byte, outpoints)
    154             .map(|(_, estimate)| estimate)
    155     }
    156 
    157     pub fn mine_pow_reward(&mut self) -> Result<Transaction> {
    158         let (tx, _) = self.build_mine_estimate()?;
    159         self.submit_public_mine_action(tx)
    160     }
    161 
    162     pub fn estimate_mine_fee(&self, _fee_per_byte: Amount) -> Result<FeeEstimate> {
    163         let tx = Transaction::Mine {
    164             recipient: self.wallet.address().to_string(),
    165             anchor: self.ledger.tip_hash().to_string(),
    166             salt: 0,
    167             nonce: 0,
    168             difficulty_bits: self.ledger.current_mine_difficulty_bits(),
    169             proof_header: None,
    170             signature: "0".repeat(64),
    171         };
    172         Ok(FeeEstimate {
    173             bytes: tx.economic_size_bytes(),
    174             fee: tx.fee(),
    175         })
    176     }
    177 
    178     pub fn external_mine_job(
    179         &self,
    180         recipient: impl Into<String>,
    181         salt: u64,
    182     ) -> Result<ExternalMineJob> {
    183         let recipient = recipient.into();
    184         let tip = self
    185             .chain()
    186             .last()
    187             .context("cannot build mine job without a chain tip")?;
    188         let difficulty_bits = self.ledger.current_mine_difficulty_bits();
    189         Ok(ExternalMineJob {
    190             template: self.ledger.stratum_mine_template(
    191                 recipient,
    192                 &tip.hash,
    193                 salt,
    194                 difficulty_bits,
    195             )?,
    196         })
    197     }
    198 
    199     pub fn submit_external_mine(
    200         &mut self,
    201         recipient: impl Into<String>,
    202         template: StratumMineTemplate,
    203         share: StratumMineShare,
    204     ) -> Result<Transaction> {
    205         let tx = self.ledger.build_stratum_mine(template, share)?;
    206         let recipient = recipient.into();
    207         if tx.to() != Some(recipient.as_str()) {
    208             bail!("submitted mine recipient does not match worker");
    209         }
    210         self.submit_public_mine_action(tx)
    211     }
    212 
    213     pub(super) fn usable_reveal_bundles(&self) -> Vec<RevealBundle> {
    214         let next_height = self.ledger.height().saturating_add(1);
    215         let mut bundles = self
    216             .reveal_bundles
    217             .iter()
    218             .filter(|((height, slot), _)| {
    219                 *height == next_height
    220                     && !self
    221                         .equivocated_reveal_bundle_slots
    222                         .contains(&(*height, *slot))
    223             })
    224             .map(|(_, bundle)| bundle.clone())
    225             .collect::<Vec<_>>();
    226         bundles.sort_by_key(|bundle| bundle.slot);
    227         bundles
    228     }
    229 
    230     pub(super) fn prune_reveal_bundles(&mut self) {
    231         let height = self.ledger.height();
    232         self.reveal_bundles
    233             .retain(|(bundle_height, _), _| *bundle_height > height);
    234         self.equivocated_reveal_bundle_slots
    235             .retain(|(bundle_height, _)| *bundle_height > height);
    236     }
    237 
    238     pub(super) fn publish_reveal_bundle_for_next_block(&mut self) -> Result<()> {
    239         let wallet = match &self.wallet {
    240             NodeWallet::Unlocked(wallet) => wallet,
    241             NodeWallet::Locked { .. } => return Ok(()),
    242         };
    243         let Some(bundle) = self.ledger.build_reveal_bundle(wallet)? else {
    244             return Ok(());
    245         };
    246         let key = (bundle.height, bundle.slot);
    247         if self.equivocated_reveal_bundle_slots.contains(&key)
    248             || self.reveal_bundles.contains_key(&key)
    249         {
    250             return Ok(());
    251         }
    252         self.ledger
    253             .validate_next_block_reveal_bundles(vec![bundle.clone()])?;
    254         self.reveal_bundles.insert(key, bundle.clone());
    255         self.outbox.push(GossipEnvelope::RevealBundle(bundle));
    256         Ok(())
    257     }
    258 
    259     pub(super) fn submit_public_mine_action(&mut self, tx: Transaction) -> Result<Transaction> {
    260         if !matches!(tx, Transaction::Mine { .. }) {
    261             bail!("only mine actions may be submitted as public mempool transactions");
    262         }
    263         if self
    264             .ledger
    265             .submit_transaction_with_outcome(tx.clone())?
    266             .added()
    267         {
    268             self.outbox.push(GossipEnvelope::MineAction(tx.clone()));
    269         }
    270         Ok(tx)
    271     }
    272 
    273     pub(super) fn build_burn_with_fee_rate(
    274         &self,
    275         amount: Amount,
    276         fee_per_byte: Amount,
    277     ) -> Result<(Transaction, FeeEstimate)> {
    278         let (built, estimate) = self.build_blinded_burn_with_fee_rate(amount, fee_per_byte)?;
    279         Ok((built.payload, estimate))
    280     }
    281 
    282     pub(super) fn build_blinded_burn_with_fee_rate(
    283         &self,
    284         amount: Amount,
    285         fee_per_byte: Amount,
    286     ) -> Result<(BuiltBlindedTransaction, FeeEstimate)> {
    287         let ledger = self.wallet_build_ledger()?;
    288         self.build_blinded_burn_with_fee_rate_on_ledger(&ledger, amount, fee_per_byte)
    289     }
    290 
    291     pub(super) fn build_blinded_burn_with_fee_rate_on_ledger(
    292         &self,
    293         ledger: &Ledger,
    294         amount: Amount,
    295         fee_per_byte: Amount,
    296     ) -> Result<(BuiltBlindedTransaction, FeeEstimate)> {
    297         let expires_at_height = self.default_blinded_transaction_expiry_height();
    298         converge_fee_by_byte(fee_per_byte, |fee| {
    299             let tx = ledger.build_burn(self.wallet.unlocked()?, amount, fee)?;
    300             ledger.build_blinded_transaction(self.wallet.unlocked()?, tx, expires_at_height)
    301         })
    302     }
    303 
    304     pub(super) fn build_blinded_burn_with_fee_on_ledger(
    305         &self,
    306         ledger: &Ledger,
    307         amount: Amount,
    308         fee: Amount,
    309     ) -> Result<BuiltBlindedTransaction> {
    310         let expires_at_height = self.default_blinded_transaction_expiry_height();
    311         let tx = ledger.build_burn(self.wallet.unlocked()?, amount, fee)?;
    312         ledger.build_blinded_transaction(self.wallet.unlocked()?, tx, expires_at_height)
    313     }
    314 
    315     pub(super) fn build_transfer_with_fee_rate(
    316         &self,
    317         to: impl Into<String>,
    318         amount: Amount,
    319         fee_per_byte: Amount,
    320         outpoints: &[OutPoint],
    321     ) -> Result<(Transaction, FeeEstimate)> {
    322         let (built, estimate) =
    323             self.build_blinded_transfer_with_fee_rate(to, amount, fee_per_byte, outpoints)?;
    324         Ok((built.payload, estimate))
    325     }
    326 
    327     pub(super) fn build_blinded_transfer_with_fee_rate(
    328         &self,
    329         to: impl Into<String>,
    330         amount: Amount,
    331         fee_per_byte: Amount,
    332         outpoints: &[OutPoint],
    333     ) -> Result<(BuiltBlindedTransaction, FeeEstimate)> {
    334         let to = to.into();
    335         let ledger = self.wallet_build_ledger()?;
    336         let expires_at_height = self.default_blinded_transaction_expiry_height();
    337         converge_fee_by_byte(fee_per_byte, |fee| {
    338             let tx = if outpoints.is_empty() {
    339                 ledger.build_transfer(self.wallet.unlocked()?, to.clone(), amount, fee)
    340             } else {
    341                 ledger.build_transfer_with_inputs(
    342                     self.wallet.unlocked()?,
    343                     to.clone(),
    344                     amount,
    345                     fee,
    346                     outpoints,
    347                 )
    348             }?;
    349             ledger.build_blinded_transaction(self.wallet.unlocked()?, tx, expires_at_height)
    350         })
    351     }
    352 
    353     pub(super) fn build_mine_estimate(&self) -> Result<(Transaction, FeeEstimate)> {
    354         let tx = self.ledger.build_mine(self.wallet.address())?;
    355         Ok((
    356             tx.clone(),
    357             FeeEstimate {
    358                 bytes: tx.economic_size_bytes(),
    359                 fee: tx.fee(),
    360             },
    361         ))
    362     }
    363 
    364     pub(super) fn wallet_build_ledger(&self) -> Result<Ledger> {
    365         let mut ledger = self.ledger.clone();
    366         self.reserve_local_block_anchor_inputs(&mut ledger)?;
    367         self.queue_owned_blinded_payloads(&mut ledger)?;
    368         Ok(ledger)
    369     }
    370 
    371     pub(super) fn wallet_anchor_build_ledger(&self) -> Result<Ledger> {
    372         let mut ledger = self.ledger.clone();
    373         self.reserve_local_block_anchor_inputs(&mut ledger)?;
    374         ledger.clear_pending_transactions();
    375         ledger.clear_pending_blinded_transactions();
    376         Ok(ledger)
    377     }
    378 
    379     pub(super) fn queue_local_block_anchor(&self, ledger: &mut Ledger) -> Result<()> {
    380         let Some((height, burn)) = &self.local_block_anchor_burn else {
    381             return Ok(());
    382         };
    383         if *height == ledger.height() && !ledger.has_transaction(burn.signature()) {
    384             let _ = ledger.submit_transaction(burn.clone())?;
    385         }
    386         Ok(())
    387     }
    388 
    389     pub(super) fn reserve_local_block_anchor_inputs(&self, ledger: &mut Ledger) -> Result<()> {
    390         let Some((height, burn)) = &self.local_block_anchor_burn else {
    391             return Ok(());
    392         };
    393         if *height == ledger.height() && !ledger.has_transaction(burn.signature()) {
    394             let _ = ledger.reserve_transaction_inputs(burn);
    395         }
    396         Ok(())
    397     }
    398 
    399     pub fn mine_one(&mut self) -> Result<Block> {
    400         self.mine_one_at(now_ms())
    401     }
    402 
    403     pub fn mine_one_at(&mut self, timestamp_ms: u64) -> Result<Block> {
    404         self.publish_reveal_bundle_for_next_block()?;
    405         let work = self.prepare_next_block_with_local_anchor(timestamp_ms)?;
    406         let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds());
    407         self.complete_prepared_block_at(work, vdf_output, timestamp_ms)
    408     }
    409 
    410     pub fn complete_prepared_block(
    411         &mut self,
    412         work: PreparedBlock,
    413         vdf_output: String,
    414     ) -> Result<Block> {
    415         self.complete_prepared_block_at(work, vdf_output, now_ms())
    416     }
    417 
    418     pub fn complete_prepared_block_at(
    419         &mut self,
    420         work: PreparedBlock,
    421         vdf_output: String,
    422         timestamp_ms: u64,
    423     ) -> Result<Block> {
    424         let block = work.finish_at(self.wallet.unlocked()?, vdf_output, timestamp_ms);
    425         self.ledger.apply_locally_mined_block(block.clone())?;
    426         self.clear_stale_local_block_anchor();
    427         self.prune_reveal_bundles();
    428         self.prune_owned_blinded_payloads_for_block(&block);
    429         self.outbox.push(GossipEnvelope::Block(block.clone()));
    430         self.publish_owned_reveals_for_block(&block)?;
    431         Ok(block)
    432     }
    433 }
    434 
    435 #[cfg(test)]
    436 mod tests {
    437     use std::collections::BTreeMap;
    438 
    439     use crate::{
    440         app::{FeeEstimate, NodeCore},
    441         domain::{
    442             GenesisBurn, Ledger, MICRO_IUNA, MINE_FINALIZER_FEE, Transaction, VDF_TARGET_BLOCK_MS,
    443             Wallet, run_vdf,
    444         },
    445     };
    446 
    447     #[test]
    448     fn fee_rate_transfer_and_burn_pay_at_least_bytes_times_rate() {
    449         let transfer_sender = Wallet::from_seed("fee-rate-transfer-sender");
    450         let transfer_recipient = Wallet::from_seed("fee-rate-transfer-recipient");
    451         let mut transfer_genesis = BTreeMap::new();
    452         transfer_genesis.insert(transfer_sender.address().to_string(), 10 * MICRO_IUNA);
    453         let transfer_ledger = Ledger::new_with_genesis_burns(
    454             transfer_genesis,
    455             vec![GenesisBurn::new(transfer_sender.address(), MICRO_IUNA)],
    456             1,
    457         )
    458         .unwrap();
    459         let mut transfer_node = NodeCore::from_ledger(transfer_sender, transfer_ledger, 0);
    460 
    461         let (transfer, transfer_estimate) = transfer_node
    462             .transfer_with_fee_rate(transfer_recipient.address(), MICRO_IUNA, 2, &[])
    463             .unwrap();
    464         let transfer_blinded_bytes =
    465             transfer_node.ledger().pending_blinded_transactions()[0].fee_rate_size_bytes();
    466         assert_eq!(transfer_estimate.bytes, transfer_blinded_bytes);
    467         let minimum_transfer_fee = transfer_blinded_bytes as u64 * 2;
    468         assert!(transfer.fee() >= minimum_transfer_fee);
    469         assert!(transfer_node.ledger().pending().is_empty());
    470         assert_eq!(
    471             transfer_node.ledger().pending_blinded_transactions().len(),
    472             1
    473         );
    474 
    475         let burn_wallet = Wallet::from_seed("fee-rate-burn-wallet");
    476         let mut burn_genesis = BTreeMap::new();
    477         burn_genesis.insert(burn_wallet.address().to_string(), 10 * MICRO_IUNA);
    478         let burn_ledger = Ledger::new_with_genesis_burns(
    479             burn_genesis,
    480             vec![GenesisBurn::new(burn_wallet.address(), MICRO_IUNA)],
    481             1,
    482         )
    483         .unwrap();
    484         let mut burn_node = NodeCore::from_ledger(burn_wallet, burn_ledger, 0);
    485         let (burn, burn_estimate) = burn_node.burn_with_fee_rate(MICRO_IUNA, 3).unwrap();
    486         let burn_blinded_bytes =
    487             burn_node.ledger().pending_blinded_transactions()[0].fee_rate_size_bytes();
    488         assert_eq!(burn_estimate.bytes, burn_blinded_bytes);
    489         let minimum_burn_fee = burn_blinded_bytes as u64 * 3;
    490         assert!(burn.fee() >= minimum_burn_fee);
    491         assert!(burn_node.ledger().pending().is_empty());
    492         assert_eq!(burn_node.ledger().pending_blinded_transactions().len(), 1);
    493     }
    494 
    495     #[test]
    496     fn mine_fee_estimate_uses_template_without_searching_pow() {
    497         let wallet = Wallet::from_seed("mine-fee-template-wallet");
    498         let mut genesis = BTreeMap::new();
    499         genesis.insert(wallet.address().to_string(), MICRO_IUNA);
    500         let ledger = Ledger::new_with_genesis_burns(
    501             genesis,
    502             vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)],
    503             1,
    504         )
    505         .unwrap();
    506         let node = NodeCore::from_ledger(wallet, ledger, 0);
    507 
    508         let estimate = node.estimate_mine_fee(MINE_FINALIZER_FEE).unwrap();
    509 
    510         assert_eq!(
    511             estimate,
    512             FeeEstimate {
    513                 bytes: Transaction::Mine {
    514                     recipient: node.wallet_address().to_string(),
    515                     anchor: node.ledger().tip_hash().to_string(),
    516                     salt: 0,
    517                     nonce: 0,
    518                     difficulty_bits: node.ledger().current_mine_difficulty_bits(),
    519                     proof_header: None,
    520                     signature: "0".repeat(64),
    521                 }
    522                 .economic_size_bytes(),
    523                 fee: MINE_FINALIZER_FEE,
    524             }
    525         );
    526     }
    527 
    528     #[test]
    529     fn wallet_building_reserves_local_anchor_burn_inputs() {
    530         let alice = Wallet::from_seed("local-anchor-reserve-alice");
    531         let finalizers = [alice.clone()];
    532         let mut allocations = BTreeMap::new();
    533         allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    534         let mut ledger = Ledger::new_with_genesis_burns(
    535             allocations,
    536             finalizers
    537                 .iter()
    538                 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    539                 .collect(),
    540             1,
    541         )
    542         .unwrap();
    543         let leader = ledger.expected_leader_for_next_block().unwrap();
    544         let leader_wallet = finalizers
    545             .iter()
    546             .find(|wallet| wallet.address() == leader)
    547             .unwrap()
    548             .clone();
    549         let split = ledger
    550             .build_transfer(&leader_wallet, leader_wallet.address(), MICRO_IUNA, 0)
    551             .unwrap();
    552         ledger.submit_transaction(split).unwrap();
    553         let anchor = ledger.build_burn(&leader_wallet, 1, 0).unwrap();
    554         ledger.submit_transaction(anchor).unwrap();
    555         let split_block = ledger.mine_next_block(&leader_wallet, 1).unwrap();
    556         ledger.apply_block(split_block).unwrap();
    557         let mut node =
    558             NodeCore::from_ledger_with_burn_fee_and_enabled(leader_wallet, ledger, true, 0, 1);
    559 
    560         let plan = node.prepare_automatic_finalization(1);
    561         assert!(plan.burned.is_some());
    562         assert!(node.status().wallet_balance < node.ledger().balance_of(node.wallet_address()));
    563         let (_, anchor_burn) = node
    564             .local_block_anchor_burn
    565             .clone()
    566             .expect("leader burn should be held as a local block anchor");
    567         let Transaction::Burn { inputs, .. } = anchor_burn else {
    568             panic!("local block anchor must be a burn");
    569         };
    570         let anchor_inputs = inputs
    571             .iter()
    572             .map(|input| input.outpoint.clone())
    573             .collect::<Vec<_>>();
    574 
    575         let blinded = node
    576             .blinded_burn_with_fee(MICRO_IUNA / 20, 1, node.chain_height() + 4)
    577             .unwrap();
    578 
    579         assert!(
    580             blinded
    581                 .inputs
    582                 .iter()
    583                 .all(|input| !anchor_inputs.contains(&input.outpoint)),
    584             "blinded wallet transactions must not spend inputs reserved by the local anchor burn"
    585         );
    586 
    587         let work = node.prepare_next_block_with_local_anchor(2).unwrap();
    588         let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds());
    589         let mut peer_ledger = node.clone_ledger();
    590         let block = node
    591             .complete_prepared_block_at(work, vdf_output, VDF_TARGET_BLOCK_MS * 2)
    592             .unwrap();
    593         assert!(block.transactions.iter().any(Transaction::is_burn));
    594         assert!(
    595             block
    596                 .blinded_transactions
    597                 .iter()
    598                 .any(|transaction| transaction.commitment == blinded.commitment)
    599         );
    600         peer_ledger.apply_block_at(block, u64::MAX).unwrap();
    601         assert_eq!(
    602             node.ledger().status().tip_hash,
    603             peer_ledger.status().tip_hash
    604         );
    605     }
    606 }