iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 &section.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 &section.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 &section.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 }