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 }