ledger_apply.rs (31454B)
1 use std::collections::BTreeSet; 2 3 use anyhow::{Context, Result, bail}; 4 5 use super::ledger_ops::{ 6 block_reward, credit_reward_outputs, ensure_block_has_burn, ensure_outputs_do_not_overflow, 7 ensure_single_input_owner, ensure_valid_recovery_block, validate_block_fee_policy, 8 verify_address_signature, verify_leader_proof, 9 }; 10 use super::ledger_v2::{ 11 apply_transaction_v2_with_lineage, decode_canonical_transaction_v2_envelope, 12 }; 13 use super::mine_policy::ensure_mine_anchor_limit; 14 use super::ticket::{ 15 apply_finalizer_ticket_effects, ticket_block_min_timestamp, tickets_created_by_block, 16 }; 17 use super::transaction::transaction_inputs_available; 18 use super::{ 19 AddressNetwork, AddressVersion, Amount, BLOCK_MEDIAN_TIME_PAST_WINDOW, Block, 20 BurnBundleSection, FinalityCheckpoint, FinalizerMode, Ledger, 21 MAX_BLOCK_TIMESTAMP_FUTURE_DRIFT_MS, TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT, 22 Transaction, ensure_transaction_v2_active, hex_encode, insert_output_with_lineage, 23 output_lineage_root_for_transaction, spend_inputs_with_lineage, unix_now_ms, verify_vdf, 24 }; 25 26 impl Ledger { 27 pub fn apply_block(&mut self, block: Block) -> Result<()> { 28 self.apply_block_at(block, unix_now_ms()) 29 } 30 31 pub(crate) fn apply_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> { 32 self.apply_block_with_vdf_policy(block, true, now_ms) 33 } 34 35 pub(crate) fn block_requires_vdf_verification_at( 36 &self, 37 block: &Block, 38 now_ms: u64, 39 ) -> Result<bool> { 40 self.precheck_block_without_vdf_at(block, now_ms) 41 } 42 43 pub fn apply_locally_mined_block(&mut self, block: Block) -> Result<()> { 44 self.apply_self_produced_block_at(block, unix_now_ms()) 45 } 46 47 pub(crate) fn apply_self_produced_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> { 48 self.verify_self_produced_block_at(&block, now_ms)?; 49 self.apply_preverified_block_at(block, now_ms) 50 } 51 52 pub(crate) fn verify_self_produced_block_at(&self, block: &Block, now_ms: u64) -> Result<()> { 53 let mut verifier = self.clone(); 54 verifier.apply_preverified_block_at(block.clone(), now_ms)?; 55 Ok(()) 56 } 57 58 pub(crate) fn apply_preverified_block_at(&mut self, block: Block, now_ms: u64) -> Result<()> { 59 self.apply_block_with_vdf_policy(block, false, now_ms) 60 } 61 62 fn apply_block_with_vdf_policy( 63 &mut self, 64 block: Block, 65 should_verify_vdf: bool, 66 now_ms: u64, 67 ) -> Result<()> { 68 if !self.precheck_block_without_vdf_at(&block, now_ms)? { 69 return Ok(()); 70 } 71 72 if should_verify_vdf && !verify_vdf(&block.vdf_seed(), block.vdf_rounds, &block.vdf_output) 73 { 74 bail!("block VDF output is invalid"); 75 } 76 77 let reward_committee = self.burn_committee_for_block(&block); 78 let certified_parent = self 79 .block_certifies_parent(&block, reward_committee.len()) 80 .then(|| FinalityCheckpoint { 81 height: self.tip().height, 82 hash: self.tip().hash.clone(), 83 }); 84 let mut utxos = self.utxos.clone(); 85 let mut utxo_lineage = self.utxo_lineage.clone(); 86 let mut lineage_values = self.lineage_values.clone(); 87 let mut lineage_owners = self.lineage_owners.clone(); 88 let signing_domain = self.transaction_signing_domain_at(block.height); 89 let mut signatures = BTreeSet::new(); 90 for tx in &block.transactions { 91 if !signatures.insert(tx.signature()) { 92 bail!("duplicate transaction in block"); 93 } 94 self.validate_transaction_terms(tx)?; 95 self.validate_transaction_anchor_for_block(tx, block.height)?; 96 apply_transaction_with_lineage( 97 tx, 98 block.height, 99 &mut utxos, 100 &mut utxo_lineage, 101 &mut lineage_values, 102 &mut lineage_owners, 103 Some(&signing_domain), 104 )?; 105 } 106 let transaction_v2_domain = self.transaction_v2_domain()?; 107 let network = AddressNetwork::from_profile_id(&self.launch_profile.profile_id); 108 let mut transaction_v2_ids = BTreeSet::new(); 109 for envelope in &block.transactions_v2 { 110 let transaction = 111 decode_canonical_transaction_v2_envelope(envelope, &transaction_v2_domain)?; 112 self.validate_transaction_v2_anchor_for_block(&transaction, block.height)?; 113 let transaction_id = hex_encode(transaction.transaction_id(&transaction_v2_domain)?); 114 if !transaction_v2_ids.insert(transaction_id) { 115 bail!("duplicate transaction v2 in block"); 116 } 117 apply_transaction_v2_with_lineage( 118 &transaction, 119 &transaction_v2_domain, 120 network, 121 &mut utxos, 122 &mut utxo_lineage, 123 &mut lineage_values, 124 &mut lineage_owners, 125 true, 126 )?; 127 } 128 let expected_reward = self.expected_reward_for_block(&block)?; 129 if block.reward != expected_reward { 130 bail!("block reward is invalid"); 131 } 132 let mined_signatures = block 133 .transactions 134 .iter() 135 .map(|tx| tx.signature().to_string()) 136 .collect::<BTreeSet<_>>(); 137 let mut tickets = self.tickets.clone(); 138 apply_finalizer_ticket_effects(self.tip(), &block, &mut tickets)?; 139 tickets.extend(tickets_created_by_block(&block, &self.launch_profile)?); 140 credit_reward_outputs(&mut utxos, &block, &reward_committee)?; 141 self.compact_block_context.append_block(&block)?; 142 self.utxos = utxos; 143 self.utxo_lineage = utxo_lineage; 144 self.lineage_values = lineage_values; 145 self.lineage_owners = lineage_owners; 146 self.tickets = tickets; 147 self.mined_transaction_ids 148 .extend(mined_signatures.iter().cloned()); 149 self.mined_transaction_ids 150 .extend(transaction_v2_ids.iter().cloned()); 151 self.chain.push(block); 152 self.update_mine_difficulty_cache_after_tip(); 153 if let Some(checkpoint) = certified_parent { 154 self.objective_finality_checkpoint = Some(checkpoint); 155 } 156 let available = self.utxos.clone(); 157 let pending = std::mem::take(&mut self.pending); 158 self.pending = pending 159 .into_iter() 160 .filter(|tx| { 161 !mined_signatures.contains(tx.signature()) 162 && transaction_inputs_available(tx, &available) 163 && self.validate_transaction_terms(tx).is_ok() 164 && self.validate_transaction_anchor_for_pending(tx).is_ok() 165 && tx 166 .verify_signature(&self.transaction_signing_domain_for_pending(tx)) 167 .is_ok() 168 }) 169 .collect(); 170 let orphans = std::mem::take(&mut self.orphans); 171 self.orphans = orphans 172 .into_iter() 173 .filter(|tx| { 174 !mined_signatures.contains(tx.signature()) 175 && self.validate_transaction_terms(tx).is_ok() 176 && self.validate_transaction_anchor_for_pending(tx).is_ok() 177 && tx 178 .verify_signature(&self.transaction_signing_domain_for_pending(tx)) 179 .is_ok() 180 }) 181 .collect(); 182 self.refresh_pending_pool_byte_counters()?; 183 self.promote_orphan_transactions()?; 184 self.revalidate_pending_v2()?; 185 self.vdf_rounds = self.next_vdf_rounds_after_tip(); 186 Ok(()) 187 } 188 189 /// Rebuild derived state from a block that this node previously validated and persisted. 190 /// This is deliberately private to snapshot restoration: network and newly produced blocks 191 /// must always use one of the validating apply paths above. 192 pub(super) fn apply_trusted_block_at(&mut self, block: Block) -> Result<()> { 193 debug_assert!(self.pending.is_empty() && self.orphans.is_empty()); 194 195 let reward_committee = self.burn_committee_for_block(&block); 196 let certified_parent = self 197 .block_certifies_parent(&block, reward_committee.len()) 198 .then(|| FinalityCheckpoint { 199 height: self.tip().height, 200 hash: self.tip().hash.clone(), 201 }); 202 for transaction in &block.transactions { 203 apply_transaction_with_lineage( 204 transaction, 205 block.height, 206 &mut self.utxos, 207 &mut self.utxo_lineage, 208 &mut self.lineage_values, 209 &mut self.lineage_owners, 210 None, 211 )?; 212 } 213 let transaction_v2_domain = self.transaction_v2_domain()?; 214 let network = AddressNetwork::from_profile_id(&self.launch_profile.profile_id); 215 let mut transaction_v2_ids = BTreeSet::new(); 216 for envelope in &block.transactions_v2 { 217 let transaction = 218 decode_canonical_transaction_v2_envelope(envelope, &transaction_v2_domain)?; 219 let transaction_id = hex_encode(transaction.transaction_id(&transaction_v2_domain)?); 220 apply_transaction_v2_with_lineage( 221 &transaction, 222 &transaction_v2_domain, 223 network, 224 &mut self.utxos, 225 &mut self.utxo_lineage, 226 &mut self.lineage_values, 227 &mut self.lineage_owners, 228 false, 229 )?; 230 transaction_v2_ids.insert(transaction_id); 231 } 232 233 let mined_signatures = block 234 .transactions 235 .iter() 236 .map(|transaction| transaction.signature().to_string()) 237 .collect::<BTreeSet<_>>(); 238 let parent = self.tip().clone(); 239 apply_finalizer_ticket_effects(&parent, &block, &mut self.tickets)?; 240 self.tickets 241 .extend(tickets_created_by_block(&block, &self.launch_profile)?); 242 credit_reward_outputs(&mut self.utxos, &block, &reward_committee)?; 243 self.compact_block_context.append_trusted_block(&block)?; 244 self.mined_transaction_ids.extend(mined_signatures); 245 self.mined_transaction_ids.extend(transaction_v2_ids); 246 self.chain.push(block); 247 self.update_mine_difficulty_cache_after_tip(); 248 if let Some(checkpoint) = certified_parent { 249 self.objective_finality_checkpoint = Some(checkpoint); 250 } 251 self.vdf_rounds = self.next_vdf_rounds_after_tip(); 252 Ok(()) 253 } 254 255 fn ensure_block_transactions_are_not_replays(&self, block: &Block) -> Result<()> { 256 if block.height < TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT { 257 return Ok(()); 258 } 259 if block 260 .transactions 261 .iter() 262 .any(|transaction| self.mined_transaction_ids.contains(transaction.signature())) 263 { 264 bail!("block replays a previously mined transaction"); 265 } 266 let domain = self.transaction_v2_domain()?; 267 let mut transaction_v2_ids = BTreeSet::new(); 268 for envelope in &block.transactions_v2 { 269 let transaction = decode_canonical_transaction_v2_envelope(envelope, &domain)?; 270 let transaction_id = hex_encode(transaction.transaction_id(&domain)?); 271 if !transaction_v2_ids.insert(transaction_id.clone()) { 272 bail!("duplicate transaction v2 in block"); 273 } 274 if self.mined_transaction_ids.contains(&transaction_id) { 275 bail!("block replays a previously mined transaction v2"); 276 } 277 } 278 Ok(()) 279 } 280 281 fn precheck_block_without_vdf_at(&self, block: &Block, now_ms: u64) -> Result<bool> { 282 if block.height <= self.tip().height { 283 let existing = self 284 .chain 285 .get(block.height as usize) 286 .with_context(|| format!("local chain has no block at height {}", block.height))?; 287 if existing.hash == block.hash { 288 return Ok(false); 289 } 290 return Err(super::ValidationError::BlockConflictsWithLocalChain { 291 height: block.height, 292 } 293 .into()); 294 } 295 296 let expected_height = self.tip().height + 1; 297 if block.height != expected_height { 298 return Err(super::ValidationError::UnexpectedBlockHeight { 299 expected: expected_height, 300 actual: block.height, 301 } 302 .into()); 303 } 304 if block.prev_hash != self.tip().hash { 305 return Err(super::ValidationError::BlockDoesNotExtendLocalTip.into()); 306 } 307 if block.compute_hash() != block.hash { 308 bail!("block hash is invalid"); 309 } 310 self.validate_reward_address(block.height, block.reward_address.as_deref(), "finalizer")?; 311 if block.height < super::HYBRID_REWARD_ACTIVATION_HEIGHT { 312 if block.reward_address_signature.is_some() { 313 bail!("finalizer reward address signature is not active yet"); 314 } 315 } else { 316 let reward_address = block 317 .reward_address 318 .as_deref() 319 .context("finalizer hybrid reward address is required")?; 320 let signature = block 321 .reward_address_signature 322 .as_deref() 323 .context("finalizer reward address signature is required")?; 324 verify_address_signature( 325 &block.miner, 326 &super::block::reward_address_payload( 327 block.height, 328 &block.prev_hash, 329 &block.miner, 330 reward_address, 331 ), 332 signature, 333 "finalizer reward address", 334 )?; 335 } 336 self.ensure_block_transactions_are_not_replays(block)?; 337 if block.reward != self.expected_reward_for_block(block)? { 338 bail!("block reward is invalid"); 339 } 340 let expected_vdf_rounds = self.expected_vdf_rounds_for_block(block)?; 341 if block.vdf_rounds != expected_vdf_rounds { 342 bail!("block VDF rounds are invalid"); 343 } 344 if block.timestamp_ms <= self.tip().timestamp_ms { 345 bail!("block timestamp must increase"); 346 } 347 if block.finalizer_mode == FinalizerMode::Ticket { 348 let min_timestamp = ticket_block_min_timestamp(self.tip(), block.finalizer_rank)?; 349 if block.timestamp_ms < min_timestamp { 350 return Err(super::ValidationError::BlockBeforeFinalizerRankSlot { 351 rank: block.finalizer_rank, 352 min_timestamp, 353 } 354 .into()); 355 } 356 } 357 let median_time_past = self.median_time_past(); 358 if block.timestamp_ms <= median_time_past { 359 bail!("block timestamp must exceed median time past"); 360 } 361 let max_future_timestamp = now_ms.saturating_add(MAX_BLOCK_TIMESTAMP_FUTURE_DRIFT_MS); 362 if block.timestamp_ms > max_future_timestamp { 363 return Err(super::ValidationError::BlockTimestampTooFarInFuture.into()); 364 } 365 if block 366 .transactions 367 .len() 368 .saturating_add(block.transactions_v2.len()) 369 > self.launch_profile.max_block_transactions 370 { 371 bail!("block has too many transactions"); 372 } 373 if !block.transactions_v2.is_empty() { 374 ensure_transaction_v2_active(block.height)?; 375 } 376 if self.consensus_block_size_bytes(block)? > self.launch_profile.max_block_bytes { 377 bail!("block exceeds max block size"); 378 } 379 ensure_mine_anchor_limit(block.height, &block.transactions)?; 380 ensure_block_has_burn(&block.transactions, &block.transactions_v2)?; 381 validate_block_fee_policy(block)?; 382 self.validate_burn_bundle_section_for_block(block)?; 383 match block.finalizer_mode { 384 FinalizerMode::Ticket => { 385 let selected_ticket = self 386 .ticket_for_finalizer_rank(block.height, block.finalizer_rank) 387 .context("no selected ticket for block finalizer rank")?; 388 if selected_ticket.owner != block.miner { 389 bail!( 390 "block finalizer {} is not selected for rank {}", 391 block.miner, 392 block.finalizer_rank 393 ); 394 } 395 if block 396 .leader_proof 397 .as_ref() 398 .is_none_or(|proof| proof.ticket_id != selected_ticket.id) 399 { 400 bail!("block does not prove the selected leader ticket"); 401 } 402 verify_leader_proof(block, &self.tickets)?; 403 } 404 FinalizerMode::Recovery => { 405 ensure_valid_recovery_block(block, self.tip())?; 406 } 407 } 408 409 Ok(true) 410 } 411 412 fn median_time_past(&self) -> u64 { 413 let mut timestamps = self 414 .chain 415 .iter() 416 .rev() 417 .take(BLOCK_MEDIAN_TIME_PAST_WINDOW) 418 .map(|block| block.timestamp_ms) 419 .collect::<Vec<_>>(); 420 timestamps.sort_unstable(); 421 timestamps[timestamps.len() / 2] 422 } 423 424 pub(super) fn expected_reward_for_next_block( 425 &self, 426 transactions: &[Transaction], 427 transactions_v2: &[String], 428 _burn_bundle_section: &BurnBundleSection, 429 ) -> Result<Amount> { 430 let v2_fees = self.transaction_v2_fees(transactions_v2)?; 431 block_reward(transactions, v2_fees) 432 } 433 434 fn expected_reward_for_block(&self, block: &Block) -> Result<Amount> { 435 let v2_fees = self.transaction_v2_fees(&block.transactions_v2)?; 436 block_reward(&block.transactions, v2_fees) 437 } 438 439 pub(super) fn validate_reward_address( 440 &self, 441 height: u64, 442 address: Option<&str>, 443 label: &str, 444 ) -> Result<()> { 445 if height < super::HYBRID_REWARD_ACTIVATION_HEIGHT { 446 if address.is_some() { 447 bail!( 448 "{label} hybrid reward address is not active before height {}", 449 super::HYBRID_REWARD_ACTIVATION_HEIGHT 450 ); 451 } 452 return Ok(()); 453 } 454 let address = 455 address.with_context(|| format!("{label} hybrid reward address is required"))?; 456 let decoded = super::decode_versioned_address( 457 address, 458 AddressNetwork::from_profile_id(&self.launch_profile.profile_id), 459 )?; 460 if decoded.version != AddressVersion::HybridKeyCommitment { 461 bail!("{label} reward address must use address v1"); 462 } 463 Ok(()) 464 } 465 466 fn transaction_v2_fees(&self, envelopes: &[String]) -> Result<Amount> { 467 let domain = self.transaction_v2_domain()?; 468 envelopes.iter().try_fold(0_u64, |total, envelope| { 469 let transaction = decode_canonical_transaction_v2_envelope(envelope, &domain)?; 470 if transaction.fee() == 0 { 471 bail!("block transaction v2 fee must be greater than zero"); 472 } 473 total 474 .checked_add(transaction.fee()) 475 .context("block transaction v2 fees overflow") 476 }) 477 } 478 } 479 480 fn apply_transaction_with_lineage( 481 transaction: &Transaction, 482 block_height: u64, 483 utxos: &mut std::collections::BTreeMap<super::OutPoint, super::TxOutput>, 484 utxo_lineage: &mut std::collections::BTreeMap<super::OutPoint, super::UtxoLineageRoot>, 485 lineage_values: &mut std::collections::BTreeMap<super::UtxoLineageRoot, Amount>, 486 lineage_owners: &mut super::LineageOwnerValues, 487 signing_domain: Option<&super::TransactionSigningDomain>, 488 ) -> Result<()> { 489 if let Some(signing_domain) = signing_domain { 490 transaction.verify_signature(signing_domain)?; 491 } 492 if matches!(transaction, Transaction::Mine { .. }) { 493 let output = transaction.outputs().remove(0); 494 ensure_outputs_do_not_overflow(utxos, std::slice::from_ref(&output))?; 495 insert_output_with_lineage( 496 super::OutPoint { 497 txid: transaction.signature().to_string(), 498 index: 0, 499 }, 500 output, 501 output_lineage_root_for_transaction(transaction, block_height, None), 502 utxos, 503 utxo_lineage, 504 lineage_values, 505 lineage_owners, 506 )?; 507 return Ok(()); 508 } 509 510 ensure_single_input_owner(transaction)?; 511 let (input_total, inherited_root) = spend_inputs_with_lineage( 512 transaction, 513 utxos, 514 utxo_lineage, 515 lineage_values, 516 lineage_owners, 517 )?; 518 let outputs = transaction.outputs(); 519 let output_total = outputs.iter().try_fold(0_u64, |total, output| { 520 total 521 .checked_add(output.amount) 522 .context("transaction outputs overflow") 523 })?; 524 let required = output_total 525 .checked_add(transaction.fee()) 526 .context("transaction outputs plus fee overflow")? 527 .checked_add(match transaction { 528 Transaction::Burn { amount, .. } => *amount, 529 Transaction::Transfer { .. } | Transaction::Mine { .. } => 0, 530 }) 531 .context("transaction outputs plus burn overflow")?; 532 if input_total != required { 533 bail!("transaction inputs do not balance outputs, burn, and fee"); 534 } 535 ensure_outputs_do_not_overflow(utxos, &outputs)?; 536 for (index, output) in outputs.into_iter().enumerate() { 537 insert_output_with_lineage( 538 super::OutPoint { 539 txid: transaction.signature().to_string(), 540 index: index as u32, 541 }, 542 output, 543 inherited_root.clone(), 544 utxos, 545 utxo_lineage, 546 lineage_values, 547 lineage_owners, 548 )?; 549 } 550 Ok(()) 551 } 552 553 #[cfg(test)] 554 mod tests { 555 use std::collections::BTreeMap; 556 557 use super::*; 558 use crate::domain::{BurnTicket, GenesisBurn, MICRO_IUNA, Wallet, run_vdf}; 559 560 fn mine_transaction(signature: &str) -> Transaction { 561 Transaction::Mine { 562 recipient: "1".repeat(64), 563 anchor: "2".repeat(64), 564 salt: 1, 565 nonce: 1, 566 difficulty_bits: 10, 567 proof_header: None, 568 signature: signature.to_string(), 569 } 570 } 571 572 #[test] 573 fn historical_mine_replay_is_rejected_from_height_1000() { 574 let signature = "3".repeat(64); 575 let mut ledger = Ledger::new(BTreeMap::new(), 1); 576 ledger.mined_transaction_ids.insert(signature.clone()); 577 let mut block = ledger.tip().clone(); 578 block.transactions = vec![mine_transaction(&signature)]; 579 580 block.height = TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT - 1; 581 ledger 582 .ensure_block_transactions_are_not_replays(&block) 583 .unwrap(); 584 585 block.height = TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT; 586 assert!( 587 ledger 588 .ensure_block_transactions_are_not_replays(&block) 589 .unwrap_err() 590 .to_string() 591 .contains("replays a previously mined transaction") 592 ); 593 } 594 595 #[test] 596 fn activated_block_validation_rejects_replayed_mine_proof() { 597 let wallet = Wallet::from_seed("activated-mine-replay-wallet"); 598 let mut ledger = Ledger::new_with_genesis_burns( 599 BTreeMap::from([(wallet.address().to_string(), 10 * MICRO_IUNA)]), 600 vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)], 601 1, 602 ) 603 .unwrap(); 604 ledger.chain.last_mut().unwrap().height = 605 TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT - 1; 606 ledger.tickets = vec![BurnTicket { 607 id: "5".repeat(64), 608 owner: wallet.address().to_string(), 609 amount: MICRO_IUNA, 610 eligible_from_height: TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT, 611 eligible_until_height: TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT, 612 }]; 613 614 let burn = ledger.build_burn_for_next_block(&wallet, 1, 1).unwrap(); 615 ledger.submit_transaction(burn).unwrap(); 616 let mine = ledger.build_mine(wallet.address()).unwrap(); 617 let replayed_id = mine.signature().to_string(); 618 ledger.submit_transaction(mine).unwrap(); 619 let prepared = ledger.prepare_next_block(wallet.address(), 1).unwrap(); 620 let block = prepared.finish(&wallet, "test-vdf-output".to_string()); 621 assert_eq!( 622 block.height, 623 TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT 624 ); 625 626 ledger.mined_transaction_ids.insert(replayed_id); 627 let error = ledger 628 .apply_preverified_block_at(block, 1) 629 .unwrap_err() 630 .to_string(); 631 632 assert!(error.contains("replays a previously mined transaction")); 633 } 634 635 #[test] 636 fn queued_burn_survives_one_applied_block_and_is_included_in_the_following_block() { 637 let finalizer = Wallet::from_seed("queued-burn-finalizer"); 638 let burner = Wallet::from_seed("queued-burn-wallet"); 639 let mut ledger = Ledger::new_with_genesis_burns( 640 BTreeMap::from([ 641 (finalizer.address().to_string(), 10 * MICRO_IUNA), 642 (burner.address().to_string(), 10 * MICRO_IUNA), 643 ]), 644 vec![GenesisBurn::new(finalizer.address(), MICRO_IUNA)], 645 1, 646 ) 647 .unwrap(); 648 ledger.chain.last_mut().unwrap().height = 649 super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT - 2; 650 ledger.tickets = vec![BurnTicket { 651 id: "6".repeat(64), 652 owner: finalizer.address().to_string(), 653 amount: MICRO_IUNA, 654 eligible_from_height: super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT - 1, 655 eligible_until_height: super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT - 1, 656 }]; 657 658 let queued_burn = ledger.build_burn(&burner, 1, 1).unwrap(); 659 assert_eq!(queued_burn.burn_anchor(), Some(ledger.tip_hash())); 660 ledger.submit_transaction(queued_burn.clone()).unwrap(); 661 let legacy_anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap(); 662 assert_eq!(legacy_anchor.burn_anchor(), None); 663 ledger.submit_transaction(legacy_anchor).unwrap(); 664 665 let prepared = ledger.prepare_next_block(finalizer.address(), 2).unwrap(); 666 let first_block = prepared.finish(&finalizer, "first-vdf-output".to_string()); 667 assert!( 668 first_block 669 .transactions 670 .iter() 671 .all(|transaction| transaction.signature() != queued_burn.signature()) 672 ); 673 ledger 674 .apply_preverified_block_at(first_block, u64::MAX) 675 .unwrap(); 676 677 assert!( 678 ledger 679 .pending() 680 .iter() 681 .any(|transaction| transaction.signature() == queued_burn.signature()) 682 ); 683 assert!(ledger.transaction_is_eligible_for_next_block(&queued_burn)); 684 685 ledger.tickets.push(BurnTicket { 686 id: "7".repeat(64), 687 owner: finalizer.address().to_string(), 688 amount: MICRO_IUNA, 689 eligible_from_height: super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT, 690 eligible_until_height: super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT, 691 }); 692 let activated_anchor = ledger.build_burn_for_next_block(&finalizer, 1, 1).unwrap(); 693 assert_eq!( 694 activated_anchor.burn_anchor(), 695 Some(ledger.tip().prev_hash.as_str()) 696 ); 697 ledger.submit_transaction(activated_anchor).unwrap(); 698 699 let prepared = ledger.prepare_next_block(finalizer.address(), 3).unwrap(); 700 let activated_block = prepared.finish(&finalizer, "second-vdf-output".to_string()); 701 assert_eq!( 702 activated_block.height, 703 super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT 704 ); 705 assert!( 706 activated_block 707 .transactions 708 .iter() 709 .any(|transaction| transaction.signature() == queued_burn.signature()) 710 ); 711 ledger 712 .apply_preverified_block_at(activated_block, u64::MAX) 713 .unwrap(); 714 assert!( 715 ledger 716 .pending() 717 .iter() 718 .all(|transaction| transaction.signature() != queued_burn.signature()) 719 ); 720 } 721 722 #[test] 723 fn applied_blocks_and_snapshot_restore_index_mined_transaction_ids() { 724 let wallet = Wallet::from_seed("replay-index-genesis-wallet"); 725 let mut ledger = Ledger::new_with_genesis_burns( 726 BTreeMap::from([(wallet.address().to_string(), 10 * MICRO_IUNA)]), 727 vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)], 728 1, 729 ) 730 .unwrap(); 731 let genesis_transaction_id = ledger.chain[0].transactions[0].signature().to_string(); 732 let burn = ledger.build_burn(&wallet, 1, 1).unwrap(); 733 ledger.submit_transaction(burn).unwrap(); 734 let mine = ledger.build_mine(wallet.address()).unwrap(); 735 let mine_transaction_id = mine.signature().to_string(); 736 ledger.submit_transaction(mine).unwrap(); 737 let prepared = ledger.prepare_next_block(wallet.address(), 1).unwrap(); 738 let vdf_output = run_vdf(prepared.vdf_seed(), prepared.vdf_rounds()); 739 let block = prepared.finish(&wallet, vdf_output); 740 ledger.apply_preverified_block_at(block, 1).unwrap(); 741 742 assert!( 743 ledger 744 .mined_transaction_ids 745 .contains(&genesis_transaction_id) 746 ); 747 assert!(ledger.mined_transaction_ids.contains(&mine_transaction_id)); 748 let restored = Ledger::from_persisted_snapshot(ledger.snapshot()).unwrap(); 749 assert!( 750 restored 751 .mined_transaction_ids 752 .contains(&genesis_transaction_id) 753 ); 754 assert!( 755 restored 756 .mined_transaction_ids 757 .contains(&mine_transaction_id) 758 ); 759 assert!(restored.has_transaction(&genesis_transaction_id)); 760 assert!(restored.has_transaction(&mine_transaction_id)); 761 } 762 763 #[test] 764 fn median_time_past_uses_the_median_of_the_latest_eleven_blocks() { 765 let mut ledger = Ledger::new(BTreeMap::new(), 1); 766 let template = ledger.tip().clone(); 767 let timestamps = [5, 500, 20, 400, 30, 300, 40, 200, 50, 100, 60, 1_000]; 768 ledger.chain = timestamps 769 .into_iter() 770 .enumerate() 771 .map(|(index, timestamp_ms)| { 772 let mut block = template.clone(); 773 block.height = index as u64; 774 block.timestamp_ms = timestamp_ms; 775 block.hash = format!("{:064x}", index + 1); 776 block 777 }) 778 .collect(); 779 780 // The oldest value (5) falls outside the 11-block window. The sorted 781 // window is 20,30,40,50,60,100,200,300,400,500,1000. 782 assert_eq!(ledger.median_time_past(), 100); 783 } 784 }