iuna

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

owned_blinded.rs (18424B)


      1 use anyhow::Result;
      2 
      3 use crate::domain::{
      4     BlindedTransaction, Block, BuiltBlindedTransaction, Ledger,
      5     MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS, OwnedBlindedTransaction, Transaction,
      6 };
      7 
      8 use super::{GossipEnvelope, NodeCore};
      9 
     10 impl NodeCore {
     11     pub fn owned_blinded_payloads(&self) -> Vec<Transaction> {
     12         self.owned_blinded_payloads.values().cloned().collect()
     13     }
     14 
     15     pub fn owned_blinded_outbox_version(&self) -> u64 {
     16         self.owned_blinded_outbox_version
     17     }
     18 
     19     pub fn owned_blinded_transactions(&self) -> Vec<OwnedBlindedTransaction> {
     20         self.owned_blinded_transactions
     21             .iter()
     22             .filter_map(|(commitment, transaction)| {
     23                 let payload = self.owned_blinded_payloads.get(commitment)?;
     24                 let reveal = self.owned_blinded_reveals.get(commitment)?;
     25                 Some(OwnedBlindedTransaction {
     26                     transaction: transaction.clone(),
     27                     payload: payload.clone(),
     28                     reveal: reveal.clone(),
     29                 })
     30             })
     31             .collect()
     32     }
     33 
     34     pub fn mark_owned_blinded_outbox_persisted(&mut self, version: u64) {
     35         if self.owned_blinded_outbox_version == version {
     36             self.owned_blinded_outbox_version = 0;
     37         }
     38     }
     39 
     40     pub fn restore_owned_blinded_transactions(
     41         &mut self,
     42         transactions: Vec<OwnedBlindedTransaction>,
     43     ) -> Result<()> {
     44         self.owned_blinded_transactions.clear();
     45         self.owned_blinded_reveals.clear();
     46         self.owned_blinded_payloads.clear();
     47         for owned in transactions {
     48             let commitment = owned.transaction.commitment.clone();
     49             if owned.reveal.commitment != commitment {
     50                 continue;
     51             }
     52             if self.ledger.has_blinded_reveal(&commitment)
     53                 || !self.ledger.has_unrevealed_blinded_transaction(&commitment)
     54                     && owned.transaction.expires_at_height <= self.ledger.height().saturating_add(1)
     55             {
     56                 continue;
     57             }
     58             if !self.ledger.has_blinded_transaction(&commitment) {
     59                 match self
     60                     .ledger
     61                     .submit_blinded_transaction(owned.transaction.clone())
     62                 {
     63                     Ok(true) => self.outbox.push(GossipEnvelope::BlindedTransaction(
     64                         owned.transaction.clone(),
     65                     )),
     66                     Ok(false) => {}
     67                     Err(_) => continue,
     68                 }
     69             }
     70             if !self.ledger.has_unrevealed_blinded_transaction(&commitment) {
     71                 continue;
     72             }
     73             self.owned_blinded_transactions
     74                 .insert(commitment.clone(), owned.transaction);
     75             self.owned_blinded_payloads
     76                 .insert(commitment.clone(), owned.payload);
     77             self.owned_blinded_reveals
     78                 .insert(commitment.clone(), owned.reveal);
     79         }
     80         self.publish_owned_reveals_for_active_commits()?;
     81         self.bump_owned_blinded_outbox_version();
     82         Ok(())
     83     }
     84 
     85     pub(super) fn submit_owned_blinded_transaction(
     86         &mut self,
     87         built: BuiltBlindedTransaction,
     88     ) -> Result<BlindedTransaction> {
     89         let transaction = built.transaction;
     90         self.owned_blinded_transactions
     91             .insert(transaction.commitment.clone(), transaction.clone());
     92         self.owned_blinded_payloads
     93             .insert(transaction.commitment.clone(), built.payload);
     94         self.owned_blinded_reveals
     95             .insert(transaction.commitment.clone(), built.reveal);
     96         self.bump_owned_blinded_outbox_version();
     97         if self
     98             .ledger
     99             .submit_blinded_transaction(transaction.clone())?
    100         {
    101             self.outbox
    102                 .push(GossipEnvelope::BlindedTransaction(transaction.clone()));
    103         }
    104         Ok(transaction)
    105     }
    106 
    107     pub(super) fn submit_transaction_as_owned_blinded(
    108         &mut self,
    109         tx: Transaction,
    110     ) -> Result<Transaction> {
    111         let built = self.ledger.build_blinded_transaction(
    112             self.wallet.unlocked()?,
    113             tx.clone(),
    114             self.default_blinded_transaction_expiry_height(),
    115         )?;
    116         self.submit_owned_blinded_transaction(built)?;
    117         Ok(tx)
    118     }
    119 
    120     pub(super) fn default_blinded_transaction_expiry_height(&self) -> u64 {
    121         self.ledger
    122             .height()
    123             .saturating_add(MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS)
    124     }
    125 
    126     pub(super) fn publish_owned_reveals_for_block(&mut self, block: &Block) -> Result<()> {
    127         for transaction in &block.blinded_transactions {
    128             let Some(reveal) = self.owned_blinded_reveals.get(&transaction.commitment) else {
    129                 continue;
    130             };
    131             if self.ledger.submit_blinded_reveal(reveal.clone())? {
    132                 self.outbox
    133                     .push(GossipEnvelope::BlindedReveal(reveal.clone()));
    134             }
    135         }
    136         Ok(())
    137     }
    138 
    139     pub(super) fn publish_owned_reveals_for_active_commits(&mut self) -> Result<()> {
    140         let reveals = self
    141             .owned_blinded_reveals
    142             .iter()
    143             .filter(|(commitment, _)| self.ledger.has_active_blinded_transaction(commitment))
    144             .map(|(_, reveal)| reveal.clone())
    145             .collect::<Vec<_>>();
    146         for reveal in reveals {
    147             if self.ledger.submit_blinded_reveal(reveal.clone())? {
    148                 self.outbox.push(GossipEnvelope::BlindedReveal(reveal));
    149             }
    150         }
    151         Ok(())
    152     }
    153 
    154     pub(super) fn prune_owned_blinded_payloads_for_block(&mut self, block: &Block) {
    155         let before = self.owned_blinded_transactions.len()
    156             + self.owned_blinded_reveals.len()
    157             + self.owned_blinded_payloads.len();
    158         for reveal in block.all_blinded_reveals() {
    159             self.owned_blinded_transactions.remove(&reveal.commitment);
    160             self.owned_blinded_reveals.remove(&reveal.commitment);
    161             self.owned_blinded_payloads.remove(&reveal.commitment);
    162         }
    163         let unrevealed = self
    164             .owned_blinded_transactions
    165             .keys()
    166             .filter(|commitment| self.ledger.has_unrevealed_blinded_transaction(commitment))
    167             .cloned()
    168             .collect::<Vec<_>>();
    169         self.owned_blinded_transactions
    170             .retain(|commitment, _| unrevealed.contains(commitment));
    171         self.owned_blinded_reveals
    172             .retain(|commitment, _| unrevealed.contains(commitment));
    173         self.owned_blinded_payloads
    174             .retain(|commitment, _| unrevealed.contains(commitment));
    175         let after = self.owned_blinded_transactions.len()
    176             + self.owned_blinded_reveals.len()
    177             + self.owned_blinded_payloads.len();
    178         if before != after {
    179             self.bump_owned_blinded_outbox_version();
    180         }
    181     }
    182 
    183     pub(super) fn queue_owned_blinded_payloads(&self, ledger: &mut Ledger) -> Result<()> {
    184         for (commitment, payload) in &self.owned_blinded_payloads {
    185             if self.ledger.has_unrevealed_blinded_transaction(commitment)
    186                 && !ledger.has_transaction(payload.signature())
    187             {
    188                 let _ = ledger.submit_transaction(payload.clone());
    189             }
    190         }
    191         Ok(())
    192     }
    193 
    194     pub(super) fn bump_owned_blinded_outbox_version(&mut self) {
    195         self.owned_blinded_outbox_version = self.owned_blinded_outbox_version.saturating_add(1);
    196     }
    197 }
    198 
    199 #[cfg(test)]
    200 mod tests {
    201     use std::collections::BTreeMap;
    202 
    203     use crate::{
    204         app::{GossipEnvelope, NodeCore},
    205         domain::{GenesisBurn, Ledger, MICRO_IUNA, Wallet},
    206     };
    207 
    208     #[test]
    209     fn owned_blinded_transaction_reveals_after_commit_block_import() {
    210         let alice = Wallet::from_seed("owned-blinded-reveal-alice");
    211         let bob = Wallet::from_seed("owned-blinded-reveal-bob");
    212         let carol = Wallet::from_seed("owned-blinded-reveal-carol");
    213         let finalizers = [alice.clone(), bob.clone()];
    214         let mut allocations = BTreeMap::new();
    215         allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    216         allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    217         allocations.insert(carol.address().to_string(), 10 * MICRO_IUNA);
    218         let ledger = Ledger::new_with_genesis_burns(
    219             allocations,
    220             finalizers
    221                 .iter()
    222                 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    223                 .collect(),
    224             1,
    225         )
    226         .unwrap();
    227         let mut wallet_node = NodeCore::from_ledger(carol.clone(), ledger.clone(), 0);
    228         let mut finalizer_ledger = ledger;
    229 
    230         let blinded = wallet_node
    231             .blinded_burn_with_fee(3, 7, wallet_node.chain_height() + 4)
    232             .unwrap();
    233         wallet_node.drain_outbox();
    234         finalizer_ledger
    235             .submit_blinded_transaction(blinded.clone())
    236             .unwrap();
    237         let leader = finalizer_ledger.expected_leader_for_next_block().unwrap();
    238         let finalizer = finalizers
    239             .iter()
    240             .find(|wallet| wallet.address() == leader)
    241             .unwrap();
    242         let burn = finalizer_ledger.build_burn(finalizer, 1, 0).unwrap();
    243         finalizer_ledger.submit_transaction(burn).unwrap();
    244         let commit_block = finalizer_ledger.mine_next_block(finalizer, 1).unwrap();
    245 
    246         wallet_node
    247             .receive(GossipEnvelope::Block(commit_block))
    248             .unwrap();
    249         let outbox = wallet_node.drain_outbox();
    250 
    251         assert!(
    252             wallet_node
    253                 .ledger()
    254                 .pending_blinded_reveals()
    255                 .iter()
    256                 .any(|reveal| reveal.commitment == blinded.commitment)
    257         );
    258         assert!(outbox.iter().any(|envelope| matches!(
    259             envelope,
    260             GossipEnvelope::BlindedReveal(reveal) if reveal.commitment == blinded.commitment
    261         )));
    262     }
    263 
    264     #[test]
    265     fn status_wallet_balance_includes_owned_blinded_change_before_and_after_commit() {
    266         let alice = Wallet::from_seed("owned-blinded-balance-alice");
    267         let bob = Wallet::from_seed("owned-blinded-balance-bob");
    268         let carol = Wallet::from_seed("owned-blinded-balance-carol");
    269         let finalizers = [alice.clone(), bob.clone()];
    270         let starting_balance = 2 * MICRO_IUNA;
    271         let burn_amount = MICRO_IUNA / 10;
    272         let fee = MICRO_IUNA / 10;
    273         let expected_balance = starting_balance - burn_amount - fee;
    274         let mut allocations = BTreeMap::new();
    275         allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    276         allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    277         allocations.insert(carol.address().to_string(), starting_balance);
    278         let ledger = Ledger::new_with_genesis_burns(
    279             allocations,
    280             finalizers
    281                 .iter()
    282                 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    283                 .collect(),
    284             1,
    285         )
    286         .unwrap();
    287         let mut wallet_node = NodeCore::from_ledger(carol.clone(), ledger.clone(), 0);
    288         let mut finalizer_ledger = ledger;
    289 
    290         let blinded = wallet_node
    291             .blinded_burn_with_fee(burn_amount, fee, wallet_node.chain_height() + 4)
    292             .unwrap();
    293 
    294         assert_eq!(wallet_node.status().wallet_balance, expected_balance);
    295 
    296         finalizer_ledger
    297             .submit_blinded_transaction(blinded.clone())
    298             .unwrap();
    299         let leader = finalizer_ledger.expected_leader_for_next_block().unwrap();
    300         let finalizer = finalizers
    301             .iter()
    302             .find(|wallet| wallet.address() == leader)
    303             .unwrap();
    304         let burn = finalizer_ledger.build_burn(finalizer, 1, 0).unwrap();
    305         finalizer_ledger.submit_transaction(burn).unwrap();
    306         let commit_block = finalizer_ledger.mine_next_block(finalizer, 1).unwrap();
    307 
    308         wallet_node
    309             .receive(GossipEnvelope::Block(commit_block))
    310             .unwrap();
    311 
    312         assert_eq!(wallet_node.ledger().balance_of(carol.address()), 0);
    313         assert_eq!(wallet_node.status().wallet_balance, expected_balance);
    314     }
    315 
    316     #[test]
    317     fn owned_blinded_transaction_restore_requeues_pending_commit() {
    318         let alice = Wallet::from_seed("owned-blinded-restore-pending-alice");
    319         let bob = Wallet::from_seed("owned-blinded-restore-pending-bob");
    320         let mut allocations = BTreeMap::new();
    321         allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    322         let ledger = Ledger::new(allocations, 1);
    323         let mut node = NodeCore::from_ledger(alice.clone(), ledger.clone(), 0);
    324 
    325         let blinded = node
    326             .blinded_transfer_with_fee(bob.address(), MICRO_IUNA, 7, node.chain_height() + 4)
    327             .unwrap();
    328         let owned = node.owned_blinded_transactions();
    329         let mut restarted = NodeCore::from_ledger(alice, ledger, 0);
    330 
    331         restarted.restore_owned_blinded_transactions(owned).unwrap();
    332 
    333         assert_eq!(
    334             restarted.ledger().pending_blinded_transactions(),
    335             std::slice::from_ref(&blinded)
    336         );
    337         let outbox = restarted.drain_outbox();
    338         assert!(outbox.iter().any(|envelope| matches!(
    339             envelope,
    340             GossipEnvelope::BlindedTransaction(transaction)
    341                 if transaction.commitment == blinded.commitment
    342         )));
    343         assert!(!outbox.iter().any(|envelope| matches!(
    344             envelope,
    345             GossipEnvelope::BlindedReveal(reveal) if reveal.commitment == blinded.commitment
    346         )));
    347     }
    348 
    349     #[test]
    350     fn owned_blinded_transaction_restore_skips_stale_commit_with_spent_input() {
    351         let alice = Wallet::from_seed("owned-blinded-restore-stale-alice");
    352         let bob = Wallet::from_seed("owned-blinded-restore-stale-bob");
    353         let finalizers = [bob.clone()];
    354         let mut allocations = BTreeMap::new();
    355         allocations.insert(alice.address().to_string(), MICRO_IUNA);
    356         allocations.insert(bob.address().to_string(), MICRO_IUNA);
    357         let ledger = Ledger::new_with_genesis_burns(
    358             allocations,
    359             finalizers
    360                 .iter()
    361                 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    362                 .collect(),
    363             1,
    364         )
    365         .unwrap();
    366         let mut wallet_node = NodeCore::from_ledger(alice.clone(), ledger.clone(), 0);
    367 
    368         wallet_node
    369             .blinded_burn_with_fee(MICRO_IUNA / 10, 7, wallet_node.chain_height() + 4)
    370             .unwrap();
    371         let owned = wallet_node.owned_blinded_transactions();
    372         let stale_conflict = ledger.build_burn(&alice, MICRO_IUNA / 10, 7).unwrap();
    373         let mut advanced_ledger = ledger;
    374         advanced_ledger.submit_transaction(stale_conflict).unwrap();
    375         let leader = advanced_ledger.expected_leader_for_next_block().unwrap();
    376         assert_eq!(leader, bob.address());
    377         let anchor_burn = advanced_ledger.build_burn(&bob, 1, 0).unwrap();
    378         advanced_ledger.submit_transaction(anchor_burn).unwrap();
    379         let block = advanced_ledger.mine_next_block(&bob, 1).unwrap();
    380         advanced_ledger.apply_block(block).unwrap();
    381         let mut restarted = NodeCore::from_ledger(alice, advanced_ledger, 0);
    382 
    383         restarted.restore_owned_blinded_transactions(owned).unwrap();
    384 
    385         assert!(restarted.owned_blinded_transactions().is_empty());
    386         assert!(restarted.ledger().pending_blinded_transactions().is_empty());
    387         assert!(restarted.drain_outbox().is_empty());
    388     }
    389 
    390     #[test]
    391     fn owned_blinded_transaction_restore_publishes_reveal_after_commit() {
    392         let alice = Wallet::from_seed("owned-blinded-restore-reveal-alice");
    393         let bob = Wallet::from_seed("owned-blinded-restore-reveal-bob");
    394         let carol = Wallet::from_seed("owned-blinded-restore-reveal-carol");
    395         let finalizers = [alice.clone(), bob.clone()];
    396         let mut allocations = BTreeMap::new();
    397         allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
    398         allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA);
    399         allocations.insert(carol.address().to_string(), 10 * MICRO_IUNA);
    400         let ledger = Ledger::new_with_genesis_burns(
    401             allocations,
    402             finalizers
    403                 .iter()
    404                 .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    405                 .collect(),
    406             1,
    407         )
    408         .unwrap();
    409         let mut wallet_node = NodeCore::from_ledger(carol.clone(), ledger.clone(), 0);
    410         let mut finalizer_ledger = ledger;
    411 
    412         let blinded = wallet_node
    413             .blinded_burn_with_fee(3, 7, wallet_node.chain_height() + 4)
    414             .unwrap();
    415         let owned = wallet_node.owned_blinded_transactions();
    416         finalizer_ledger
    417             .submit_blinded_transaction(blinded.clone())
    418             .unwrap();
    419         let leader = finalizer_ledger.expected_leader_for_next_block().unwrap();
    420         let finalizer = finalizers
    421             .iter()
    422             .find(|wallet| wallet.address() == leader)
    423             .unwrap();
    424         let burn = finalizer_ledger.build_burn(finalizer, 1, 0).unwrap();
    425         finalizer_ledger.submit_transaction(burn).unwrap();
    426         let commit_block = finalizer_ledger.mine_next_block(finalizer, 1).unwrap();
    427         finalizer_ledger.apply_block(commit_block.clone()).unwrap();
    428         let mut restarted_ledger = NodeCore::from_ledger(
    429             carol,
    430             Ledger::from_snapshot(finalizer_ledger.snapshot()).unwrap(),
    431             0,
    432         );
    433 
    434         restarted_ledger
    435             .restore_owned_blinded_transactions(owned)
    436             .unwrap();
    437 
    438         assert!(
    439             restarted_ledger
    440                 .ledger()
    441                 .pending_blinded_reveals()
    442                 .iter()
    443                 .any(|reveal| reveal.commitment == blinded.commitment)
    444         );
    445         assert!(
    446             restarted_ledger
    447                 .drain_outbox()
    448                 .iter()
    449                 .any(|envelope| matches!(
    450                     envelope,
    451                     GossipEnvelope::BlindedReveal(reveal) if reveal.commitment == blinded.commitment
    452                 ))
    453         );
    454         assert!(
    455             commit_block
    456                 .blinded_transactions
    457                 .iter()
    458                 .any(|transaction| transaction.commitment == blinded.commitment)
    459         );
    460     }
    461 }