compact.rs (60674B)
1 use std::{collections::BTreeMap, sync::Arc}; 2 3 use anyhow::{Context, Result, bail}; 4 5 use crate::domain::{ 6 AddressNetwork, Amount, Block, BurnBundleSection, BurnBundleSignature, ChainSnapshot, 7 FinalizerMode, LaunchProfile, LeaderProof, MaskedBurn, OutPoint, Transaction, TxInput, 8 TxOutput, decode_versioned_address, 9 }; 10 11 const COMPACT_SNAPSHOT_MAGIC: &[u8] = b"IUNA-SNAPSHOT"; 12 const MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION: u8 = 6; 13 const COMPACT_SNAPSHOT_VERSION: u8 = 9; 14 const TRANSACTION_V2_COMPACT_SNAPSHOT_VERSION: u8 = 8; 15 const HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION: u8 = 9; 16 const VDF_SOLUTION_PREFIX: &str = "classgroup-wesolowski-bqfc-v1:"; 17 const MAX_COMPACT_GENESIS_ALLOCATIONS: usize = 100_000; 18 const MAX_COMPACT_SNAPSHOT_BLOCKS: usize = 10_000; 19 const MAX_COMPACT_VEC_ITEMS: usize = 10_000; 20 const MAX_COMPACT_BYTE_FIELD: usize = 8 * 1024 * 1024; 21 22 pub(super) fn legacy_compact_snapshot_version(bytes: &[u8]) -> Option<u8> { 23 let version_offset = COMPACT_SNAPSHOT_MAGIC.len(); 24 if !bytes.starts_with(COMPACT_SNAPSHOT_MAGIC) || bytes.len() <= version_offset { 25 return None; 26 } 27 let version = bytes[version_offset]; 28 (version < MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION).then_some(version) 29 } 30 31 #[derive(Clone, Debug, Default)] 32 struct EncodeTables { 33 addresses: BTreeMap<String, u64>, 34 protocol_ids: BTreeMap<String, u64>, 35 } 36 37 #[derive(Clone, Debug, Default)] 38 pub(crate) struct CompactBlockContext { 39 tables: EncodeTables, 40 size_breakdowns: Arc<BTreeMap<String, CompactBlockSizeBreakdown>>, 41 } 42 43 #[derive(Clone, Debug, Default, Eq, PartialEq)] 44 pub(crate) struct CompactBlockSizeBreakdown { 45 pub(crate) total_bytes: usize, 46 pub(crate) header_and_proof_bytes: usize, 47 pub(crate) transaction_bytes: usize, 48 pub(crate) transfer_bytes: usize, 49 pub(crate) burn_bytes: usize, 50 pub(crate) mine_bytes: usize, 51 pub(crate) burn_bundle_bytes: usize, 52 } 53 54 impl CompactBlockContext { 55 pub(crate) fn for_chain( 56 genesis_allocations: &BTreeMap<String, Amount>, 57 blocks: &[Block], 58 ) -> Result<Self> { 59 let mut context = Self::default(); 60 for address in genesis_allocations.keys() { 61 context.tables.register_address(address); 62 } 63 for block in blocks { 64 context.append_block_with_size_breakdown(block)?; 65 } 66 Ok(context) 67 } 68 69 pub(crate) fn block_size_bytes(&self, block: &Block) -> Result<usize> { 70 Ok(self.block_size_breakdown(block)?.total_bytes) 71 } 72 73 pub(crate) fn block_size_breakdown(&self, block: &Block) -> Result<CompactBlockSizeBreakdown> { 74 let mut tables = self.tables.clone(); 75 let mut writer = CompactWriter::default(); 76 // Preserve the exact pre-v2 consensus size for legacy-only blocks. Snapshot v8 has one 77 // additional count field, but that storage framing must not move the historical block-size 78 // boundary before height 3000. 79 let has_hybrid_rewards = block.reward_address.is_some() 80 || block.reward_address_signature.is_some() 81 || !block.burn_bundle_section.burns_v2.is_empty() 82 || block 83 .burn_bundle_section 84 .signatures 85 .iter() 86 .any(|signature| signature.reward_address.is_some()); 87 let version = if has_hybrid_rewards { 88 HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION 89 } else if block.transactions_v2.is_empty() { 90 TRANSACTION_V2_COMPACT_SNAPSHOT_VERSION - 1 91 } else { 92 TRANSACTION_V2_COMPACT_SNAPSHOT_VERSION 93 }; 94 encode_block_body_with_size_breakdown_for_version(&mut writer, block, &mut tables, version) 95 } 96 97 pub(crate) fn append_block(&mut self, block: &Block) -> Result<()> { 98 self.append_block_with_size_breakdown(block).map(|_| ()) 99 } 100 101 pub(crate) fn append_block_with_size_breakdown( 102 &mut self, 103 block: &Block, 104 ) -> Result<CompactBlockSizeBreakdown> { 105 let mut tables = self.tables.clone(); 106 let mut writer = CompactWriter::default(); 107 let breakdown = encode_block_body_with_size_breakdown(&mut writer, block, &mut tables)?; 108 tables.register_protocol_id(&block.hash); 109 self.tables = tables; 110 Arc::make_mut(&mut self.size_breakdowns).insert(block.hash.clone(), breakdown.clone()); 111 Ok(breakdown) 112 } 113 114 pub(crate) fn append_trusted_block(&mut self, block: &Block) -> Result<()> { 115 self.append_block(block) 116 } 117 118 pub(crate) fn stored_block_size_breakdown( 119 &self, 120 block_hash: &str, 121 ) -> Option<&CompactBlockSizeBreakdown> { 122 self.size_breakdowns.get(block_hash) 123 } 124 } 125 126 #[derive(Default)] 127 struct DecodeTables { 128 addresses: Vec<String>, 129 address_indices: BTreeMap<String, u64>, 130 protocol_ids: Vec<String>, 131 protocol_id_indices: BTreeMap<String, u64>, 132 } 133 134 impl EncodeTables { 135 fn register_address(&mut self, value: &str) { 136 let next = self.addresses.len() as u64; 137 self.addresses.entry(value.to_string()).or_insert(next); 138 } 139 140 fn register_protocol_id(&mut self, value: &str) { 141 let next = self.protocol_ids.len() as u64; 142 self.protocol_ids.entry(value.to_string()).or_insert(next); 143 } 144 } 145 146 impl DecodeTables { 147 fn register_address(&mut self, value: &str) { 148 if !self.address_indices.contains_key(value) { 149 let index = self.addresses.len() as u64; 150 self.addresses.push(value.to_string()); 151 self.address_indices.insert(value.to_string(), index); 152 } 153 } 154 155 fn register_protocol_id(&mut self, value: &str) { 156 if !self.protocol_id_indices.contains_key(value) { 157 let index = self.protocol_ids.len() as u64; 158 self.protocol_ids.push(value.to_string()); 159 self.protocol_id_indices.insert(value.to_string(), index); 160 } 161 } 162 } 163 164 pub(super) fn encode_compact_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<u8>> { 165 let mut writer = CompactWriter::default(); 166 let mut tables = EncodeTables::default(); 167 encode_compact_snapshot_prefix( 168 &mut writer, 169 &mut tables, 170 &snapshot.genesis_allocations, 171 snapshot.vdf_rounds, 172 &snapshot.launch_profile, 173 snapshot.blocks.len(), 174 )?; 175 let mut expected_prev_hash = "0".repeat(64); 176 for (height, block) in snapshot.blocks.iter().enumerate() { 177 if block.height != height as u64 { 178 bail!( 179 "chain snapshot block height {} does not match compact position {}", 180 block.height, 181 height 182 ); 183 } 184 if block.prev_hash != expected_prev_hash { 185 bail!("chain snapshot block {height} has non-canonical previous hash"); 186 } 187 if block.hash != block.compute_hash() { 188 bail!("chain snapshot block {height} has a non-canonical hash"); 189 } 190 encode_block_body(&mut writer, block, &mut tables)?; 191 tables.register_protocol_id(&block.hash); 192 expected_prev_hash = block.hash.clone(); 193 } 194 Ok(writer.into_inner()) 195 } 196 197 pub(crate) fn compact_snapshot_fixed_prefix_size( 198 genesis_allocations: &BTreeMap<String, Amount>, 199 vdf_rounds: u64, 200 launch_profile: &LaunchProfile, 201 ) -> Result<usize> { 202 let mut writer = CompactWriter::default(); 203 let mut tables = EncodeTables::default(); 204 encode_compact_snapshot_fixed_prefix( 205 &mut writer, 206 &mut tables, 207 genesis_allocations, 208 vdf_rounds, 209 launch_profile, 210 )?; 211 Ok(writer.into_inner().len()) 212 } 213 214 pub(crate) fn compact_varint_size(mut value: usize) -> usize { 215 let mut bytes = 1; 216 while value >= 0x80 { 217 value >>= 7; 218 bytes += 1; 219 } 220 bytes 221 } 222 223 fn encode_compact_snapshot_prefix( 224 writer: &mut CompactWriter, 225 tables: &mut EncodeTables, 226 genesis_allocations: &BTreeMap<String, Amount>, 227 vdf_rounds: u64, 228 launch_profile: &LaunchProfile, 229 block_count: usize, 230 ) -> Result<()> { 231 encode_compact_snapshot_fixed_prefix( 232 writer, 233 tables, 234 genesis_allocations, 235 vdf_rounds, 236 launch_profile, 237 )?; 238 writer.varint(block_count as u64); 239 Ok(()) 240 } 241 242 fn encode_compact_snapshot_fixed_prefix( 243 writer: &mut CompactWriter, 244 tables: &mut EncodeTables, 245 genesis_allocations: &BTreeMap<String, Amount>, 246 vdf_rounds: u64, 247 launch_profile: &LaunchProfile, 248 ) -> Result<()> { 249 writer.bytes(COMPACT_SNAPSHOT_MAGIC); 250 let version = COMPACT_SNAPSHOT_VERSION; 251 writer.u8(version); 252 writer.varint(genesis_allocations.len() as u64); 253 for (address, amount) in genesis_allocations { 254 writer.address(address, tables)?; 255 writer.varint(*amount); 256 } 257 writer.varint(vdf_rounds); 258 encode_launch_profile(writer, launch_profile); 259 Ok(()) 260 } 261 262 pub(super) fn decode_compact_snapshot(bytes: &[u8]) -> Result<ChainSnapshot> { 263 let mut reader = CompactReader::new(bytes); 264 let mut tables = DecodeTables::default(); 265 reader.magic(COMPACT_SNAPSHOT_MAGIC)?; 266 let version = reader.u8()?; 267 if !(MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION..=COMPACT_SNAPSHOT_VERSION).contains(&version) { 268 bail!("unsupported compact chain snapshot version {version}"); 269 } 270 let genesis_count = 271 reader.bounded_usize("genesis allocation count", MAX_COMPACT_GENESIS_ALLOCATIONS)?; 272 let mut genesis_allocations = std::collections::BTreeMap::new(); 273 for _ in 0..genesis_count { 274 let address = reader.address(&mut tables)?; 275 let amount = reader.varint()?; 276 genesis_allocations.insert(address, amount); 277 } 278 let vdf_rounds = reader.varint()?; 279 let launch_profile = decode_launch_profile(&mut reader)?; 280 let block_count = reader.bounded_usize("block count", MAX_COMPACT_SNAPSHOT_BLOCKS)?; 281 let mut blocks = Vec::with_capacity(block_count); 282 let mut prev_hash = "0".repeat(64); 283 for height in 0..block_count { 284 let block = decode_block_body_for_version( 285 &mut reader, 286 height as u64, 287 prev_hash, 288 &mut tables, 289 version, 290 )?; 291 tables.register_protocol_id(&block.hash); 292 prev_hash = block.hash.clone(); 293 blocks.push(block); 294 } 295 reader.finish()?; 296 Ok(ChainSnapshot { 297 genesis_allocations, 298 vdf_rounds, 299 launch_profile, 300 blocks, 301 }) 302 } 303 304 fn encode_launch_profile(writer: &mut CompactWriter, profile: &LaunchProfile) { 305 writer.string(&profile.profile_id); 306 writer.varint(profile.ticket_maturity_delay_heights); 307 writer.varint(profile.ticket_expiry_window_heights); 308 writer.varint(u64::from(profile.mine_difficulty_bits)); 309 writer.varint(profile.burn_lineage_maturity_heights); 310 writer.varint(profile.max_pending_transactions as u64); 311 writer.varint(profile.max_block_transactions as u64); 312 writer.varint(profile.max_block_bytes as u64); 313 } 314 315 fn decode_launch_profile(reader: &mut CompactReader<'_>) -> Result<LaunchProfile> { 316 Ok(LaunchProfile { 317 profile_id: reader.string()?, 318 ticket_maturity_delay_heights: reader.varint()?, 319 ticket_expiry_window_heights: reader.varint()?, 320 mine_difficulty_bits: reader.u32()?, 321 burn_lineage_maturity_heights: reader.varint()?, 322 max_pending_transactions: reader.usize()?, 323 max_block_transactions: reader.usize()?, 324 max_block_bytes: reader.usize()?, 325 }) 326 } 327 328 fn encode_block_body( 329 writer: &mut CompactWriter, 330 block: &Block, 331 tables: &mut EncodeTables, 332 ) -> Result<()> { 333 encode_block_body_with_size_breakdown(writer, block, tables).map(|_| ()) 334 } 335 336 fn encode_block_body_with_size_breakdown( 337 writer: &mut CompactWriter, 338 block: &Block, 339 tables: &mut EncodeTables, 340 ) -> Result<CompactBlockSizeBreakdown> { 341 encode_block_body_with_size_breakdown_for_version( 342 writer, 343 block, 344 tables, 345 COMPACT_SNAPSHOT_VERSION, 346 ) 347 } 348 349 fn encode_block_body_with_size_breakdown_for_version( 350 writer: &mut CompactWriter, 351 block: &Block, 352 tables: &mut EncodeTables, 353 version: u8, 354 ) -> Result<CompactBlockSizeBreakdown> { 355 let block_start = writer.bytes.len(); 356 writer.varint(block.timestamp_ms); 357 writer.address(&block.miner, tables)?; 358 if version >= HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION { 359 writer.bool(block.reward_address.is_some()); 360 if let Some(address) = &block.reward_address { 361 writer.address(address, tables)?; 362 } 363 writer.bool(block.reward_address_signature.is_some()); 364 if let Some(signature) = &block.reward_address_signature { 365 writer.fixed_hex::<64>(signature, "reward address signature")?; 366 } 367 } else if block.reward_address.is_some() || block.reward_address_signature.is_some() { 368 bail!("compact snapshot version {version} cannot encode a hybrid reward address"); 369 } 370 writer.u8(match block.finalizer_mode { 371 FinalizerMode::Ticket => 0, 372 FinalizerMode::Recovery => 1, 373 }); 374 writer.varint(u64::from(block.finalizer_rank)); 375 writer.varint(block.reward); 376 writer.varint(block.vdf_rounds); 377 writer.compact_vdf_output(&block.vdf_output)?; 378 writer.bool(block.leader_proof.is_some()); 379 if let Some(proof) = &block.leader_proof { 380 writer.protocol_id_ref(&proof.ticket_id, tables)?; 381 writer.address(&proof.public_key, tables)?; 382 writer.fixed_hex::<64>(&proof.signature, "leader signature")?; 383 } 384 writer.varint(block.transactions.len() as u64); 385 let header_end = writer.bytes.len(); 386 let transaction_start = writer.bytes.len(); 387 let mut transfer_bytes = 0_usize; 388 let mut burn_bytes = 0_usize; 389 let mut mine_bytes = 0_usize; 390 for transaction in &block.transactions { 391 let item_start = writer.bytes.len(); 392 encode_transaction(writer, transaction, tables)?; 393 let item_bytes = writer.bytes.len().saturating_sub(item_start); 394 match transaction { 395 Transaction::Transfer { .. } => { 396 transfer_bytes = transfer_bytes.saturating_add(item_bytes) 397 } 398 Transaction::Burn { .. } => burn_bytes = burn_bytes.saturating_add(item_bytes), 399 Transaction::Mine { .. } => mine_bytes = mine_bytes.saturating_add(item_bytes), 400 } 401 tables.register_protocol_id(transaction.signature()); 402 } 403 if version >= TRANSACTION_V2_COMPACT_SNAPSHOT_VERSION { 404 writer.varint(block.transactions_v2.len() as u64); 405 for envelope in &block.transactions_v2 { 406 writer.hex(envelope)?; 407 } 408 } else if !block.transactions_v2.is_empty() { 409 bail!("compact snapshot version {version} cannot encode transaction v2 envelopes"); 410 } 411 let burn_bundle_start = writer.bytes.len(); 412 encode_burn_bundle_section(writer, block, tables, version)?; 413 let block_end = writer.bytes.len(); 414 Ok(CompactBlockSizeBreakdown { 415 total_bytes: block_end.saturating_sub(block_start), 416 header_and_proof_bytes: header_end.saturating_sub(block_start), 417 transaction_bytes: burn_bundle_start.saturating_sub(transaction_start), 418 transfer_bytes, 419 burn_bytes, 420 mine_bytes, 421 burn_bundle_bytes: block_end.saturating_sub(burn_bundle_start), 422 }) 423 } 424 425 #[cfg(test)] 426 fn decode_block_body( 427 reader: &mut CompactReader<'_>, 428 height: u64, 429 prev_hash: String, 430 tables: &mut DecodeTables, 431 ) -> Result<Block> { 432 decode_block_body_for_version(reader, height, prev_hash, tables, COMPACT_SNAPSHOT_VERSION) 433 } 434 435 fn decode_block_body_for_version( 436 reader: &mut CompactReader<'_>, 437 height: u64, 438 prev_hash: String, 439 tables: &mut DecodeTables, 440 version: u8, 441 ) -> Result<Block> { 442 let timestamp_ms = reader.varint()?; 443 let miner = reader.address(tables)?; 444 let reward_address = if version >= HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION && reader.bool()? { 445 Some(reader.address(tables)?) 446 } else { 447 None 448 }; 449 let reward_address_signature = 450 if version >= HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION && reader.bool()? { 451 Some(reader.fixed_hex::<64>()?) 452 } else { 453 None 454 }; 455 let finalizer_mode = match reader.u8()? { 456 0 => FinalizerMode::Ticket, 457 1 => FinalizerMode::Recovery, 458 other => bail!("invalid finalizer mode tag {other}"), 459 }; 460 let finalizer_rank = reader.u32()?; 461 let reward = reader.varint()?; 462 let vdf_rounds = reader.varint()?; 463 let vdf_output = reader.compact_vdf_output()?; 464 let leader_proof = if reader.bool()? { 465 Some(LeaderProof { 466 ticket_id: reader.protocol_id_ref(tables)?, 467 public_key: reader.address(tables)?, 468 signature: reader.fixed_hex::<64>()?, 469 }) 470 } else { 471 None 472 }; 473 let transaction_count = 474 reader.bounded_usize("block transaction count", MAX_COMPACT_VEC_ITEMS)?; 475 let mut transactions = Vec::with_capacity(transaction_count); 476 for _ in 0..transaction_count { 477 let transaction = decode_transaction(reader, tables)?; 478 tables.register_protocol_id(transaction.signature()); 479 transactions.push(transaction); 480 } 481 let transactions_v2 = if version >= TRANSACTION_V2_COMPACT_SNAPSHOT_VERSION { 482 decode_vec( 483 reader, 484 "block transaction v2 count", 485 MAX_COMPACT_VEC_ITEMS, 486 |reader| reader.hex(), 487 )? 488 } else { 489 Vec::new() 490 }; 491 let burn_bundle_section = 492 decode_burn_bundle_section(reader, &transactions, &transactions_v2, tables, version)?; 493 let mut block = Block { 494 height, 495 prev_hash, 496 timestamp_ms, 497 miner, 498 reward_address, 499 reward_address_signature, 500 finalizer_mode, 501 finalizer_rank, 502 reward, 503 vdf_rounds, 504 vdf_output, 505 leader_proof, 506 burn_bundle_section, 507 transactions, 508 transactions_v2, 509 hash: String::new(), 510 }; 511 block.hash = block.compute_hash(); 512 Ok(block) 513 } 514 515 fn encode_burn_bundle_section( 516 writer: &mut CompactWriter, 517 block: &Block, 518 tables: &mut EncodeTables, 519 version: u8, 520 ) -> Result<()> { 521 let section = &block.burn_bundle_section; 522 writer.varint(section.signatures.len() as u64); 523 for signature in §ion.signatures { 524 writer.varint(u64::from(signature.slot)); 525 writer.address(&signature.member, tables)?; 526 if version >= HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION { 527 writer.bool(signature.reward_address.is_some()); 528 if let Some(address) = &signature.reward_address { 529 writer.address(address, tables)?; 530 } 531 } else if signature.reward_address.is_some() { 532 bail!("compact snapshot version {version} cannot encode a committee reward address"); 533 } 534 writer.fixed_hex::<64>(&signature.signature, "burn bundle signature")?; 535 } 536 writer.varint(section.burns.len() as u64); 537 for masked in §ion.burns { 538 let transaction_index = block 539 .transactions 540 .iter() 541 .position(|transaction| transaction == &masked.burn) 542 .context("burn bundle transaction is missing from block transactions")?; 543 writer.varint(transaction_index as u64); 544 writer.u8(masked.bundle_mask); 545 } 546 if version >= HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION { 547 writer.varint(section.burns_v2.len() as u64); 548 for masked in §ion.burns_v2 { 549 let transaction_index = block 550 .transactions_v2 551 .iter() 552 .position(|envelope| envelope == &masked.envelope) 553 .context("transaction v2 burn bundle envelope is missing from block")?; 554 writer.varint(transaction_index as u64); 555 writer.u8(masked.bundle_mask); 556 } 557 } else if !section.burns_v2.is_empty() { 558 bail!("compact snapshot version {version} cannot encode transaction v2 burn attestations"); 559 } 560 Ok(()) 561 } 562 563 fn decode_burn_bundle_section( 564 reader: &mut CompactReader<'_>, 565 transactions: &[Transaction], 566 transactions_v2: &[String], 567 tables: &mut DecodeTables, 568 version: u8, 569 ) -> Result<BurnBundleSection> { 570 let signatures = decode_vec( 571 reader, 572 "burn bundle signature count", 573 MAX_COMPACT_VEC_ITEMS, 574 |reader| { 575 Ok(BurnBundleSignature { 576 slot: u8::try_from(reader.varint()?).context("burn bundle slot does not fit u8")?, 577 member: reader.address(tables)?, 578 reward_address: if version >= HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION 579 && reader.bool()? 580 { 581 Some(reader.address(tables)?) 582 } else { 583 None 584 }, 585 signature: reader.fixed_hex::<64>()?, 586 }) 587 }, 588 )?; 589 let burns = decode_vec( 590 reader, 591 "burn bundle burn count", 592 MAX_COMPACT_VEC_ITEMS, 593 |reader| { 594 let transaction_index = reader.bounded_usize( 595 "burn bundle transaction index", 596 transactions.len().saturating_sub(1), 597 )?; 598 let burn = transactions 599 .get(transaction_index) 600 .context("burn bundle transaction index is out of bounds")? 601 .clone(); 602 if !burn.is_burn() { 603 bail!("burn bundle transaction index does not reference a burn"); 604 } 605 Ok(MaskedBurn { 606 burn, 607 bundle_mask: reader.u8()?, 608 }) 609 }, 610 )?; 611 let burns_v2 = if version >= HYBRID_REWARD_COMPACT_SNAPSHOT_VERSION { 612 decode_vec( 613 reader, 614 "transaction v2 burn bundle count", 615 MAX_COMPACT_VEC_ITEMS, 616 |reader| { 617 let transaction_index = reader.bounded_usize( 618 "transaction v2 burn bundle index", 619 transactions_v2.len().saturating_sub(1), 620 )?; 621 Ok(crate::domain::MaskedBurnV2 { 622 envelope: transactions_v2 623 .get(transaction_index) 624 .context("transaction v2 burn bundle index is out of bounds")? 625 .clone(), 626 bundle_mask: reader.u8()?, 627 }) 628 }, 629 )? 630 } else { 631 Vec::new() 632 }; 633 Ok(BurnBundleSection { 634 signatures, 635 burns, 636 burns_v2, 637 }) 638 } 639 640 fn encode_transaction( 641 writer: &mut CompactWriter, 642 transaction: &Transaction, 643 tables: &mut EncodeTables, 644 ) -> Result<()> { 645 match transaction { 646 Transaction::Transfer { 647 inputs, 648 outputs, 649 fee, 650 signature, 651 } => { 652 writer.u8(0); 653 let owner = common_input_owner(inputs)?; 654 encode_outpoints(writer, inputs, tables)?; 655 writer.address(owner, tables)?; 656 encode_outputs(writer, outputs, tables)?; 657 writer.varint(*fee); 658 writer.fixed_hex::<64>(signature, "transfer signature")?; 659 ensure_input_signatures(inputs, signature)?; 660 } 661 Transaction::Burn { 662 inputs, 663 change, 664 amount, 665 fee, 666 anchor, 667 signature, 668 } => { 669 writer.u8(1); 670 let owner = common_input_owner(inputs)?; 671 let genesis = inputs.iter().all(|input| input.signature == "genesis"); 672 if !genesis { 673 ensure_input_signatures(inputs, signature)?; 674 } 675 let change_mode = match change.as_slice() { 676 [] => 0, 677 [output] if output.address == owner => 1, 678 _ => 2, 679 }; 680 writer.u8(change_mode | (u8::from(genesis) << 2) | (u8::from(anchor.is_some()) << 3)); 681 encode_outpoints(writer, inputs, tables)?; 682 writer.address(owner, tables)?; 683 match change_mode { 684 0 => {} 685 1 => writer.varint(change[0].amount), 686 2 => encode_outputs(writer, change, tables)?, 687 _ => unreachable!(), 688 } 689 writer.varint(*amount); 690 writer.varint(*fee); 691 if let Some(anchor) = anchor { 692 writer.protocol_id_ref(anchor, tables)?; 693 } 694 if genesis { 695 writer.fixed_hex::<32>(signature, "genesis burn signature")?; 696 } else { 697 writer.fixed_hex::<64>(signature, "burn signature")?; 698 } 699 } 700 Transaction::Mine { 701 recipient, 702 anchor, 703 salt, 704 nonce, 705 difficulty_bits, 706 proof_header, 707 signature, 708 } => { 709 writer.u8(2); 710 writer.address(recipient, tables)?; 711 writer.protocol_id_ref(anchor, tables)?; 712 writer.varint(*salt); 713 writer.varint(*nonce); 714 writer.varint(u64::from(*difficulty_bits)); 715 writer.bool(proof_header.is_some()); 716 if let Some(proof_header) = proof_header { 717 writer.fixed_hex::<80>(proof_header, "mine proof header")?; 718 } 719 writer.fixed_hex::<32>(signature, "mine signature")?; 720 } 721 } 722 Ok(()) 723 } 724 725 fn decode_transaction( 726 reader: &mut CompactReader<'_>, 727 tables: &mut DecodeTables, 728 ) -> Result<Transaction> { 729 match reader.u8()? { 730 0 => { 731 let outpoints = decode_outpoints(reader, tables)?; 732 let owner = reader.address(tables)?; 733 let outputs = decode_outputs(reader, tables)?; 734 let fee = reader.varint()?; 735 let signature = reader.fixed_hex::<64>()?; 736 Ok(Transaction::Transfer { 737 inputs: signed_inputs(outpoints, &owner, &signature), 738 outputs, 739 fee, 740 signature, 741 }) 742 } 743 1 => { 744 let mode = reader.u8()?; 745 if mode & !0b1111 != 0 || mode & 0b11 > 2 { 746 bail!("invalid compact burn mode {mode}"); 747 } 748 let genesis = mode & 0b100 != 0; 749 let anchored = mode & 0b1000 != 0; 750 let outpoints = decode_outpoints(reader, tables)?; 751 let owner = reader.address(tables)?; 752 let change = match mode & 0b11 { 753 0 => Vec::new(), 754 1 => vec![TxOutput { 755 address: owner.clone(), 756 amount: reader.varint()?, 757 }], 758 2 => decode_outputs(reader, tables)?, 759 _ => unreachable!(), 760 }; 761 let amount = reader.varint()?; 762 let fee = reader.varint()?; 763 let anchor = if anchored { 764 Some(reader.protocol_id_ref(tables)?) 765 } else { 766 None 767 }; 768 let signature = if genesis { 769 reader.fixed_hex::<32>()? 770 } else { 771 reader.fixed_hex::<64>()? 772 }; 773 let input_signature = if genesis { "genesis" } else { &signature }; 774 Ok(Transaction::Burn { 775 inputs: signed_inputs(outpoints, &owner, input_signature), 776 change, 777 amount, 778 fee, 779 anchor, 780 signature, 781 }) 782 } 783 2 => { 784 let recipient = reader.address(tables)?; 785 let anchor = reader.protocol_id_ref(tables)?; 786 let salt = reader.varint()?; 787 let nonce = reader.varint()?; 788 let difficulty_bits = reader.u32()?; 789 let proof_header = if reader.bool()? { 790 Some(reader.fixed_hex::<80>()?) 791 } else { 792 None 793 }; 794 let signature = reader.fixed_hex::<32>()?; 795 Ok(Transaction::Mine { 796 recipient, 797 anchor, 798 salt, 799 nonce, 800 difficulty_bits, 801 proof_header, 802 signature, 803 }) 804 } 805 other => bail!("invalid transaction tag {other}"), 806 } 807 } 808 809 fn common_input_owner(inputs: &[TxInput]) -> Result<&str> { 810 let owner = inputs 811 .first() 812 .map(|input| input.owner.as_str()) 813 .context("stored transaction has no inputs")?; 814 if inputs.iter().any(|input| input.owner != owner) { 815 bail!("stored transaction inputs have different owners"); 816 } 817 Ok(owner) 818 } 819 820 fn ensure_input_signatures(inputs: &[TxInput], signature: &str) -> Result<()> { 821 if inputs.iter().any(|input| input.signature != signature) { 822 bail!("stored transaction input signature differs from transaction signature"); 823 } 824 Ok(()) 825 } 826 827 fn encode_outpoints( 828 writer: &mut CompactWriter, 829 inputs: &[TxInput], 830 tables: &mut EncodeTables, 831 ) -> Result<()> { 832 writer.varint(inputs.len() as u64); 833 for input in inputs { 834 writer.protocol_id_ref(&input.outpoint.txid, tables)?; 835 writer.varint(u64::from(input.outpoint.index)); 836 } 837 Ok(()) 838 } 839 840 fn decode_outpoints( 841 reader: &mut CompactReader<'_>, 842 tables: &mut DecodeTables, 843 ) -> Result<Vec<OutPoint>> { 844 decode_vec( 845 reader, 846 "transaction input count", 847 MAX_COMPACT_VEC_ITEMS, 848 |reader| { 849 Ok(OutPoint { 850 txid: reader.protocol_id_ref(tables)?, 851 index: reader.u32()?, 852 }) 853 }, 854 ) 855 } 856 857 fn signed_inputs(outpoints: Vec<OutPoint>, owner: &str, signature: &str) -> Vec<TxInput> { 858 outpoints 859 .into_iter() 860 .map(|outpoint| TxInput { 861 outpoint, 862 owner: owner.to_string(), 863 signature: signature.to_string(), 864 }) 865 .collect() 866 } 867 868 fn encode_outputs( 869 writer: &mut CompactWriter, 870 outputs: &[TxOutput], 871 tables: &mut EncodeTables, 872 ) -> Result<()> { 873 writer.varint(outputs.len() as u64); 874 for output in outputs { 875 writer.address(&output.address, tables)?; 876 writer.varint(output.amount); 877 } 878 Ok(()) 879 } 880 881 fn decode_outputs( 882 reader: &mut CompactReader<'_>, 883 tables: &mut DecodeTables, 884 ) -> Result<Vec<TxOutput>> { 885 decode_vec( 886 reader, 887 "transaction output count", 888 MAX_COMPACT_VEC_ITEMS, 889 |reader| { 890 Ok(TxOutput { 891 address: reader.address(tables)?, 892 amount: reader.varint()?, 893 }) 894 }, 895 ) 896 } 897 898 fn decode_vec<T>( 899 reader: &mut CompactReader<'_>, 900 label: &str, 901 max_len: usize, 902 mut decode: impl FnMut(&mut CompactReader<'_>) -> Result<T>, 903 ) -> Result<Vec<T>> { 904 let len = reader.bounded_usize(label, max_len)?; 905 let mut values = Vec::with_capacity(len); 906 for _ in 0..len { 907 values.push(decode(reader)?); 908 } 909 Ok(values) 910 } 911 912 #[derive(Default)] 913 struct CompactWriter { 914 bytes: Vec<u8>, 915 } 916 917 impl CompactWriter { 918 fn into_inner(self) -> Vec<u8> { 919 self.bytes 920 } 921 922 fn bytes(&mut self, bytes: &[u8]) { 923 self.bytes.extend_from_slice(bytes); 924 } 925 926 fn u8(&mut self, value: u8) { 927 self.bytes.push(value); 928 } 929 930 fn bool(&mut self, value: bool) { 931 self.u8(u8::from(value)); 932 } 933 934 fn varint(&mut self, mut value: u64) { 935 while value >= 0x80 { 936 self.u8((value as u8) | 0x80); 937 value >>= 7; 938 } 939 self.u8(value as u8); 940 } 941 942 fn string(&mut self, value: &str) { 943 self.varint(value.len() as u64); 944 self.bytes(value.as_bytes()); 945 } 946 947 fn hex(&mut self, value: &str) -> Result<()> { 948 let bytes = decode_hex(value)?; 949 self.varint(bytes.len() as u64); 950 self.bytes(&bytes); 951 Ok(()) 952 } 953 954 fn fixed_hex<const N: usize>(&mut self, value: &str, label: &str) -> Result<()> { 955 let bytes = decode_hex(value).with_context(|| format!("invalid {label}"))?; 956 if bytes.len() != N { 957 bail!("invalid {label}: expected {N} bytes, got {}", bytes.len()); 958 } 959 self.bytes(&bytes); 960 Ok(()) 961 } 962 963 fn address(&mut self, value: &str, tables: &mut EncodeTables) -> Result<()> { 964 if let Some(index) = tables.addresses.get(value) { 965 self.u8(0); 966 self.varint(*index); 967 } else { 968 if decode_hex(value).is_ok() { 969 self.u8(1); 970 self.fixed_hex::<32>(value, "address")?; 971 } else { 972 self.u8(2); 973 self.string(value); 974 } 975 tables.register_address(value); 976 } 977 Ok(()) 978 } 979 980 fn protocol_id_ref(&mut self, value: &str, tables: &mut EncodeTables) -> Result<()> { 981 if let Some(index) = tables.protocol_ids.get(value) { 982 self.u8(0); 983 self.varint(*index); 984 return Ok(()); 985 } 986 let bytes = decode_hex(value).context("invalid protocol id")?; 987 match bytes.len() { 988 32 => self.u8(1), 989 64 => self.u8(2), 990 length => bail!("invalid protocol id: expected 32 or 64 bytes, got {length}"), 991 } 992 self.bytes(&bytes); 993 tables.register_protocol_id(value); 994 Ok(()) 995 } 996 997 fn compact_vdf_output(&mut self, value: &str) -> Result<()> { 998 if let Some(hex) = value.strip_prefix(VDF_SOLUTION_PREFIX) { 999 self.u8(2); 1000 self.hex(hex)?; 1001 } else if value.len() % 2 == 0 1002 && !value.is_empty() 1003 && value.as_bytes().iter().all(|byte| byte.is_ascii_hexdigit()) 1004 { 1005 self.u8(1); 1006 self.hex(value)?; 1007 } else { 1008 self.u8(0); 1009 self.string(value); 1010 } 1011 Ok(()) 1012 } 1013 } 1014 1015 struct CompactReader<'a> { 1016 bytes: &'a [u8], 1017 offset: usize, 1018 } 1019 1020 impl<'a> CompactReader<'a> { 1021 fn new(bytes: &'a [u8]) -> Self { 1022 Self { bytes, offset: 0 } 1023 } 1024 1025 fn finish(&self) -> Result<()> { 1026 if self.offset != self.bytes.len() { 1027 bail!("compact chain snapshot has trailing bytes"); 1028 } 1029 Ok(()) 1030 } 1031 1032 fn magic(&mut self, magic: &[u8]) -> Result<()> { 1033 let bytes = self.take(magic.len())?; 1034 if bytes != magic { 1035 bail!("invalid compact chain snapshot magic"); 1036 } 1037 Ok(()) 1038 } 1039 1040 fn take(&mut self, len: usize) -> Result<&'a [u8]> { 1041 let end = self 1042 .offset 1043 .checked_add(len) 1044 .context("compact chain snapshot offset overflow")?; 1045 if end > self.bytes.len() { 1046 bail!("unexpected end of compact chain snapshot"); 1047 } 1048 let bytes = &self.bytes[self.offset..end]; 1049 self.offset = end; 1050 Ok(bytes) 1051 } 1052 1053 fn u8(&mut self) -> Result<u8> { 1054 Ok(self.take(1)?[0]) 1055 } 1056 1057 fn bool(&mut self) -> Result<bool> { 1058 match self.u8()? { 1059 0 => Ok(false), 1060 1 => Ok(true), 1061 other => bail!("invalid compact bool tag {other}"), 1062 } 1063 } 1064 1065 fn varint(&mut self) -> Result<u64> { 1066 let mut value = 0_u64; 1067 let mut shift = 0_u32; 1068 loop { 1069 let byte = self.u8()?; 1070 value |= u64::from(byte & 0x7f) 1071 .checked_shl(shift) 1072 .context("compact varint shift overflow")?; 1073 if byte & 0x80 == 0 { 1074 return Ok(value); 1075 } 1076 shift += 7; 1077 if shift >= 64 { 1078 bail!("compact varint is too large"); 1079 } 1080 } 1081 } 1082 1083 fn usize(&mut self) -> Result<usize> { 1084 self.varint()? 1085 .try_into() 1086 .context("compact integer does not fit usize") 1087 } 1088 1089 fn bounded_usize(&mut self, label: &str, max: usize) -> Result<usize> { 1090 let len = self.usize()?; 1091 if len > max { 1092 bail!("compact {label} {len} exceeds limit {max}"); 1093 } 1094 Ok(len) 1095 } 1096 1097 fn u32(&mut self) -> Result<u32> { 1098 self.varint()? 1099 .try_into() 1100 .context("compact integer does not fit u32") 1101 } 1102 1103 fn string(&mut self) -> Result<String> { 1104 let len = self.bounded_usize("string length", MAX_COMPACT_BYTE_FIELD)?; 1105 let bytes = self.take(len)?; 1106 String::from_utf8(bytes.to_vec()).context("compact string is not valid UTF-8") 1107 } 1108 1109 fn hex(&mut self) -> Result<String> { 1110 let len = self.bounded_usize("byte field length", MAX_COMPACT_BYTE_FIELD)?; 1111 Ok(hex_encode(self.take(len)?)) 1112 } 1113 1114 fn fixed_hex<const N: usize>(&mut self) -> Result<String> { 1115 Ok(hex_encode(self.take(N)?)) 1116 } 1117 1118 fn address(&mut self, tables: &mut DecodeTables) -> Result<String> { 1119 match self.u8()? { 1120 0 => { 1121 let index = self.usize()?; 1122 tables 1123 .addresses 1124 .get(index) 1125 .cloned() 1126 .context("compact address reference is out of bounds") 1127 } 1128 1 => { 1129 let value = self.fixed_hex::<32>()?; 1130 if tables.address_indices.contains_key(&value) { 1131 bail!("compact address is encoded twice instead of referenced"); 1132 } 1133 tables.register_address(&value); 1134 Ok(value) 1135 } 1136 2 => { 1137 let value = self.string()?; 1138 let network = if value.starts_with("tiuna1") { 1139 AddressNetwork::Testnet 1140 } else { 1141 AddressNetwork::Mainnet 1142 }; 1143 decode_versioned_address(&value, network) 1144 .context("compact versioned address is invalid")?; 1145 if tables.address_indices.contains_key(&value) { 1146 bail!("compact address is encoded twice instead of referenced"); 1147 } 1148 tables.register_address(&value); 1149 Ok(value) 1150 } 1151 other => bail!("invalid compact address tag {other}"), 1152 } 1153 } 1154 1155 fn protocol_id_ref(&mut self, tables: &mut DecodeTables) -> Result<String> { 1156 match self.u8()? { 1157 0 => { 1158 let index = self.usize()?; 1159 tables 1160 .protocol_ids 1161 .get(index) 1162 .cloned() 1163 .context("compact protocol id reference is out of bounds") 1164 } 1165 tag @ (1 | 2) => { 1166 let value = if tag == 1 { 1167 self.fixed_hex::<32>()? 1168 } else { 1169 self.fixed_hex::<64>()? 1170 }; 1171 if tables.protocol_id_indices.contains_key(&value) { 1172 bail!("compact protocol id is encoded twice instead of referenced"); 1173 } 1174 tables.register_protocol_id(&value); 1175 Ok(value) 1176 } 1177 other => bail!("invalid compact protocol id tag {other}"), 1178 } 1179 } 1180 1181 fn compact_vdf_output(&mut self) -> Result<String> { 1182 match self.u8()? { 1183 0 => self.string(), 1184 1 => self.hex(), 1185 2 => Ok(format!("{VDF_SOLUTION_PREFIX}{}", self.hex()?)), 1186 other => bail!("invalid compact VDF output tag {other}"), 1187 } 1188 } 1189 } 1190 1191 fn decode_hex(input: &str) -> Result<Vec<u8>> { 1192 if input.len() % 2 != 0 { 1193 bail!("hex string has odd length"); 1194 } 1195 let mut bytes = Vec::with_capacity(input.len() / 2); 1196 for pair in input.as_bytes().chunks_exact(2) { 1197 let high = hex_value(pair[0])?; 1198 let low = hex_value(pair[1])?; 1199 bytes.push((high << 4) | low); 1200 } 1201 Ok(bytes) 1202 } 1203 1204 fn hex_value(byte: u8) -> Result<u8> { 1205 match byte { 1206 b'0'..=b'9' => Ok(byte - b'0'), 1207 b'a'..=b'f' => Ok(byte - b'a' + 10), 1208 b'A'..=b'F' => Ok(byte - b'A' + 10), 1209 _ => bail!("invalid hex character"), 1210 } 1211 } 1212 1213 fn hex_encode(bytes: impl AsRef<[u8]>) -> String { 1214 bytes 1215 .as_ref() 1216 .iter() 1217 .map(|byte| format!("{byte:02x}")) 1218 .collect() 1219 } 1220 1221 #[cfg(test)] 1222 mod tests { 1223 use std::{collections::BTreeMap, panic}; 1224 1225 use crate::domain::{ 1226 AddressNetwork, Block, BurnBundleSection, BurnBundleSignature, FinalizerMode, 1227 LaunchProfile, Ledger, MICRO_IUNA, MaskedBurn, MaskedBurnV2, OutPoint, Transaction, 1228 TxInput, TxOutput, Wallet, 1229 }; 1230 1231 use super::{ 1232 COMPACT_SNAPSHOT_MAGIC, COMPACT_SNAPSHOT_VERSION, CompactBlockContext, CompactReader, 1233 CompactWriter, DecodeTables, EncodeTables, MAX_COMPACT_BYTE_FIELD, 1234 MAX_COMPACT_GENESIS_ALLOCATIONS, MAX_COMPACT_SNAPSHOT_BLOCKS, MAX_COMPACT_VEC_ITEMS, 1235 MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION, compact_snapshot_fixed_prefix_size, 1236 compact_varint_size, decode_block_body, decode_compact_snapshot, decode_launch_profile, 1237 decode_transaction, encode_block_body, encode_block_body_with_size_breakdown_for_version, 1238 encode_compact_snapshot, encode_launch_profile, encode_transaction, 1239 }; 1240 1241 #[test] 1242 fn compact_snapshot_v9_roundtrips_default_and_local_profiles() { 1243 let wallet = Wallet::from_seed("compact-profile-wire-version"); 1244 let allocations = BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]); 1245 let default_snapshot = Ledger::new(allocations.clone(), 1).snapshot(); 1246 let default_bytes = encode_compact_snapshot(&default_snapshot).unwrap(); 1247 assert_eq!( 1248 default_bytes[COMPACT_SNAPSHOT_MAGIC.len()], 1249 COMPACT_SNAPSHOT_VERSION 1250 ); 1251 assert_eq!( 1252 decode_compact_snapshot(&default_bytes).unwrap(), 1253 default_snapshot 1254 ); 1255 let default_context = CompactBlockContext::for_chain( 1256 &default_snapshot.genesis_allocations, 1257 &default_snapshot.blocks, 1258 ) 1259 .unwrap(); 1260 let default_block_bytes = default_snapshot 1261 .blocks 1262 .iter() 1263 .map(|block| { 1264 default_context 1265 .stored_block_size_breakdown(&block.hash) 1266 .unwrap() 1267 .total_bytes 1268 }) 1269 .sum::<usize>(); 1270 assert_eq!( 1271 default_bytes.len(), 1272 compact_snapshot_fixed_prefix_size( 1273 &default_snapshot.genesis_allocations, 1274 default_snapshot.vdf_rounds, 1275 &default_snapshot.launch_profile, 1276 ) 1277 .unwrap() 1278 + compact_varint_size(default_snapshot.blocks.len()) 1279 + default_block_bytes 1280 ); 1281 1282 let local_ledger = Ledger::new_with_genesis_burns_and_profile( 1283 allocations, 1284 Vec::new(), 1285 1, 1286 LaunchProfile::local_testnet(), 1287 ) 1288 .unwrap(); 1289 let local_snapshot = local_ledger.snapshot(); 1290 let local_bytes = encode_compact_snapshot(&local_snapshot).unwrap(); 1291 assert_eq!( 1292 local_bytes[COMPACT_SNAPSHOT_MAGIC.len()], 1293 COMPACT_SNAPSHOT_VERSION 1294 ); 1295 assert_eq!( 1296 decode_compact_snapshot(&local_bytes).unwrap(), 1297 local_snapshot 1298 ); 1299 } 1300 1301 #[test] 1302 fn compact_snapshot_v7_remains_readable() { 1303 let wallet = Wallet::from_seed("compact-v7-backward-compatibility"); 1304 let snapshot = Ledger::new( 1305 BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]), 1306 1, 1307 ) 1308 .snapshot(); 1309 let mut writer = CompactWriter::default(); 1310 let mut tables = EncodeTables::default(); 1311 writer.bytes(COMPACT_SNAPSHOT_MAGIC); 1312 writer.u8(7); 1313 writer.varint(snapshot.genesis_allocations.len() as u64); 1314 for (address, amount) in &snapshot.genesis_allocations { 1315 writer.address(address, &mut tables).unwrap(); 1316 writer.varint(*amount); 1317 } 1318 writer.varint(snapshot.vdf_rounds); 1319 encode_launch_profile(&mut writer, &snapshot.launch_profile); 1320 writer.varint(snapshot.blocks.len() as u64); 1321 for block in &snapshot.blocks { 1322 encode_block_body_with_size_breakdown_for_version(&mut writer, block, &mut tables, 7) 1323 .unwrap(); 1324 tables.register_protocol_id(&block.hash); 1325 } 1326 1327 assert_eq!( 1328 decode_compact_snapshot(&writer.into_inner()).unwrap(), 1329 snapshot 1330 ); 1331 } 1332 1333 #[test] 1334 fn compact_launch_profile_roundtrips_local_lineage_maturity() { 1335 let expected = LaunchProfile::local_testnet(); 1336 let mut writer = CompactWriter::default(); 1337 encode_launch_profile(&mut writer, &expected); 1338 let bytes = writer.into_inner(); 1339 let mut reader = CompactReader::new(&bytes); 1340 1341 let decoded = decode_launch_profile(&mut reader).unwrap(); 1342 1343 reader.finish().unwrap(); 1344 assert_eq!(decoded, expected); 1345 } 1346 1347 fn input(owner: &str, signature: &str) -> TxInput { 1348 TxInput { 1349 outpoint: OutPoint { 1350 txid: "1".repeat(128), 1351 index: 0, 1352 }, 1353 owner: owner.to_string(), 1354 signature: signature.to_string(), 1355 } 1356 } 1357 1358 fn assert_transaction_roundtrip(transaction: &Transaction) -> usize { 1359 let mut writer = CompactWriter::default(); 1360 encode_transaction(&mut writer, transaction, &mut EncodeTables::default()).unwrap(); 1361 let bytes = writer.into_inner(); 1362 let mut reader = CompactReader::new(&bytes); 1363 let decoded = decode_transaction(&mut reader, &mut DecodeTables::default()).unwrap(); 1364 reader.finish().unwrap(); 1365 assert_eq!(&decoded, transaction); 1366 bytes.len() 1367 } 1368 1369 #[test] 1370 fn compact_transactions_roundtrip_and_prioritize_burn_storage() { 1371 let owner = "2".repeat(64); 1372 let signature = "3".repeat(128); 1373 let burn = Transaction::Burn { 1374 inputs: vec![input(&owner, &signature)], 1375 change: vec![TxOutput { 1376 address: owner.clone(), 1377 amount: 10, 1378 }], 1379 amount: 20, 1380 fee: 1, 1381 anchor: None, 1382 signature: signature.clone(), 1383 }; 1384 let transfer = Transaction::Transfer { 1385 inputs: vec![input(&owner, &signature)], 1386 outputs: vec![TxOutput { 1387 address: "4".repeat(64), 1388 amount: 10, 1389 }], 1390 fee: 1, 1391 signature, 1392 }; 1393 let mine = Transaction::Mine { 1394 recipient: owner, 1395 anchor: "5".repeat(64), 1396 salt: 1, 1397 nonce: 1, 1398 difficulty_bits: 1, 1399 proof_header: None, 1400 signature: "6".repeat(64), 1401 }; 1402 1403 assert_eq!(assert_transaction_roundtrip(&burn), 169); 1404 assert_eq!(assert_transaction_roundtrip(&transfer), 201); 1405 assert_eq!(assert_transaction_roundtrip(&mine), 103); 1406 } 1407 1408 #[test] 1409 fn compact_transaction_roundtrips_tip_bound_burn() { 1410 let owner = "2".repeat(64); 1411 let signature = "3".repeat(128); 1412 let anchor = "4".repeat(64); 1413 let burn = Transaction::Burn { 1414 inputs: vec![input(&owner, &signature)], 1415 change: Vec::new(), 1416 amount: 20, 1417 fee: 1, 1418 anchor: Some(anchor.clone()), 1419 signature, 1420 }; 1421 let mut encode_tables = EncodeTables::default(); 1422 encode_tables.register_protocol_id(&anchor); 1423 let mut writer = CompactWriter::default(); 1424 encode_transaction(&mut writer, &burn, &mut encode_tables).unwrap(); 1425 let bytes = writer.into_inner(); 1426 let mut decode_tables = DecodeTables::default(); 1427 decode_tables.register_protocol_id(&anchor); 1428 let mut reader = CompactReader::new(&bytes); 1429 1430 let decoded = decode_transaction(&mut reader, &mut decode_tables).unwrap(); 1431 1432 reader.finish().unwrap(); 1433 assert_eq!(decoded, burn); 1434 } 1435 1436 #[test] 1437 fn repeated_burn_references_shrink_to_small_varints() { 1438 let owner = "2".repeat(64); 1439 let signature = "3".repeat(128); 1440 let spent_txid = "1".repeat(128); 1441 let burn = Transaction::Burn { 1442 inputs: vec![input(&owner, &signature)], 1443 change: vec![TxOutput { 1444 address: owner.clone(), 1445 amount: 10, 1446 }], 1447 amount: 20, 1448 fee: 1, 1449 anchor: None, 1450 signature, 1451 }; 1452 let mut tables = EncodeTables::default(); 1453 tables.register_address(&owner); 1454 tables.register_protocol_id(&spent_txid); 1455 let mut writer = CompactWriter::default(); 1456 encode_transaction(&mut writer, &burn, &mut tables).unwrap(); 1457 1458 assert_eq!(writer.into_inner().len(), 75); 1459 } 1460 1461 #[test] 1462 fn burn_bundle_section_stores_transaction_index_instead_of_burn_copy() { 1463 let owner = "2".repeat(64); 1464 let signature = "3".repeat(128); 1465 let burn = Transaction::Burn { 1466 inputs: vec![input(&owner, &signature)], 1467 change: Vec::new(), 1468 amount: 20, 1469 fee: 1, 1470 anchor: None, 1471 signature, 1472 }; 1473 let block_with_section = |burn_bundle_section| { 1474 let mut block = Block { 1475 height: 0, 1476 prev_hash: "0".repeat(64), 1477 timestamp_ms: 1, 1478 miner: owner.clone(), 1479 reward_address: None, 1480 reward_address_signature: None, 1481 finalizer_mode: FinalizerMode::Ticket, 1482 finalizer_rank: 0, 1483 reward: 0, 1484 vdf_rounds: 1, 1485 vdf_output: "vdf".to_string(), 1486 leader_proof: None, 1487 burn_bundle_section, 1488 transactions: vec![burn.clone()], 1489 transactions_v2: Vec::new(), 1490 hash: String::new(), 1491 }; 1492 block.hash = block.compute_hash(); 1493 block 1494 }; 1495 let without = block_with_section(BurnBundleSection::default()); 1496 let with = block_with_section(BurnBundleSection { 1497 signatures: Vec::new(), 1498 burns: vec![MaskedBurn { 1499 burn: burn.clone(), 1500 bundle_mask: 0b10, 1501 }], 1502 burns_v2: Vec::new(), 1503 }); 1504 let encode = |block: &Block| { 1505 let mut writer = CompactWriter::default(); 1506 encode_block_body(&mut writer, block, &mut EncodeTables::default()).unwrap(); 1507 writer.into_inner() 1508 }; 1509 let without_bytes = encode(&without); 1510 let with_bytes = encode(&with); 1511 assert_eq!(with_bytes.len() - without_bytes.len(), 2); 1512 1513 let mut context = CompactBlockContext::default(); 1514 let breakdown = context.append_block_with_size_breakdown(&with).unwrap(); 1515 assert_eq!(breakdown.total_bytes, with_bytes.len()); 1516 assert_eq!(breakdown.burn_bundle_bytes, 5); 1517 assert!(breakdown.burn_bytes > 0); 1518 assert_eq!(breakdown.transfer_bytes, 0); 1519 assert_eq!(breakdown.mine_bytes, 0); 1520 assert_eq!( 1521 breakdown.total_bytes, 1522 breakdown.header_and_proof_bytes 1523 + breakdown.transaction_bytes 1524 + breakdown.burn_bundle_bytes 1525 ); 1526 assert_eq!( 1527 context.stored_block_size_breakdown(&with.hash), 1528 Some(&breakdown) 1529 ); 1530 1531 let mut reader = CompactReader::new(&with_bytes); 1532 let decoded = decode_block_body( 1533 &mut reader, 1534 with.height, 1535 with.prev_hash.clone(), 1536 &mut DecodeTables::default(), 1537 ) 1538 .unwrap(); 1539 reader.finish().unwrap(); 1540 assert_eq!(decoded, with); 1541 } 1542 1543 #[test] 1544 fn compact_block_roundtrips_transaction_v2_envelopes() { 1545 let wallet = Wallet::from_seed("compact-v2-envelope"); 1546 let mut block = Ledger::new( 1547 BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]), 1548 1, 1549 ) 1550 .snapshot() 1551 .blocks[0] 1552 .clone(); 1553 block.transactions_v2 = vec!["000102ff".to_string()]; 1554 block.hash = block.compute_hash(); 1555 let mut writer = CompactWriter::default(); 1556 encode_block_body(&mut writer, &block, &mut EncodeTables::default()).unwrap(); 1557 let bytes = writer.into_inner(); 1558 let mut reader = CompactReader::new(&bytes); 1559 1560 let decoded = decode_block_body( 1561 &mut reader, 1562 block.height, 1563 block.prev_hash.clone(), 1564 &mut DecodeTables::default(), 1565 ) 1566 .unwrap(); 1567 1568 reader.finish().unwrap(); 1569 assert_eq!(decoded, block); 1570 } 1571 1572 #[test] 1573 fn compact_v9_roundtrips_hybrid_reward_addresses() { 1574 let wallet = Wallet::from_seed("compact-hybrid-reward"); 1575 let mut block = Ledger::new( 1576 BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]), 1577 1, 1578 ) 1579 .snapshot() 1580 .blocks[0] 1581 .clone(); 1582 block.reward_address = Some(wallet.hybrid_address(AddressNetwork::Mainnet)); 1583 block.burn_bundle_section.signatures = vec![BurnBundleSignature { 1584 slot: 1, 1585 member: wallet.address().to_string(), 1586 reward_address: Some(wallet.hybrid_address(AddressNetwork::Mainnet)), 1587 signature: "11".repeat(64), 1588 }]; 1589 block.transactions_v2 = vec!["aa".to_string()]; 1590 block.burn_bundle_section.burns_v2 = vec![MaskedBurnV2 { 1591 envelope: "aa".to_string(), 1592 bundle_mask: 1 << 1, 1593 }]; 1594 block.hash = block.compute_hash(); 1595 let mut writer = CompactWriter::default(); 1596 encode_block_body(&mut writer, &block, &mut EncodeTables::default()).unwrap(); 1597 let bytes = writer.into_inner(); 1598 let mut reader = CompactReader::new(&bytes); 1599 1600 let decoded = decode_block_body( 1601 &mut reader, 1602 block.height, 1603 block.prev_hash.clone(), 1604 &mut DecodeTables::default(), 1605 ) 1606 .unwrap(); 1607 1608 reader.finish().unwrap(); 1609 assert_eq!(decoded, block); 1610 } 1611 1612 #[test] 1613 fn legacy_only_consensus_block_size_keeps_v7_framing() { 1614 let wallet = Wallet::from_seed("compact-legacy-consensus-size"); 1615 let block = Ledger::new( 1616 BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]), 1617 1, 1618 ) 1619 .snapshot() 1620 .blocks[0] 1621 .clone(); 1622 let context = CompactBlockContext::default(); 1623 let mut writer = CompactWriter::default(); 1624 encode_block_body_with_size_breakdown_for_version( 1625 &mut writer, 1626 &block, 1627 &mut EncodeTables::default(), 1628 7, 1629 ) 1630 .unwrap(); 1631 1632 assert_eq!( 1633 context.block_size_bytes(&block).unwrap(), 1634 writer.bytes.len() 1635 ); 1636 } 1637 1638 fn snapshot_prefix(block_count: u64) -> Vec<u8> { 1639 let mut writer = CompactWriter::default(); 1640 writer.bytes(COMPACT_SNAPSHOT_MAGIC); 1641 writer.u8(COMPACT_SNAPSHOT_VERSION); 1642 writer.varint(0); 1643 writer.varint(1); 1644 encode_launch_profile(&mut writer, &LaunchProfile::default()); 1645 writer.varint(block_count); 1646 writer.into_inner() 1647 } 1648 1649 fn empty_snapshot_bytes() -> Vec<u8> { 1650 snapshot_prefix(0) 1651 } 1652 1653 fn block_body_prefix(writer: &mut CompactWriter) { 1654 writer.varint(1); 1655 writer.u8(1); 1656 writer 1657 .fixed_hex::<32>(&"0".repeat(64), "test miner") 1658 .unwrap(); 1659 writer.bool(false); 1660 writer.bool(false); 1661 } 1662 1663 fn block_body_through_leader_proof_flag(writer: &mut CompactWriter) { 1664 block_body_prefix(writer); 1665 writer.u8(0); 1666 writer.varint(0); 1667 writer.varint(0); 1668 writer.varint(0); 1669 writer.u8(0); 1670 writer.string("0:0"); 1671 } 1672 1673 fn block_body_through_transaction_count(writer: &mut CompactWriter, tx_count: u64) { 1674 block_body_through_leader_proof_flag(writer); 1675 writer.bool(false); 1676 writer.varint(tx_count); 1677 } 1678 1679 fn assert_decode_error_contains(bytes: &[u8], expected: &str) { 1680 let error = decode_compact_snapshot(bytes).unwrap_err().to_string(); 1681 assert!( 1682 error.contains(expected), 1683 "expected error containing {expected:?}, got {error:?}" 1684 ); 1685 } 1686 1687 #[test] 1688 fn compact_snapshot_decoder_rejects_huge_lengths_before_allocation() { 1689 let mut huge_genesis = Vec::new(); 1690 let mut writer = CompactWriter::default(); 1691 writer.bytes(COMPACT_SNAPSHOT_MAGIC); 1692 writer.u8(COMPACT_SNAPSHOT_VERSION); 1693 writer.varint(MAX_COMPACT_GENESIS_ALLOCATIONS as u64 + 1); 1694 huge_genesis.extend(writer.into_inner()); 1695 assert_decode_error_contains(&huge_genesis, "genesis allocation count"); 1696 1697 let huge_blocks = snapshot_prefix(MAX_COMPACT_SNAPSHOT_BLOCKS as u64 + 1); 1698 assert_decode_error_contains(&huge_blocks, "block count"); 1699 1700 let mut huge_transactions = snapshot_prefix(1); 1701 let mut writer = CompactWriter::default(); 1702 block_body_through_transaction_count(&mut writer, MAX_COMPACT_VEC_ITEMS as u64 + 1); 1703 huge_transactions.extend(writer.into_inner()); 1704 assert_decode_error_contains(&huge_transactions, "block transaction count"); 1705 1706 let mut huge_string = snapshot_prefix(1); 1707 let mut writer = CompactWriter::default(); 1708 block_body_prefix(&mut writer); 1709 writer.u8(0); 1710 writer.varint(0); 1711 writer.varint(0); 1712 writer.varint(0); 1713 writer.u8(0); 1714 writer.varint(MAX_COMPACT_BYTE_FIELD as u64 + 1); 1715 huge_string.extend(writer.into_inner()); 1716 assert_decode_error_contains(&huge_string, "string length"); 1717 } 1718 1719 #[test] 1720 fn compact_snapshot_decoder_rejects_oversized_varints() { 1721 let mut bytes = Vec::new(); 1722 bytes.extend_from_slice(COMPACT_SNAPSHOT_MAGIC); 1723 bytes.push(COMPACT_SNAPSHOT_VERSION); 1724 bytes.extend_from_slice(&[0xff; 10]); 1725 1726 assert_decode_error_contains(&bytes, "compact varint is too large"); 1727 } 1728 1729 #[test] 1730 fn compact_snapshot_decoder_rejects_pre_reset_versions() { 1731 let mut bytes = Vec::new(); 1732 bytes.extend_from_slice(COMPACT_SNAPSHOT_MAGIC); 1733 bytes.push(MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION - 1); 1734 1735 assert_decode_error_contains(&bytes, "unsupported compact chain snapshot version 5"); 1736 } 1737 1738 #[test] 1739 fn compact_snapshot_decoder_rejects_trailing_bytes() { 1740 let mut bytes = empty_snapshot_bytes(); 1741 bytes.push(0); 1742 1743 assert_decode_error_contains(&bytes, "trailing bytes"); 1744 } 1745 1746 #[test] 1747 fn compact_snapshot_decoder_rejects_truncated_payloads() { 1748 let bytes = empty_snapshot_bytes(); 1749 for len in 0..bytes.len() { 1750 assert!( 1751 decode_compact_snapshot(&bytes[..len]).is_err(), 1752 "truncated compact snapshot of length {len} decoded successfully" 1753 ); 1754 } 1755 } 1756 1757 #[test] 1758 fn compact_snapshot_decoder_rejects_invalid_tags() { 1759 let mut invalid_finalizer_mode = snapshot_prefix(1); 1760 let mut writer = CompactWriter::default(); 1761 block_body_prefix(&mut writer); 1762 writer.u8(9); 1763 invalid_finalizer_mode.extend(writer.into_inner()); 1764 assert_decode_error_contains(&invalid_finalizer_mode, "invalid finalizer mode tag 9"); 1765 1766 let mut invalid_bool = snapshot_prefix(1); 1767 let mut writer = CompactWriter::default(); 1768 block_body_through_leader_proof_flag(&mut writer); 1769 writer.u8(2); 1770 invalid_bool.extend(writer.into_inner()); 1771 assert_decode_error_contains(&invalid_bool, "invalid compact bool tag 2"); 1772 1773 let mut invalid_transaction = snapshot_prefix(1); 1774 let mut writer = CompactWriter::default(); 1775 block_body_through_transaction_count(&mut writer, 1); 1776 writer.u8(99); 1777 invalid_transaction.extend(writer.into_inner()); 1778 assert_decode_error_contains(&invalid_transaction, "invalid transaction tag 99"); 1779 } 1780 1781 #[test] 1782 fn compact_snapshot_decoder_random_bytes_do_not_panic() { 1783 let mut state = 0x5eed_5eed_1234_5678_u64; 1784 for len in 0..256 { 1785 let mut bytes = Vec::with_capacity(len); 1786 for _ in 0..len { 1787 state = state.wrapping_mul(6364136223846793005).wrapping_add(1); 1788 bytes.push((state >> 32) as u8); 1789 } 1790 1791 let result = panic::catch_unwind(|| { 1792 let _ = decode_compact_snapshot(&bytes); 1793 }); 1794 assert!(result.is_ok(), "decoder panicked for random length {len}"); 1795 } 1796 } 1797 }