Skip to main content

pepper_sync/wallet/
serialization.rs

1//! Serialization and de-serialization of wallet structs in [`crate::wallet`] including utilities.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    io::{Read, Write},
6    ops::Range,
7};
8
9use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
10
11use incrementalmerkletree::{Hashable, Position};
12use shardtree::{
13    LocatedPrunableTree, ShardTree,
14    store::{Checkpoint, ShardStore, TreeState, memory::MemoryShardStore},
15};
16use zcash_client_backend::serialization::shardtree::{read_shard, write_shard};
17use zcash_encoding::{Optional, Vector};
18use zcash_primitives::{
19    block::BlockHash,
20    merkle_tree::HashSer,
21    transaction::{Transaction, TxId},
22};
23use zcash_protocol::{
24    consensus::{self, BlockHeight},
25    memo::Memo,
26    value::Zatoshis,
27};
28use zcash_transparent::address::Script;
29
30use zcash_transparent::keys::NonHardenedChildIndex;
31use zingo_status::confirmation_status::ConfirmationStatus;
32
33use crate::{
34    keys::{
35        KeyId, decode_unified_address,
36        transparent::{TransparentAddressId, TransparentScope},
37    },
38    sync::{SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE, ScanPriority, ScanRange},
39    wallet::{ScanTarget, ScanTargetOrigin, ScanTargetPools},
40};
41
42use super::{
43    InitialSyncState, IronwoodNote, KeyIdInterface, NullifierMap, OrchardNote,
44    OutgoingIronwoodNote, OutgoingNote, OutgoingNoteInterface, OutgoingOrchardNote,
45    OutgoingSaplingNote, OutputId, OutputInterface, SaplingNote, ShardTrees, SyncState,
46    TransparentCoin, TreeBounds, WalletBlock, WalletNote, WalletTransaction,
47};
48
49fn read_string<R: Read>(mut reader: R) -> std::io::Result<String> {
50    let str_len = reader.read_u64::<LittleEndian>()?;
51    let mut str_bytes = vec![0; str_len as usize];
52    reader.read_exact(&mut str_bytes)?;
53
54    String::from_utf8(str_bytes)
55        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
56}
57
58fn write_string<W: Write>(mut writer: W, str: &str) -> std::io::Result<()> {
59    writer.write_u64::<LittleEndian>(str.len() as u64)?;
60    writer.write_all(str.as_bytes())
61}
62
63/// Reads the serialized version of the type named `type_name`.
64///
65/// A version above `current_version`, the version this build writes, was written by a newer build in a layout this
66/// build cannot read. It is refused so the newer layout is never read as an older one.
67pub(crate) fn read_version<R: Read>(
68    mut reader: R,
69    type_name: &str,
70    current_version: u8,
71) -> std::io::Result<u8> {
72    let version = reader.read_u8()?;
73    if version > current_version {
74        return Err(std::io::Error::new(
75            std::io::ErrorKind::InvalidData,
76            format!(
77                "{type_name} serialized version {version} was written by a newer build. this build reads up to \
78                 version {current_version}."
79            ),
80        ));
81    }
82
83    Ok(version)
84}
85
86impl ScanTarget {
87    fn serialized_version() -> u8 {
88        // Version 1 makes the txid optional, and replaces the narrow scan area flag with the pools the target's
89        // transaction holds notes of and the target's origin.
90        1
91    }
92
93    const INTERNAL_ORIGIN: u8 = 0;
94    const EXTERNAL_ORIGIN: u8 = 1;
95
96    /// Deserialize into `reader`
97    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
98        let version = read_version(&mut reader, "ScanTarget", Self::serialized_version())?;
99        let block_height = BlockHeight::from_u32(reader.read_u32::<LittleEndian>()?);
100        if version == 0 {
101            // a version 0 target without a particular transaction carried a zero txid, and its narrow scan area
102            // flag is dropped, as the narrow area is now the default. the origin is not recorded, and a version 0
103            // target is taken as external, so a transaction the server does not serve drops it.
104            let txid = TxId::read(&mut reader)?;
105            let _narrow_scan_area = reader.read_u8()?;
106            return Ok(Self::new(
107                block_height,
108                (txid != TxId::from_bytes([0; 32])).then_some(txid),
109            ));
110        }
111        let txid = Optional::read(&mut reader, TxId::read)?;
112        let pools = ScanTargetPools::from_bits(reader.read_u8()?).ok_or_else(|| {
113            std::io::Error::new(
114                std::io::ErrorKind::InvalidData,
115                "scan target names a shielded pool this version does not know",
116            )
117        })?;
118        let origin = match reader.read_u8()? {
119            Self::INTERNAL_ORIGIN => ScanTargetOrigin::Internal,
120            Self::EXTERNAL_ORIGIN => ScanTargetOrigin::External,
121            unknown => {
122                return Err(std::io::Error::new(
123                    std::io::ErrorKind::InvalidData,
124                    format!("unknown scan target origin {unknown}"),
125                ));
126            }
127        };
128
129        Ok(Self {
130            block_height,
131            txid,
132            pools,
133            origin,
134        })
135    }
136
137    /// Serialize into `writer`
138    pub fn write<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
139        writer.write_u8(Self::serialized_version())?;
140        writer.write_u32::<LittleEndian>(self.block_height().into())?;
141        Optional::write(&mut *writer, self.txid(), |w, txid| txid.write(w))?;
142        writer.write_u8(self.pools().bits())?;
143        writer.write_u8(match self.origin() {
144            ScanTargetOrigin::Internal => Self::INTERNAL_ORIGIN,
145            ScanTargetOrigin::External => Self::EXTERNAL_ORIGIN,
146        })
147    }
148}
149
150impl SyncState {
151    fn serialized_version() -> u8 {
152        // Version 4 inserts the ironwood shard ranges after the orchard ones.
153        // Version 5 appends the transparent scan floor.
154        5
155    }
156
157    /// Deserialize into `reader`
158    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
159        let version = read_version(&mut reader, "SyncState", Self::serialized_version())?;
160        let scan_ranges = Vector::read(&mut reader, |r| {
161            let start = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
162            let end = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
163            let priority = match version {
164                3.. => match r.read_u8()? {
165                    0 => Ok(ScanPriority::RefetchingNullifiers),
166                    1 => Ok(ScanPriority::Scanning),
167                    2 => Ok(ScanPriority::Scanned),
168                    3 => Ok(ScanPriority::ScannedWithoutMapping),
169                    4 => Ok(ScanPriority::Historic),
170                    5 => Ok(ScanPriority::OpenAdjacent),
171                    6 => Ok(ScanPriority::FoundNote),
172                    7 => Ok(ScanPriority::ChainTip),
173                    8 => Ok(ScanPriority::Verify),
174                    _ => Err(std::io::Error::new(
175                        std::io::ErrorKind::InvalidData,
176                        "invalid scan priority",
177                    )),
178                }?,
179                2 => match r.read_u8()? {
180                    0 => Ok(ScanPriority::Scanning),
181                    1 => Ok(ScanPriority::Scanned),
182                    2 => Ok(ScanPriority::ScannedWithoutMapping),
183                    3 => Ok(ScanPriority::Historic),
184                    4 => Ok(ScanPriority::OpenAdjacent),
185                    5 => Ok(ScanPriority::FoundNote),
186                    6 => Ok(ScanPriority::ChainTip),
187                    7 => Ok(ScanPriority::Verify),
188                    _ => Err(std::io::Error::new(
189                        std::io::ErrorKind::InvalidData,
190                        "invalid scan priority",
191                    )),
192                }?,
193                0 | 1 => match r.read_u8()? {
194                    0 => Ok(ScanPriority::Scanning),
195                    1 => Ok(ScanPriority::Scanned),
196                    2 => Ok(ScanPriority::Historic),
197                    3 => Ok(ScanPriority::OpenAdjacent),
198                    4 => Ok(ScanPriority::FoundNote),
199                    5 => Ok(ScanPriority::ChainTip),
200                    6 => Ok(ScanPriority::Verify),
201                    _ => Err(std::io::Error::new(
202                        std::io::ErrorKind::InvalidData,
203                        "invalid scan priority",
204                    )),
205                }?,
206            };
207
208            Ok(ScanRange::from_parts(start..end, priority))
209        })?;
210        let sapling_shard_ranges = Vector::read(&mut reader, |r| {
211            let start = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
212            let end = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
213
214            Ok(start..end)
215        })?;
216        let orchard_shard_ranges = Vector::read(&mut reader, |r| {
217            let start = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
218            let end = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
219
220            Ok(start..end)
221        })?;
222        let ironwood_shard_ranges = if version >= 4 {
223            Vector::read(&mut reader, |r| {
224                let start = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
225                let end = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
226
227                Ok(start..end)
228            })?
229        } else {
230            Vec::new()
231        };
232        let scan_targets = Vector::read(&mut reader, |r| {
233            Ok(if version >= 1 {
234                ScanTarget::read(r)?
235            } else {
236                let block_height = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
237                let txid = TxId::read(r)?;
238
239                ScanTarget::new(block_height, Some(txid))
240            })
241        })?
242        .into_iter()
243        .collect::<BTreeSet<_>>();
244        let transparent_scan_floor = if version >= 5 {
245            Optional::read(&mut reader, |r| {
246                Ok(BlockHeight::from_u32(r.read_u32::<LittleEndian>()?))
247            })?
248        } else {
249            None
250        };
251
252        Ok(Self {
253            scan_ranges,
254            sapling_shard_ranges,
255            orchard_shard_ranges,
256            ironwood_shard_ranges,
257            scan_targets,
258            initial_sync_state: InitialSyncState::new(),
259            transparent_scan_floor,
260        })
261    }
262
263    /// Serialize into `writer`
264    pub fn write<W: Write>(&mut self, mut writer: W) -> std::io::Result<()> {
265        writer.write_u8(Self::serialized_version())?;
266        Vector::write(&mut writer, self.scan_ranges(), |w, scan_range| {
267            w.write_u32::<LittleEndian>(scan_range.block_range().start.into())?;
268            w.write_u32::<LittleEndian>(scan_range.block_range().end.into())?;
269            w.write_u8(scan_range.priority() as u8)
270        })?;
271        Vector::write(&mut writer, &self.sapling_shard_ranges, |w, shard_range| {
272            w.write_u32::<LittleEndian>(shard_range.start.into())?;
273            w.write_u32::<LittleEndian>(shard_range.end.into())
274        })?;
275        Vector::write(&mut writer, &self.orchard_shard_ranges, |w, shard_range| {
276            w.write_u32::<LittleEndian>(shard_range.start.into())?;
277            w.write_u32::<LittleEndian>(shard_range.end.into())
278        })?;
279        Vector::write(
280            &mut writer,
281            &self.ironwood_shard_ranges,
282            |w, shard_range| {
283                w.write_u32::<LittleEndian>(shard_range.start.into())?;
284                w.write_u32::<LittleEndian>(shard_range.end.into())
285            },
286        )?;
287        Vector::write(
288            &mut writer,
289            &self.scan_targets.iter().collect::<Vec<_>>(),
290            |w, &scan_target| scan_target.write(w),
291        )?;
292        Optional::write(&mut writer, self.transparent_scan_floor, |w, floor| {
293            w.write_u32::<LittleEndian>(floor.into())
294        })
295    }
296}
297
298impl TreeBounds {
299    fn serialized_version() -> u8 {
300        // Version 1 appends the ironwood tree sizes.
301        1
302    }
303
304    /// Deserialize into `reader`
305    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
306        let version = read_version(&mut reader, "TreeBounds", Self::serialized_version())?;
307        let sapling_initial_tree_size = reader.read_u32::<LittleEndian>()?;
308        let sapling_final_tree_size = reader.read_u32::<LittleEndian>()?;
309        let orchard_initial_tree_size = reader.read_u32::<LittleEndian>()?;
310        let orchard_final_tree_size = reader.read_u32::<LittleEndian>()?;
311        let (ironwood_initial_tree_size, ironwood_final_tree_size) = if version >= 1 {
312            (
313                reader.read_u32::<LittleEndian>()?,
314                reader.read_u32::<LittleEndian>()?,
315            )
316        } else {
317            (0, 0)
318        };
319
320        Ok(Self {
321            sapling_initial_tree_size,
322            sapling_final_tree_size,
323            orchard_initial_tree_size,
324            orchard_final_tree_size,
325            ironwood_initial_tree_size,
326            ironwood_final_tree_size,
327        })
328    }
329
330    /// Serialize into `writer`
331    pub fn write<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
332        writer.write_u8(Self::serialized_version())?;
333        writer.write_u32::<LittleEndian>(self.sapling_initial_tree_size)?;
334        writer.write_u32::<LittleEndian>(self.sapling_final_tree_size)?;
335        writer.write_u32::<LittleEndian>(self.orchard_initial_tree_size)?;
336        writer.write_u32::<LittleEndian>(self.orchard_final_tree_size)?;
337        writer.write_u32::<LittleEndian>(self.ironwood_initial_tree_size)?;
338        writer.write_u32::<LittleEndian>(self.ironwood_final_tree_size)
339    }
340}
341
342impl NullifierMap {
343    fn serialized_version() -> u8 {
344        // Version 2 appends the ironwood nullifier map.
345        2
346    }
347
348    /// Deserialize into `reader`
349    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
350        let version = read_version(&mut reader, "NullifierMap", Self::serialized_version())?;
351        let sapling = Vector::read(&mut reader, |r| {
352            let mut nullifier_bytes = [0u8; 32];
353            r.read_exact(&mut nullifier_bytes)?;
354            let nullifier =
355                sapling_crypto::Nullifier::from_slice(&nullifier_bytes).map_err(|e| {
356                    std::io::Error::new(
357                        std::io::ErrorKind::InvalidData,
358                        format!("failed to read nullifier. {e}"),
359                    )
360                })?;
361            let scan_target = if version >= 1 {
362                ScanTarget::read(r)?
363            } else {
364                let block_height = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
365                let txid = TxId::read(r)?;
366
367                ScanTarget::internal(block_height, txid)
368            };
369
370            Ok((nullifier, scan_target))
371        })?
372        .into_iter()
373        .collect::<BTreeMap<_, _>>();
374
375        let orchard = Vector::read(&mut reader, |r| {
376            let mut nullifier_bytes = [0u8; 32];
377            r.read_exact(&mut nullifier_bytes)?;
378            let nullifier = orchard::note::Nullifier::from_bytes(&nullifier_bytes)
379                .expect("nullifier bytes should be valid");
380            let scan_target = if version >= 1 {
381                ScanTarget::read(r)?
382            } else {
383                let block_height = BlockHeight::from_u32(r.read_u32::<LittleEndian>()?);
384                let txid = TxId::read(r)?;
385
386                ScanTarget::internal(block_height, txid)
387            };
388
389            Ok((nullifier, scan_target))
390        })?
391        .into_iter()
392        .collect::<BTreeMap<_, _>>();
393
394        let ironwood = if version >= 2 {
395            Vector::read(&mut reader, |r| {
396                let mut nullifier_bytes = [0u8; 32];
397                r.read_exact(&mut nullifier_bytes)?;
398                let nullifier = orchard::note::Nullifier::from_bytes(&nullifier_bytes)
399                    .expect("nullifier bytes should be valid");
400                let scan_target = ScanTarget::read(r)?;
401
402                Ok((nullifier, scan_target))
403            })?
404            .into_iter()
405            .collect::<BTreeMap<_, _>>()
406        } else {
407            BTreeMap::new()
408        };
409
410        Ok(NullifierMap {
411            sapling,
412            orchard,
413            ironwood,
414        })
415    }
416
417    /// Serialize into `writer`
418    pub fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
419        writer.write_u8(Self::serialized_version())?;
420        Vector::write(
421            &mut writer,
422            &self.sapling.iter().collect::<Vec<_>>(),
423            |w, &(&nullifier, &scan_target)| {
424                w.write_all(nullifier.as_ref())?;
425                scan_target.write(w)
426            },
427        )?;
428        Vector::write(
429            &mut writer,
430            &self.orchard.iter().collect::<Vec<_>>(),
431            |w, &(&nullifier, &scan_target)| {
432                w.write_all(&nullifier.to_bytes())?;
433                scan_target.write(w)
434            },
435        )?;
436        Vector::write(
437            &mut writer,
438            &self.ironwood.iter().collect::<Vec<_>>(),
439            |w, &(&nullifier, &scan_target)| {
440                w.write_all(&nullifier.to_bytes())?;
441                scan_target.write(w)
442            },
443        )
444    }
445}
446
447impl WalletBlock {
448    fn serialized_version() -> u8 {
449        0
450    }
451
452    /// Deserialize into `reader`
453    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
454        read_version(&mut reader, "WalletBlock", Self::serialized_version())?;
455        let block_height = BlockHeight::from_u32(reader.read_u32::<LittleEndian>()?);
456        let mut block_hash = BlockHash([0u8; 32]);
457        reader.read_exact(&mut block_hash.0)?;
458        let mut prev_hash = BlockHash([0u8; 32]);
459        reader.read_exact(&mut prev_hash.0)?;
460        let time = reader.read_u32::<LittleEndian>()?;
461        let txids = Vector::read(&mut reader, |r| TxId::read(r))?;
462        let tree_bounds = TreeBounds::read(&mut reader)?;
463
464        Ok(Self {
465            block_height,
466            block_hash,
467            prev_hash,
468            time,
469            txids,
470            tree_bounds,
471        })
472    }
473
474    /// Serialize into `writer`
475    pub fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
476        writer.write_u8(Self::serialized_version())?;
477        writer.write_u32::<LittleEndian>(self.block_height.into())?;
478        writer.write_all(&self.block_hash.0)?;
479        writer.write_all(&self.prev_hash.0)?;
480        writer.write_u32::<LittleEndian>(self.time)?;
481        Vector::write(&mut writer, self.txids(), |w, txid| txid.write(w))?;
482        self.tree_bounds.write(&mut writer)
483    }
484}
485
486impl WalletTransaction {
487    fn serialized_version() -> u8 {
488        // Version 1 appends the ironwood note collections.
489        1
490    }
491
492    /// Deserialize into `reader`
493    pub fn read<R: Read>(
494        mut reader: R,
495        consensus_parameters: &impl consensus::Parameters,
496    ) -> std::io::Result<Self> {
497        let version = read_version(&mut reader, "WalletTransaction", Self::serialized_version())?;
498        let txid = TxId::read(&mut reader)?;
499        let status = ConfirmationStatus::read(&mut reader)?;
500        let transaction = Transaction::read(
501            &mut reader,
502            consensus::BranchId::for_height(consensus_parameters, status.get_height()),
503        )?;
504        let datetime = reader.read_u32::<LittleEndian>()?;
505        let transparent_coins = Vector::read(&mut reader, |r| TransparentCoin::read(r))?;
506        let sapling_notes = Vector::read(&mut reader, |r| SaplingNote::read(r))?;
507        let orchard_notes = Vector::read(&mut reader, |r| OrchardNote::read(r))?;
508        let outgoing_sapling_notes = Vector::read(&mut reader, |r| {
509            OutgoingSaplingNote::read(r, consensus_parameters)
510        })?;
511        let outgoing_orchard_notes = Vector::read(&mut reader, |r| {
512            OutgoingOrchardNote::read(r, consensus_parameters)
513        })?;
514        let (ironwood_notes, outgoing_ironwood_notes) = if version >= 1 {
515            (
516                Vector::read(&mut reader, |r| IronwoodNote::read(r))?,
517                Vector::read(&mut reader, |r| {
518                    OutgoingIronwoodNote::read(r, consensus_parameters)
519                })?,
520            )
521        } else {
522            (Vec::new(), Vec::new())
523        };
524
525        Ok(Self {
526            txid,
527            status,
528            transaction,
529            datetime,
530            transparent_coins,
531            sapling_notes,
532            orchard_notes,
533            ironwood_notes,
534            outgoing_sapling_notes,
535            outgoing_orchard_notes,
536            outgoing_ironwood_notes,
537        })
538    }
539
540    /// Serialize into `writer`
541    pub fn write<W: Write>(
542        &self,
543        mut writer: W,
544        consensus_parameters: &impl consensus::Parameters,
545    ) -> std::io::Result<()> {
546        writer.write_u8(Self::serialized_version())?;
547        self.txid.write(&mut writer)?;
548        self.status.write(&mut writer)?;
549        self.transaction.write(&mut writer)?;
550        writer.write_u32::<LittleEndian>(self.datetime)?;
551        Vector::write(&mut writer, self.transparent_coins(), |w, output| {
552            output.write(w)
553        })?;
554        Vector::write(&mut writer, self.sapling_notes(), |w, output| {
555            output.write(w)
556        })?;
557        Vector::write(&mut writer, self.orchard_notes(), |w, output| {
558            output.write(w)
559        })?;
560        Vector::write(&mut writer, self.outgoing_sapling_notes(), |w, output| {
561            output.write(w, consensus_parameters)
562        })?;
563        Vector::write(&mut writer, self.outgoing_orchard_notes(), |w, output| {
564            output.write(w, consensus_parameters)
565        })?;
566        Vector::write(&mut writer, self.ironwood_notes(), |w, output| {
567            output.write(w)
568        })?;
569        Vector::write(&mut writer, self.outgoing_ironwood_notes(), |w, output| {
570            output.write(w, consensus_parameters)
571        })
572    }
573}
574
575impl TransparentCoin {
576    fn serialized_version() -> u8 {
577        1
578    }
579
580    /// Deserialize into `reader`
581    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
582        let version = read_version(&mut reader, "TransparentCoin", Self::serialized_version())?;
583
584        let txid = TxId::read(&mut reader)?;
585        let output_index = if version >= 1 {
586            reader.read_u32::<LittleEndian>()?
587        } else {
588            u32::from(reader.read_u16::<LittleEndian>()?)
589        };
590
591        let account_id = zip32::AccountId::try_from(reader.read_u32::<LittleEndian>()?)
592            .expect("only valid account ids written");
593        let scope = TransparentScope::try_from(reader.read_u8()?)?;
594        let address_index = reader.read_u32::<LittleEndian>()?;
595
596        let address = read_string(&mut reader)?;
597        let script = Script::read(&mut reader)?;
598        let value = Zatoshis::from_u64(reader.read_u64::<LittleEndian>()?)
599            .expect("only valid values written");
600        let spending_transaction = Optional::read(&mut reader, TxId::read)?;
601
602        Ok(Self {
603            output_id: OutputId { txid, output_index },
604            key_id: TransparentAddressId::new(
605                account_id,
606                scope,
607                NonHardenedChildIndex::from_index(address_index)
608                    .expect("only non-hardened child indexes should be written"),
609            ),
610            address,
611            value,
612            script,
613            spending_transaction,
614        })
615    }
616
617    /// Serialize into `writer`
618    pub fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
619        writer.write_u8(Self::serialized_version())?;
620
621        self.output_id.txid().write(&mut writer)?;
622        writer.write_u32::<LittleEndian>(self.output_id.output_index())?;
623
624        writer.write_u32::<LittleEndian>(self.key_id.account_id().into())?;
625        writer.write_u8(self.key_id.scope() as u8)?;
626        writer.write_u32::<LittleEndian>(self.key_id.address_index().index())?;
627
628        write_string(&mut writer, &self.address)?;
629        self.script.write(&mut writer)?;
630        writer.write_u64::<LittleEndian>(self.value())?;
631        Optional::write(&mut writer, self.spending_transaction, |w, txid| {
632            txid.write(w)
633        })?;
634
635        Ok(())
636    }
637}
638
639impl<N, Nf: Copy, P> WalletNote<N, Nf, P> {
640    fn serialized_version() -> u8 {
641        2
642    }
643}
644
645fn read_refetch_nullifier_ranges(
646    reader: &mut impl Read,
647    version: u8,
648) -> std::io::Result<Vec<Range<BlockHeight>>> {
649    if version >= 1 {
650        Vector::read(reader, |r| {
651            let start = r.read_u32::<LittleEndian>()?;
652            let end = r.read_u32::<LittleEndian>()?;
653            Ok(BlockHeight::from_u32(start)..BlockHeight::from_u32(end))
654        })
655    } else {
656        Ok(Vec::new())
657    }
658}
659
660fn write_refetch_nullifier_ranges(
661    writer: &mut impl Write,
662    ranges: &[Range<BlockHeight>],
663) -> std::io::Result<()> {
664    Vector::write(writer, ranges, |w, range| {
665        w.write_u32::<LittleEndian>(range.start.into())?;
666        w.write_u32::<LittleEndian>(range.end.into())
667    })
668}
669
670impl SaplingNote {
671    /// Deserialize into `reader`
672    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
673        let version = read_version(&mut reader, "WalletNote", Self::serialized_version())?;
674
675        let txid = TxId::read(&mut reader)?;
676        let output_index = if version >= 2 {
677            reader.read_u32::<LittleEndian>()?
678        } else {
679            u32::from(reader.read_u16::<LittleEndian>()?)
680        };
681
682        let account_id =
683            zip32::AccountId::try_from(reader.read_u32::<LittleEndian>()?).map_err(|e| {
684                std::io::Error::new(
685                    std::io::ErrorKind::InvalidData,
686                    format!("failed to read account id. {e}"),
687                )
688            })?;
689        let scope = match reader.read_u8()? {
690            0 => Ok(zip32::Scope::External),
691            1 => Ok(zip32::Scope::Internal),
692            _ => Err(std::io::Error::new(
693                std::io::ErrorKind::InvalidData,
694                "invalid scope value",
695            )),
696        }?;
697
698        let mut address_bytes = [0u8; 43];
699        reader.read_exact(&mut address_bytes)?;
700        let recipient =
701            sapling_crypto::PaymentAddress::from_bytes(&address_bytes).ok_or_else(|| {
702                std::io::Error::new(
703                    std::io::ErrorKind::InvalidData,
704                    "failed to read payment address",
705                )
706            })?;
707        let value = sapling_crypto::value::NoteValue::from_raw(reader.read_u64::<LittleEndian>()?);
708        let rseed_zip212 = reader.read_u8()?;
709        let mut rseed_bytes = [0u8; 32];
710        reader.read_exact(&mut rseed_bytes)?;
711        let rseed = match rseed_zip212 {
712            0 => sapling_crypto::Rseed::BeforeZip212(
713                jubjub::Fr::from_bytes(&rseed_bytes).expect("should read valid jubjub bytes"),
714            ),
715            1 => sapling_crypto::Rseed::AfterZip212(rseed_bytes),
716            _ => {
717                return Err(std::io::Error::new(
718                    std::io::ErrorKind::InvalidData,
719                    "invalid rseed zip212 byte",
720                ));
721            }
722        };
723
724        let nullifier = Optional::read(&mut reader, |r| {
725            let mut nullifier_bytes = [0u8; 32];
726            r.read_exact(&mut nullifier_bytes)?;
727
728            sapling_crypto::Nullifier::from_slice(&nullifier_bytes).map_err(|e| {
729                std::io::Error::new(
730                    std::io::ErrorKind::InvalidData,
731                    format!("failed to read nullifier. {e}"),
732                )
733            })
734        })?;
735        let position = Optional::read(&mut reader, |r| {
736            Ok(Position::from(r.read_u64::<LittleEndian>()?))
737        })?;
738        let mut memo_bytes = [0u8; 512];
739        reader.read_exact(&mut memo_bytes)?;
740        let memo = Memo::from_bytes(&memo_bytes).map_err(|e| {
741            std::io::Error::new(
742                std::io::ErrorKind::InvalidData,
743                format!("failed to read memo. {e}"),
744            )
745        })?;
746
747        let spending_transaction = Optional::read(&mut reader, TxId::read)?;
748        let refetch_nullifier_ranges = read_refetch_nullifier_ranges(&mut reader, version)?;
749
750        Ok(Self {
751            output_id: OutputId::new(txid, output_index),
752            key_id: KeyId::from_parts(account_id, scope),
753            note: sapling_crypto::Note::from_parts(recipient, value, rseed),
754            nullifier,
755            position,
756            memo,
757            spending_transaction,
758            refetch_nullifier_ranges,
759            marker: std::marker::PhantomData,
760        })
761    }
762
763    /// Serialize into `writer`
764    pub fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
765        writer.write_u8(Self::serialized_version())?;
766
767        self.output_id.txid().write(&mut writer)?;
768        writer.write_u32::<LittleEndian>(self.output_id.output_index())?;
769
770        writer.write_u32::<LittleEndian>(self.key_id.account_id.into())?;
771        writer.write_u8(self.key_id.scope as u8)?;
772
773        writer.write_all(&self.note.recipient().to_bytes())?;
774        writer.write_u64::<LittleEndian>(self.value())?;
775        match self.note.rseed() {
776            sapling_crypto::Rseed::BeforeZip212(fr) => {
777                writer.write_u8(0)?;
778                writer.write_all(&fr.to_bytes())?;
779            }
780            sapling_crypto::Rseed::AfterZip212(bytes) => {
781                writer.write_u8(1)?;
782                writer.write_all(bytes)?;
783            }
784        }
785
786        Optional::write(&mut writer, self.nullifier, |w, nullifier| {
787            w.write_all(nullifier.as_ref())
788        })?;
789        Optional::write(&mut writer, self.position, |w, position| {
790            w.write_u64::<LittleEndian>(position.into())
791        })?;
792        writer.write_all(self.memo.encode().as_array())?;
793
794        Optional::write(&mut writer, self.spending_transaction, |w, txid| {
795            txid.write(w)
796        })?;
797
798        write_refetch_nullifier_ranges(&mut writer, &self.refetch_nullifier_ranges)
799    }
800}
801
802/// Shared reader for the Orchard-protocol note layout. Orchard and Ironwood
803/// notes serialize identically, differing only in the note version fixed at
804/// construction.
805fn read_orchard_protocol_note<R: Read, P>(
806    mut reader: R,
807    note_version: orchard::note::NoteVersion,
808) -> std::io::Result<WalletNote<orchard::Note, orchard::note::Nullifier, P>> {
809    let version = read_version(
810        &mut reader,
811        "WalletNote",
812        WalletNote::<orchard::Note, orchard::note::Nullifier, P>::serialized_version(),
813    )?;
814
815    let txid = TxId::read(&mut reader)?;
816    let output_index = if version >= 2 {
817        reader.read_u32::<LittleEndian>()?
818    } else {
819        u32::from(reader.read_u16::<LittleEndian>()?)
820    };
821
822    let account_id =
823        zip32::AccountId::try_from(reader.read_u32::<LittleEndian>()?).map_err(|e| {
824            std::io::Error::new(
825                std::io::ErrorKind::InvalidData,
826                format!("failed to read account id. {e}"),
827            )
828        })?;
829    let scope = match reader.read_u8()? {
830        0 => Ok(zip32::Scope::External),
831        1 => Ok(zip32::Scope::Internal),
832        _ => Err(std::io::Error::new(
833            std::io::ErrorKind::InvalidData,
834            "invalid scope value",
835        )),
836    }?;
837
838    let mut address_bytes = [0u8; 43];
839    reader.read_exact(&mut address_bytes)?;
840    let recipient = orchard::Address::from_raw_address_bytes(&address_bytes)
841        .expect("should be a valid address");
842    let value = orchard::value::NoteValue::from_raw(reader.read_u64::<LittleEndian>()?);
843    let mut rho_bytes = [0u8; 32];
844    reader.read_exact(&mut rho_bytes)?;
845    let rho = orchard::note::Rho::from_bytes(&rho_bytes).expect("should be valid rho bytes");
846    let mut rseed_bytes = [0u8; 32];
847    reader.read_exact(&mut rseed_bytes)?;
848    let rseed = orchard::note::RandomSeed::from_bytes(rseed_bytes, &rho)
849        .expect("should be valid random seed bytes");
850
851    let nullifier = Optional::read(&mut reader, |r| {
852        let mut nullifier_bytes = [0u8; 32];
853        r.read_exact(&mut nullifier_bytes)?;
854
855        Ok(orchard::note::Nullifier::from_bytes(&nullifier_bytes)
856            .expect("should be valid nullfiier bytes"))
857    })?;
858    let position = Optional::read(&mut reader, |r| {
859        Ok(Position::from(r.read_u64::<LittleEndian>()?))
860    })?;
861    let mut memo_bytes = [0u8; 512];
862    reader.read_exact(&mut memo_bytes)?;
863    let memo = Memo::from_bytes(&memo_bytes).map_err(|e| {
864        std::io::Error::new(
865            std::io::ErrorKind::InvalidData,
866            format!("failed to read memo. {e}"),
867        )
868    })?;
869
870    let spending_transaction = Optional::read(&mut reader, TxId::read)?;
871    let refetch_nullifier_ranges = read_refetch_nullifier_ranges(&mut reader, version)?;
872
873    Ok(WalletNote {
874        output_id: OutputId::new(txid, output_index),
875        key_id: KeyId::from_parts(account_id, scope),
876        note: orchard::note::Note::from_parts(recipient, value, rho, rseed, note_version)
877            .expect("should be a valid orchard note"),
878        nullifier,
879        position,
880        memo,
881        spending_transaction,
882        refetch_nullifier_ranges,
883        marker: std::marker::PhantomData,
884    })
885}
886
887/// Shared writer for the Orchard-protocol note layout.
888fn write_orchard_protocol_note<W: Write, P>(
889    note: &WalletNote<orchard::Note, orchard::note::Nullifier, P>,
890    mut writer: W,
891) -> std::io::Result<()> {
892    writer
893        .write_u8(WalletNote::<orchard::Note, orchard::note::Nullifier, P>::serialized_version())?;
894
895    note.output_id.txid().write(&mut writer)?;
896    writer.write_u32::<LittleEndian>(note.output_id.output_index())?;
897
898    writer.write_u32::<LittleEndian>(note.key_id.account_id.into())?;
899    writer.write_u8(note.key_id.scope as u8)?;
900
901    writer.write_all(&note.note.recipient().to_raw_address_bytes())?;
902    writer.write_u64::<LittleEndian>(note.note.value().inner())?;
903    writer.write_all(&note.note.rho().to_bytes())?;
904    writer.write_all(note.note.rseed().as_bytes())?;
905
906    Optional::write(&mut writer, note.nullifier, |w, nullifier| {
907        w.write_all(&nullifier.to_bytes())
908    })?;
909    Optional::write(&mut writer, note.position, |w, position| {
910        w.write_u64::<LittleEndian>(position.into())
911    })?;
912    writer.write_all(note.memo.encode().as_array())?;
913    Optional::write(&mut writer, note.spending_transaction, |w, txid| {
914        txid.write(w)
915    })?;
916
917    write_refetch_nullifier_ranges(&mut writer, &note.refetch_nullifier_ranges)
918}
919
920impl OrchardNote {
921    /// Deserialize into `reader`
922    pub fn read<R: Read>(reader: R) -> std::io::Result<Self> {
923        read_orchard_protocol_note(reader, orchard::note::NoteVersion::V2)
924    }
925
926    /// Serialize into `writer`
927    pub fn write<W: Write>(&self, writer: W) -> std::io::Result<()> {
928        write_orchard_protocol_note(self, writer)
929    }
930}
931
932impl IronwoodNote {
933    /// Deserialize into `reader`
934    pub fn read<R: Read>(reader: R) -> std::io::Result<Self> {
935        read_orchard_protocol_note(reader, orchard::note::NoteVersion::V3)
936    }
937
938    /// Serialize into `writer`
939    pub fn write<W: Write>(&self, writer: W) -> std::io::Result<()> {
940        write_orchard_protocol_note(self, writer)
941    }
942}
943
944impl<N, P> OutgoingNote<N, P> {
945    fn serialized_version() -> u8 {
946        1
947    }
948}
949
950impl OutgoingSaplingNote {
951    /// Deserialize into `reader`
952    pub fn read<R: Read>(
953        mut reader: R,
954        consensus_parameters: &impl consensus::Parameters,
955    ) -> std::io::Result<Self> {
956        let version = read_version(&mut reader, "OutgoingNote", Self::serialized_version())?;
957
958        let txid = TxId::read(&mut reader)?;
959        let output_index = if version >= 1 {
960            reader.read_u32::<LittleEndian>()?
961        } else {
962            u32::from(reader.read_u16::<LittleEndian>()?)
963        };
964
965        let account_id =
966            zip32::AccountId::try_from(reader.read_u32::<LittleEndian>()?).map_err(|e| {
967                std::io::Error::new(
968                    std::io::ErrorKind::InvalidData,
969                    format!("failed to read account id. {e}"),
970                )
971            })?;
972        let scope = match reader.read_u8()? {
973            0 => Ok(zip32::Scope::External),
974            1 => Ok(zip32::Scope::Internal),
975            _ => Err(std::io::Error::new(
976                std::io::ErrorKind::InvalidData,
977                "invalid scope value",
978            )),
979        }?;
980
981        let mut address_bytes = [0u8; 43];
982        reader.read_exact(&mut address_bytes)?;
983        let recipient =
984            sapling_crypto::PaymentAddress::from_bytes(&address_bytes).ok_or_else(|| {
985                std::io::Error::new(
986                    std::io::ErrorKind::InvalidData,
987                    "failed to read payment address",
988                )
989            })?;
990        let value = sapling_crypto::value::NoteValue::from_raw(reader.read_u64::<LittleEndian>()?);
991        let rseed_zip212 = reader.read_u8()?;
992        let mut rseed_bytes = [0u8; 32];
993        reader.read_exact(&mut rseed_bytes)?;
994        let rseed = match rseed_zip212 {
995            0 => sapling_crypto::Rseed::BeforeZip212(
996                jubjub::Fr::from_bytes(&rseed_bytes).expect("should read valid jubjub bytes"),
997            ),
998            1 => sapling_crypto::Rseed::AfterZip212(rseed_bytes),
999            _ => {
1000                return Err(std::io::Error::new(
1001                    std::io::ErrorKind::InvalidData,
1002                    "invalid rseed zip212 byte",
1003                ));
1004            }
1005        };
1006
1007        let mut memo_bytes = [0u8; 512];
1008        reader.read_exact(&mut memo_bytes)?;
1009        let memo = Memo::from_bytes(&memo_bytes).map_err(|e| {
1010            std::io::Error::new(
1011                std::io::ErrorKind::InvalidData,
1012                format!("failed to read memo. {e}"),
1013            )
1014        })?;
1015
1016        let recipient_unified_address = Optional::read(&mut reader, |r| {
1017            let encoded_address = read_string(r)?;
1018
1019            decode_unified_address(consensus_parameters, &encoded_address)
1020        })?;
1021
1022        Ok(Self {
1023            output_id: OutputId::new(txid, output_index),
1024            key_id: KeyId::from_parts(account_id, scope),
1025            note: sapling_crypto::Note::from_parts(recipient, value, rseed),
1026            memo,
1027            recipient_full_unified_address: recipient_unified_address,
1028            marker: std::marker::PhantomData,
1029        })
1030    }
1031
1032    /// Serialize into `writer`
1033    pub fn write<W: Write>(
1034        &self,
1035        mut writer: W,
1036        consensus_parameters: &impl consensus::Parameters,
1037    ) -> std::io::Result<()> {
1038        writer.write_u8(Self::serialized_version())?;
1039
1040        self.output_id.txid().write(&mut writer)?;
1041        writer.write_u32::<LittleEndian>(self.output_id.output_index())?;
1042
1043        writer.write_u32::<LittleEndian>(self.key_id.account_id.into())?;
1044        writer.write_u8(self.key_id.scope as u8)?;
1045
1046        writer.write_all(&self.note.recipient().to_bytes())?;
1047        writer.write_u64::<LittleEndian>(self.value())?;
1048        match self.note.rseed() {
1049            sapling_crypto::Rseed::BeforeZip212(fr) => {
1050                writer.write_u8(0)?;
1051                writer.write_all(&fr.to_bytes())?;
1052            }
1053            sapling_crypto::Rseed::AfterZip212(bytes) => {
1054                writer.write_u8(1)?;
1055                writer.write_all(bytes)?;
1056            }
1057        }
1058
1059        writer.write_all(self.memo.encode().as_array())?;
1060        Optional::write(
1061            &mut writer,
1062            self.recipient_full_unified_address.as_ref(),
1063            |w, unified_address| write_string(w, &unified_address.encode(consensus_parameters)),
1064        )?;
1065
1066        Ok(())
1067    }
1068}
1069
1070/// Shared reader for the Orchard-protocol outgoing note layout. Orchard and
1071/// Ironwood outgoing notes serialize identically, differing only in the note
1072/// version fixed at construction.
1073fn read_orchard_protocol_outgoing_note<R: Read, P>(
1074    mut reader: R,
1075    consensus_parameters: &impl consensus::Parameters,
1076    note_version: orchard::note::NoteVersion,
1077) -> std::io::Result<OutgoingNote<orchard::Note, P>> {
1078    let version = read_version(
1079        &mut reader,
1080        "OutgoingNote",
1081        OutgoingNote::<orchard::Note, P>::serialized_version(),
1082    )?;
1083
1084    let txid = TxId::read(&mut reader)?;
1085    let output_index = if version >= 1 {
1086        reader.read_u32::<LittleEndian>()?
1087    } else {
1088        u32::from(reader.read_u16::<LittleEndian>()?)
1089    };
1090
1091    let account_id =
1092        zip32::AccountId::try_from(reader.read_u32::<LittleEndian>()?).map_err(|e| {
1093            std::io::Error::new(
1094                std::io::ErrorKind::InvalidData,
1095                format!("failed to read account id. {e}"),
1096            )
1097        })?;
1098    let scope = match reader.read_u8()? {
1099        0 => Ok(zip32::Scope::External),
1100        1 => Ok(zip32::Scope::Internal),
1101        _ => Err(std::io::Error::new(
1102            std::io::ErrorKind::InvalidData,
1103            "invalid scope value",
1104        )),
1105    }?;
1106
1107    let mut address_bytes = [0u8; 43];
1108    reader.read_exact(&mut address_bytes)?;
1109    let recipient = orchard::Address::from_raw_address_bytes(&address_bytes)
1110        .expect("should be a valid address");
1111    let value = orchard::value::NoteValue::from_raw(reader.read_u64::<LittleEndian>()?);
1112    let mut rho_bytes = [0u8; 32];
1113    reader.read_exact(&mut rho_bytes)?;
1114    let rho = orchard::note::Rho::from_bytes(&rho_bytes).expect("should be valid rho bytes");
1115    let mut rseed_bytes = [0u8; 32];
1116    reader.read_exact(&mut rseed_bytes)?;
1117    let rseed = orchard::note::RandomSeed::from_bytes(rseed_bytes, &rho)
1118        .expect("should be valid random seed bytes");
1119
1120    let mut memo_bytes = [0u8; 512];
1121    reader.read_exact(&mut memo_bytes)?;
1122    let memo = Memo::from_bytes(&memo_bytes).map_err(|e| {
1123        std::io::Error::new(
1124            std::io::ErrorKind::InvalidData,
1125            format!("failed to read memo. {e}"),
1126        )
1127    })?;
1128
1129    let recipient_unified_address = Optional::read(&mut reader, |r| {
1130        let encoded_address = read_string(r)?;
1131
1132        decode_unified_address(consensus_parameters, &encoded_address)
1133    })?;
1134
1135    Ok(OutgoingNote {
1136        output_id: OutputId::new(txid, output_index),
1137        key_id: KeyId::from_parts(account_id, scope),
1138        note: orchard::note::Note::from_parts(recipient, value, rho, rseed, note_version)
1139            .expect("should be a valid orchard note"),
1140        memo,
1141        recipient_full_unified_address: recipient_unified_address,
1142        marker: std::marker::PhantomData,
1143    })
1144}
1145
1146/// Shared writer for the Orchard-protocol outgoing note layout.
1147fn write_orchard_protocol_outgoing_note<W: Write, P>(
1148    note: &OutgoingNote<orchard::Note, P>,
1149    mut writer: W,
1150    consensus_parameters: &impl consensus::Parameters,
1151) -> std::io::Result<()> {
1152    writer.write_u8(OutgoingNote::<orchard::Note, P>::serialized_version())?;
1153
1154    note.output_id.txid().write(&mut writer)?;
1155    writer.write_u32::<LittleEndian>(note.output_id.output_index())?;
1156
1157    writer.write_u32::<LittleEndian>(note.key_id.account_id.into())?;
1158    writer.write_u8(note.key_id.scope as u8)?;
1159
1160    writer.write_all(&note.note.recipient().to_raw_address_bytes())?;
1161    writer.write_u64::<LittleEndian>(note.note.value().inner())?;
1162    writer.write_all(&note.note.rho().to_bytes())?;
1163    writer.write_all(note.note.rseed().as_bytes())?;
1164
1165    writer.write_all(note.memo.encode().as_array())?;
1166    Optional::write(
1167        &mut writer,
1168        note.recipient_full_unified_address.as_ref(),
1169        |w, unified_address| write_string(w, &unified_address.encode(consensus_parameters)),
1170    )?;
1171
1172    Ok(())
1173}
1174
1175impl OutgoingOrchardNote {
1176    /// Deserialize into `reader`
1177    pub fn read<R: Read>(
1178        reader: R,
1179        consensus_parameters: &impl consensus::Parameters,
1180    ) -> std::io::Result<Self> {
1181        read_orchard_protocol_outgoing_note(
1182            reader,
1183            consensus_parameters,
1184            orchard::note::NoteVersion::V2,
1185        )
1186    }
1187
1188    /// Serialize into `writer`
1189    pub fn write<W: Write>(
1190        &self,
1191        writer: W,
1192        consensus_parameters: &impl consensus::Parameters,
1193    ) -> std::io::Result<()> {
1194        write_orchard_protocol_outgoing_note(self, writer, consensus_parameters)
1195    }
1196}
1197
1198impl OutgoingIronwoodNote {
1199    /// Deserialize into `reader`
1200    pub fn read<R: Read>(
1201        reader: R,
1202        consensus_parameters: &impl consensus::Parameters,
1203    ) -> std::io::Result<Self> {
1204        read_orchard_protocol_outgoing_note(
1205            reader,
1206            consensus_parameters,
1207            orchard::note::NoteVersion::V3,
1208        )
1209    }
1210
1211    /// Serialize into `writer`
1212    pub fn write<W: Write>(
1213        &self,
1214        writer: W,
1215        consensus_parameters: &impl consensus::Parameters,
1216    ) -> std::io::Result<()> {
1217        write_orchard_protocol_outgoing_note(self, writer, consensus_parameters)
1218    }
1219}
1220
1221impl ShardTrees {
1222    fn serialized_version() -> u8 {
1223        // Version 1 appends the Ironwood shard tree after the Orchard one.
1224        1
1225    }
1226
1227    /// Deserialize into `reader`
1228    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
1229        let version = read_version(&mut reader, "ShardTrees", Self::serialized_version())?;
1230        let sapling = Self::read_shardtree(&mut reader)?;
1231        let orchard = Self::read_shardtree(&mut reader)?;
1232        let ironwood = if version >= 1 {
1233            Self::read_shardtree(&mut reader)?
1234        } else {
1235            // Pre-Ironwood wallet files: start with an empty Ironwood tree.
1236            Self::new().ironwood
1237        };
1238
1239        Ok(Self {
1240            sapling,
1241            orchard,
1242            ironwood,
1243        })
1244    }
1245
1246    /// Serialize into `writer`
1247    pub fn write<W: Write>(&mut self, mut writer: W) -> std::io::Result<()> {
1248        writer.write_u8(Self::serialized_version())?;
1249        Self::write_shardtree(&mut writer, &mut self.sapling)?;
1250        Self::write_shardtree(&mut writer, &mut self.orchard)?;
1251        Self::write_shardtree(&mut writer, &mut self.ironwood)?;
1252
1253        Ok(())
1254    }
1255
1256    fn read_shardtree<
1257        H: Hashable + Clone + HashSer + Eq,
1258        C: Ord + std::fmt::Debug + Copy + From<u32>,
1259        R: Read,
1260        const DEPTH: u8,
1261        const SHARD_HEIGHT: u8,
1262    >(
1263        mut reader: R,
1264    ) -> std::io::Result<ShardTree<MemoryShardStore<H, C>, DEPTH, SHARD_HEIGHT>> {
1265        let shards = Vector::read(&mut reader, |r| {
1266            let level = incrementalmerkletree::Level::from(r.read_u8()?);
1267            let index = r.read_u64::<LittleEndian>()?;
1268            let root_addr = incrementalmerkletree::Address::from_parts(level, index);
1269            let shard = read_shard(r)?;
1270
1271            LocatedPrunableTree::from_parts(root_addr, shard).map_err(|addr| {
1272                std::io::Error::new(
1273                    std::io::ErrorKind::InvalidData,
1274                    format!("parent node in root has level 0 relative to root address: {addr:?}"),
1275                )
1276            })
1277        })?;
1278        let mut store = MemoryShardStore::empty();
1279        for shard in shards {
1280            store.put_shard(shard).expect("infallible");
1281        }
1282        let checkpoints = Vector::read(&mut reader, |r| {
1283            let checkpoint_id = C::from(r.read_u32::<LittleEndian>()?);
1284            let tree_state = match r.read_u8()? {
1285                0 => TreeState::Empty,
1286                1 => TreeState::AtPosition(Position::from(r.read_u64::<LittleEndian>()?)),
1287                otherwise => {
1288                    return Err(std::io::Error::new(
1289                        std::io::ErrorKind::InvalidData,
1290                        format!(
1291                            "failed to read TreeState. expected boolean value, found {otherwise}"
1292                        ),
1293                    ));
1294                }
1295            };
1296            let marks_removed =
1297                Vector::read(r, |r| r.read_u64::<LittleEndian>().map(Position::from))?;
1298            Ok((
1299                checkpoint_id,
1300                Checkpoint::from_parts(tree_state, marks_removed.into_iter().collect()),
1301            ))
1302        })?;
1303        if checkpoints.is_empty() {
1304            store
1305                .add_checkpoint(C::from(0), Checkpoint::tree_empty())
1306                .expect("Infallible");
1307        }
1308        for (checkpoint_id, checkpoint) in checkpoints {
1309            store
1310                .add_checkpoint(checkpoint_id, checkpoint)
1311                .expect("Infallible");
1312        }
1313        store.put_cap(read_shard(reader)?).expect("Infallible");
1314
1315        Ok(shardtree::ShardTree::new(
1316            store,
1317            SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1318        ))
1319    }
1320
1321    /// Write memory-backed shardstore, represented tree.
1322    fn write_shardtree<
1323        H: Hashable + Clone + Eq + HashSer,
1324        C: Ord + std::fmt::Debug + Copy,
1325        W: Write,
1326        const DEPTH: u8,
1327        const SHARD_HEIGHT: u8,
1328    >(
1329        mut writer: W,
1330        shardtree: &mut ShardTree<MemoryShardStore<H, C>, DEPTH, SHARD_HEIGHT>,
1331    ) -> std::io::Result<()>
1332    where
1333        u32: From<C>,
1334    {
1335        fn write_shards<W, H, C>(
1336            mut writer: W,
1337            store: &MemoryShardStore<H, C>,
1338        ) -> std::io::Result<()>
1339        where
1340            H: Hashable + Clone + Eq + HashSer,
1341            C: Ord + std::fmt::Debug + Copy,
1342            W: Write,
1343        {
1344            let roots = store.get_shard_roots().expect("Infallible");
1345            Vector::write(&mut writer, &roots, |w, root| {
1346                w.write_u8(root.level().into())?;
1347                w.write_u64::<LittleEndian>(root.index())?;
1348                let shard = store
1349                    .get_shard(*root)
1350                    .expect("Infallible")
1351                    .expect("cannot find root that shard store claims to have");
1352                write_shard(w, shard.root())
1353            })
1354        }
1355
1356        fn write_checkpoints<W, Cid>(
1357            mut writer: W,
1358            checkpoints: &[(Cid, Checkpoint)],
1359        ) -> std::io::Result<()>
1360        where
1361            W: Write,
1362            Cid: Ord + std::fmt::Debug + Copy,
1363            u32: From<Cid>,
1364        {
1365            Vector::write(
1366                &mut writer,
1367                checkpoints,
1368                |mut w, (checkpoint_id, checkpoint)| {
1369                    w.write_u32::<LittleEndian>(u32::from(*checkpoint_id))?;
1370                    match checkpoint.tree_state() {
1371                        shardtree::store::TreeState::Empty => w.write_u8(0),
1372                        shardtree::store::TreeState::AtPosition(pos) => {
1373                            w.write_u8(1)?;
1374                            w.write_u64::<LittleEndian>(<u64 as From<Position>>::from(pos))
1375                        }
1376                    }?;
1377                    Vector::write(
1378                        &mut w,
1379                        &checkpoint.marks_removed().iter().collect::<Vec<_>>(),
1380                        |w, mark| {
1381                            w.write_u64::<LittleEndian>(<u64 as From<Position>>::from(**mark))
1382                        },
1383                    )
1384                },
1385            )
1386        }
1387
1388        // Replace original tree with empty tree, and mutate new version into store.
1389        let mut store = std::mem::replace(
1390            shardtree,
1391            shardtree::ShardTree::new(MemoryShardStore::empty(), 0),
1392        )
1393        .into_store();
1394
1395        macro_rules! write_with_error_handling {
1396            ($writer: ident, $from: ident) => {
1397                if let Err(e) = $writer(&mut writer, &$from) {
1398                    *shardtree = shardtree::ShardTree::new(
1399                        store,
1400                        SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1401                    );
1402                    return Err(e);
1403                }
1404            };
1405        }
1406
1407        // Write located prunable trees
1408        write_with_error_handling!(write_shards, store);
1409
1410        // Write checkpoints
1411        let mut checkpoints = Vec::new();
1412        let checkpoint_count = store.checkpoint_count().expect("Infallible");
1413        store
1414            .with_checkpoints(checkpoint_count, |checkpoint_id, checkpoint| {
1415                checkpoints.push((*checkpoint_id, checkpoint.clone()));
1416                Ok(())
1417            })
1418            .expect("Infallible");
1419        write_with_error_handling!(write_checkpoints, checkpoints);
1420
1421        // Write cap
1422        let cap = store.get_cap().expect("Infallible");
1423        write_with_error_handling!(write_shard, cap);
1424
1425        *shardtree =
1426            shardtree::ShardTree::new(store, SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize);
1427
1428        Ok(())
1429    }
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434    use crate::{sync::MAX_SHARDTREE_CHECKPOINTS, witness::ANCHOR_RETENTION_INTERVALS};
1435
1436    use super::*;
1437
1438    // checkpoints appended past the rolling window, so pruning has run before a round trip
1439    const APPENDED: u32 = SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE + 49;
1440
1441    fn written(scan_target: &ScanTarget) -> Vec<u8> {
1442        let mut bytes = Vec::new();
1443        scan_target.write(&mut bytes).unwrap();
1444        bytes
1445    }
1446
1447    #[test]
1448    fn scan_target_round_trips_with_its_pools_and_origin() {
1449        let txid = TxId::from_bytes([3; 32]);
1450        for scan_target in [
1451            ScanTarget::new(BlockHeight::from_u32(10), None),
1452            ScanTarget::new(BlockHeight::from_u32(10), Some(txid)),
1453            ScanTarget::internal(BlockHeight::from_u32(20), txid)
1454                .with_pool(zcash_protocol::ShieldedPool::Sapling)
1455                .with_pool(zcash_protocol::ShieldedPool::Ironwood),
1456        ] {
1457            let read = ScanTarget::read(written(&scan_target).as_slice()).unwrap();
1458            assert_eq!(read, scan_target);
1459            assert_eq!(read.txid(), scan_target.txid());
1460            assert_eq!(read.pools(), scan_target.pools());
1461            assert_eq!(read.origin(), scan_target.origin());
1462        }
1463    }
1464
1465    // A version 0 target carried a zero txid where no particular transaction was wanted, and a narrow scan area
1466    // flag. It is read as an external target with no pool.
1467    #[test]
1468    fn scan_target_version_zero_is_read_as_external() {
1469        let v0 = |txid: [u8; 32]| {
1470            let mut bytes = vec![0u8];
1471            bytes.write_u32::<LittleEndian>(30).unwrap();
1472            bytes.extend_from_slice(&txid);
1473            bytes.push(1);
1474            bytes
1475        };
1476
1477        let without_transaction = ScanTarget::read(v0([0; 32]).as_slice()).unwrap();
1478        assert_eq!(
1479            without_transaction,
1480            ScanTarget::new(BlockHeight::from_u32(30), None)
1481        );
1482        assert_eq!(without_transaction.txid(), None);
1483        assert_eq!(without_transaction.origin(), ScanTargetOrigin::External);
1484        assert!(without_transaction.pools().is_empty());
1485
1486        let with_transaction = ScanTarget::read(v0([5; 32]).as_slice()).unwrap();
1487        assert_eq!(with_transaction.txid(), Some(TxId::from_bytes([5; 32])));
1488    }
1489
1490    #[test]
1491    fn scan_target_with_an_unknown_pool_or_origin_is_refused() {
1492        let mut unknown_pool = written(&ScanTarget::new(BlockHeight::from_u32(10), None));
1493        *unknown_pool.last_mut().unwrap() = 0;
1494        let pools_index = unknown_pool.len() - 2;
1495        unknown_pool[pools_index] = 1 << 3;
1496        assert_eq!(
1497            ScanTarget::read(unknown_pool.as_slice())
1498                .unwrap_err()
1499                .kind(),
1500            std::io::ErrorKind::InvalidData
1501        );
1502
1503        let mut unknown_origin = written(&ScanTarget::new(BlockHeight::from_u32(10), None));
1504        *unknown_origin.last_mut().unwrap() = 2;
1505        assert_eq!(
1506            ScanTarget::read(unknown_origin.as_slice())
1507                .unwrap_err()
1508                .kind(),
1509            std::io::ErrorKind::InvalidData
1510        );
1511    }
1512
1513    // Helper: build a minimal v3 SyncState byte blob (no ironwood_shard_ranges).
1514    // Format: version(1) | scan_ranges[0] | sapling_shard_ranges[0] |
1515    //         orchard_shard_ranges[0] | scan_targets[0]
1516    fn v3_sync_state_bytes() -> Vec<u8> {
1517        let mut out = Vec::new();
1518        out.write_u8(3).unwrap();
1519        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1520        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1521        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1522        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1523        out
1524    }
1525
1526    #[test]
1527    fn sync_state_v3_reads_with_empty_ironwood_ranges() {
1528        let bytes = v3_sync_state_bytes();
1529        let sync_state = SyncState::read(bytes.as_slice()).expect("v3 should read cleanly");
1530        assert!(sync_state.ironwood_shard_ranges().is_empty());
1531    }
1532
1533    #[test]
1534    fn sync_state_v4_roundtrip_preserves_ironwood_ranges() {
1535        let mut state = SyncState::new();
1536        state.ironwood_shard_ranges = vec![
1537            BlockHeight::from_u32(100)..BlockHeight::from_u32(200),
1538            BlockHeight::from_u32(300)..BlockHeight::from_u32(400),
1539        ];
1540        state.scan_ranges.push(ScanRange::from_parts(
1541            BlockHeight::from_u32(100)..BlockHeight::from_u32(400),
1542            ScanPriority::Historic,
1543        ));
1544        let mut bytes = Vec::new();
1545        state.write(&mut bytes).expect("write should succeed");
1546        let recovered = SyncState::read(bytes.as_slice()).expect("read should succeed");
1547        assert_eq!(recovered.ironwood_shard_ranges, state.ironwood_shard_ranges);
1548        assert_eq!(recovered.scan_ranges, state.scan_ranges);
1549    }
1550
1551    // Helper: build a minimal v4 SyncState byte blob (no transparent scan floor).
1552    // Format: version(1) | scan_ranges[0] | sapling_shard_ranges[0] |
1553    //         orchard_shard_ranges[0] | ironwood_shard_ranges[0] | scan_targets[0]
1554    fn v4_sync_state_bytes() -> Vec<u8> {
1555        let mut out = Vec::new();
1556        out.write_u8(4).unwrap();
1557        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1558        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1559        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1560        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1561        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1562        out
1563    }
1564
1565    #[test]
1566    fn sync_state_v4_reads_with_unset_transparent_scan_floor() {
1567        let bytes = v4_sync_state_bytes();
1568        let sync_state = SyncState::read(bytes.as_slice()).expect("v4 should read cleanly");
1569        assert_eq!(sync_state.transparent_scan_floor, None);
1570    }
1571
1572    #[test]
1573    fn sync_state_v5_roundtrip_preserves_transparent_scan_floor() {
1574        for transparent_scan_floor in [None, Some(BlockHeight::from_u32(100))] {
1575            let mut state = SyncState::new();
1576            state.transparent_scan_floor = transparent_scan_floor;
1577            let mut bytes = Vec::new();
1578            state.write(&mut bytes).expect("write should succeed");
1579            let recovered = SyncState::read(bytes.as_slice()).expect("read should succeed");
1580            assert_eq!(recovered.transparent_scan_floor, transparent_scan_floor);
1581        }
1582    }
1583
1584    mod read_version {
1585        use super::super::read_version;
1586
1587        /// The current version of a made-up type. It follows no type's serialized version.
1588        const ARBITRARY_CURRENT_VERSION: u8 = 2;
1589
1590        #[test]
1591        fn accepts_versions_up_to_the_current_version() {
1592            for version in [0, ARBITRARY_CURRENT_VERSION] {
1593                assert_eq!(
1594                    read_version([version].as_slice(), "Type", ARBITRARY_CURRENT_VERSION).unwrap(),
1595                    version
1596                );
1597            }
1598        }
1599
1600        #[test]
1601        fn refuses_a_version_above_the_current_version() {
1602            let error = read_version(
1603                [ARBITRARY_CURRENT_VERSION + 1].as_slice(),
1604                "Type",
1605                ARBITRARY_CURRENT_VERSION,
1606            )
1607            .expect_err("a version above the current version is refused");
1608            assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
1609        }
1610    }
1611
1612    /// Each reader is given only a serialized version above the one its type writes. A reader that refuses the
1613    /// version returns invalid data. A reader that read on would report the end of the input.
1614    #[test]
1615    fn readers_refuse_serialized_versions_above_their_own() {
1616        let consensus_parameters = zcash_protocol::consensus::MAIN_NETWORK;
1617        let newer_scan_target = [ScanTarget::serialized_version() + 1];
1618        let newer_sync_state = [SyncState::serialized_version() + 1];
1619        let newer_tree_bounds = [TreeBounds::serialized_version() + 1];
1620        let newer_nullifier_map = [NullifierMap::serialized_version() + 1];
1621        let newer_wallet_block = [WalletBlock::serialized_version() + 1];
1622        let newer_wallet_transaction = [WalletTransaction::serialized_version() + 1];
1623        let newer_transparent_coin = [TransparentCoin::serialized_version() + 1];
1624        let newer_wallet_note = [SaplingNote::serialized_version() + 1];
1625        let newer_outgoing_note = [OutgoingSaplingNote::serialized_version() + 1];
1626        let newer_shard_trees = [ShardTrees::serialized_version() + 1];
1627
1628        for (type_name, read) in [
1629            (
1630                "ScanTarget",
1631                ScanTarget::read(newer_scan_target.as_slice()).map(drop),
1632            ),
1633            (
1634                "SyncState",
1635                SyncState::read(newer_sync_state.as_slice()).map(drop),
1636            ),
1637            (
1638                "TreeBounds",
1639                TreeBounds::read(newer_tree_bounds.as_slice()).map(drop),
1640            ),
1641            (
1642                "NullifierMap",
1643                NullifierMap::read(newer_nullifier_map.as_slice()).map(drop),
1644            ),
1645            (
1646                "WalletBlock",
1647                WalletBlock::read(newer_wallet_block.as_slice()).map(drop),
1648            ),
1649            (
1650                "WalletTransaction",
1651                WalletTransaction::read(newer_wallet_transaction.as_slice(), &consensus_parameters)
1652                    .map(drop),
1653            ),
1654            (
1655                "TransparentCoin",
1656                TransparentCoin::read(newer_transparent_coin.as_slice()).map(drop),
1657            ),
1658            (
1659                "SaplingNote",
1660                SaplingNote::read(newer_wallet_note.as_slice()).map(drop),
1661            ),
1662            (
1663                "OrchardNote",
1664                OrchardNote::read(newer_wallet_note.as_slice()).map(drop),
1665            ),
1666            (
1667                "IronwoodNote",
1668                IronwoodNote::read(newer_wallet_note.as_slice()).map(drop),
1669            ),
1670            (
1671                "OutgoingSaplingNote",
1672                OutgoingSaplingNote::read(newer_outgoing_note.as_slice(), &consensus_parameters)
1673                    .map(drop),
1674            ),
1675            (
1676                "OutgoingOrchardNote",
1677                OutgoingOrchardNote::read(newer_outgoing_note.as_slice(), &consensus_parameters)
1678                    .map(drop),
1679            ),
1680            (
1681                "OutgoingIronwoodNote",
1682                OutgoingIronwoodNote::read(newer_outgoing_note.as_slice(), &consensus_parameters)
1683                    .map(drop),
1684            ),
1685            (
1686                "ShardTrees",
1687                ShardTrees::read(newer_shard_trees.as_slice()).map(drop),
1688            ),
1689        ] {
1690            assert_eq!(
1691                read.expect_err(type_name).kind(),
1692                std::io::ErrorKind::InvalidData,
1693                "{type_name}"
1694            );
1695        }
1696    }
1697
1698    // Helper: build a minimal v1 NullifierMap byte blob (no ironwood BTreeMap).
1699    fn v1_nullifier_map_bytes() -> Vec<u8> {
1700        let mut out = Vec::new();
1701        out.write_u8(1).unwrap();
1702        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1703        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1704        out
1705    }
1706
1707    #[test]
1708    fn nullifier_map_v1_reads_with_empty_ironwood_map() {
1709        let bytes = v1_nullifier_map_bytes();
1710        let map = NullifierMap::read(bytes.as_slice()).expect("v1 should read cleanly");
1711        assert!(map.ironwood.is_empty());
1712    }
1713
1714    #[test]
1715    fn nullifier_map_v2_roundtrip_preserves_ironwood() {
1716        let map = NullifierMap::new();
1717        let mut bytes = Vec::new();
1718        map.write(&mut bytes).expect("write should succeed");
1719        let recovered = NullifierMap::read(bytes.as_slice()).expect("read should succeed");
1720        assert!(recovered.ironwood.is_empty());
1721    }
1722
1723    // Helper: build a v0 TreeBounds byte blob (no ironwood tree sizes).
1724    fn v0_tree_bounds_bytes(
1725        sapling_initial: u32,
1726        sapling_final: u32,
1727        orchard_initial: u32,
1728        orchard_final: u32,
1729    ) -> Vec<u8> {
1730        let mut out = Vec::new();
1731        out.write_u8(0).unwrap();
1732        out.write_u32::<LittleEndian>(sapling_initial).unwrap();
1733        out.write_u32::<LittleEndian>(sapling_final).unwrap();
1734        out.write_u32::<LittleEndian>(orchard_initial).unwrap();
1735        out.write_u32::<LittleEndian>(orchard_final).unwrap();
1736        out
1737    }
1738
1739    #[test]
1740    fn tree_bounds_v0_reads_with_zero_ironwood_sizes() {
1741        let bytes = v0_tree_bounds_bytes(10, 20, 30, 40);
1742        let bounds = TreeBounds::read(bytes.as_slice()).expect("v0 should read cleanly");
1743        assert_eq!(bounds.sapling_initial_tree_size, 10);
1744        assert_eq!(bounds.orchard_final_tree_size, 40);
1745        assert_eq!(bounds.ironwood_initial_tree_size, 0);
1746        assert_eq!(bounds.ironwood_final_tree_size, 0);
1747    }
1748
1749    #[test]
1750    fn tree_bounds_v1_roundtrip() {
1751        let bounds = TreeBounds {
1752            sapling_initial_tree_size: 1,
1753            sapling_final_tree_size: 2,
1754            orchard_initial_tree_size: 3,
1755            orchard_final_tree_size: 4,
1756            ironwood_initial_tree_size: 5,
1757            ironwood_final_tree_size: 6,
1758        };
1759        let mut bytes = Vec::new();
1760        bounds.write(&mut bytes).expect("write should succeed");
1761        let recovered = TreeBounds::read(bytes.as_slice()).expect("read should succeed");
1762        assert_eq!(recovered.ironwood_initial_tree_size, 5);
1763        assert_eq!(recovered.ironwood_final_tree_size, 6);
1764    }
1765
1766    /// The checkpoint set of a synced wallet decomposes into exactly two parts:
1767    /// [`SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE`] rolling checkpoints, which serve ordinary reorg handling and
1768    /// near-tip spends, plus [`ANCHOR_RETENTION_INTERVALS`] pinned ZIP 318 grid boundaries,
1769    /// which serve pool crossings.
1770    ///
1771    /// The two parts are disjoint and independently bounded: pinning a boundary must not
1772    /// consume a rolling slot (that would shrink the reorg window), and the rolling budget must
1773    /// not displace a boundary (that would break crossings).
1774    #[test]
1775    fn checkpoint_set_is_reorg_window_plus_pinned_boundaries() {
1776        use crate::shardtree_ext::ShardTreeExt as _;
1777        use crate::witness::{anchor_retention_window, repin_anchor_checkpoints};
1778        use zcash_client_backend::data_api::anchor_retention::{
1779            AnchorRetention, AnchorRetentionInterval,
1780        };
1781
1782        const TIP: u32 = 100_000;
1783        const INTERVAL: u32 = 144;
1784        let policy = AnchorRetention::new(
1785            BlockHeight::from_u32(90_000),
1786            zcash_protocol::zip318::BlockSpacing::UNIFORM,
1787            AnchorRetentionInterval::default(),
1788        );
1789        let mut shard_trees = ShardTrees::new();
1790
1791        for height in (TIP - 2000)..=TIP {
1792            let height = BlockHeight::from_u32(height);
1793            let window = anchor_retention_window(&policy, height);
1794            repin_anchor_checkpoints(&policy, &window, shard_trees.orchard.store_mut());
1795            shard_trees
1796                .orchard
1797                .append_checkpoint(height)
1798                .expect("infallible");
1799        }
1800
1801        let store = shard_trees.orchard.store();
1802        let total = store.checkpoint_count().expect("infallible");
1803        let pinned_ids = store.retained_checkpoints().expect("infallible");
1804        let mut pinned = Vec::new();
1805        let mut rolling = Vec::new();
1806        store
1807            .for_each_checkpoint(total, |id, _| {
1808                if pinned_ids.contains(id) {
1809                    pinned.push(u32::from(*id));
1810                } else {
1811                    rolling.push(u32::from(*id));
1812                }
1813                Ok(())
1814            })
1815            .expect("infallible");
1816
1817        assert!(
1818            pinned.iter().all(|height| height % INTERVAL == 0),
1819            "every pinned checkpoint must be a grid boundary, got {pinned:?}"
1820        );
1821        assert_eq!(rolling.last().copied(), Some(TIP));
1822        assert_eq!(
1823            (rolling.len(), pinned.len(), total),
1824            (
1825                SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1826                ANCHOR_RETENTION_INTERVALS as usize,
1827                MAX_SHARDTREE_CHECKPOINTS as usize,
1828            ),
1829            "(rolling, pinned, total): the pinned boundaries must not be part of the \
1830             SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE total"
1831        );
1832
1833        for height in (TIP - SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE + 1)..=TIP {
1834            assert!(
1835                store
1836                    .get_checkpoint(&BlockHeight::from_u32(height))
1837                    .expect("infallible")
1838                    .is_some(),
1839                "reorg window is missing height {height}"
1840            );
1841        }
1842
1843        // a pinned boundary inside the rolling window holds no rolling slot, so the rolling set reaches at
1844        // most one height per boundary below the window
1845        assert!(
1846            rolling
1847                .iter()
1848                .all(|height| *height >= TIP - MAX_SHARDTREE_CHECKPOINTS),
1849            "a rolling checkpoint survived below the reorg window: {rolling:?}"
1850        );
1851
1852        let mut bytes = Vec::new();
1853        shard_trees.write(&mut bytes).expect("write should succeed");
1854        let reloaded = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
1855        let reloaded_store = reloaded.orchard.store();
1856        for boundary in &pinned {
1857            assert!(
1858                reloaded_store
1859                    .get_checkpoint(&BlockHeight::from_u32(*boundary))
1860                    .expect("infallible")
1861                    .is_some(),
1862                "boundary {boundary} lost on reload; a crossing anchored there cannot be built"
1863            );
1864        }
1865        assert_eq!(
1866            reloaded_store.checkpoint_count().expect("infallible"),
1867            MAX_SHARDTREE_CHECKPOINTS as usize
1868        );
1869    }
1870
1871    /// Serialization preserves the checkpoints pruning left instead of imposing a cap.
1872    #[test]
1873    fn shardtree_roundtrip_keeps_newest_checkpoints() {
1874        use crate::shardtree_ext::ShardTreeExt as _;
1875
1876        let mut shard_trees = ShardTrees::new();
1877
1878        for height in 1..=APPENDED {
1879            let height = BlockHeight::from_u32(height);
1880            shard_trees
1881                .sapling
1882                .append_checkpoint(height)
1883                .expect("infallible");
1884            shard_trees
1885                .orchard
1886                .append_checkpoint(height)
1887                .expect("infallible");
1888        }
1889
1890        let mut bytes = Vec::new();
1891        shard_trees.write(&mut bytes).expect("write should succeed");
1892        let roundtripped = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
1893
1894        let sapling_store = roundtripped.sapling.store();
1895        let orchard_store = roundtripped.orchard.store();
1896
1897        let oldest_kept =
1898            BlockHeight::from_u32(APPENDED - SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE + 1);
1899        fn assert_window<S>(store: &S, oldest_kept: BlockHeight)
1900        where
1901            S: ShardStore<CheckpointId = BlockHeight, Error = std::convert::Infallible>,
1902        {
1903            assert_eq!(
1904                store.checkpoint_count().expect("infallible"),
1905                SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize
1906            );
1907            assert_eq!(
1908                store.min_checkpoint_id().expect("infallible"),
1909                Some(oldest_kept)
1910            );
1911            assert_eq!(
1912                store.max_checkpoint_id().expect("infallible"),
1913                Some(BlockHeight::from_u32(APPENDED))
1914            );
1915            assert!(
1916                store
1917                    .get_checkpoint(&(oldest_kept - 1))
1918                    .expect("infallible")
1919                    .is_none()
1920            );
1921        }
1922        assert_window(sapling_store, oldest_kept);
1923        assert_window(orchard_store, oldest_kept);
1924    }
1925
1926    /// A blob with no checkpoint reads back with the height-zero checkpoint.
1927    #[test]
1928    fn shardtree_read_adds_initialization_checkpoint_when_blob_has_none() {
1929        fn checkpoint_less<H, const DEPTH: u8, const SHARD_HEIGHT: u8>()
1930        -> ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>
1931        where
1932            H: Hashable + Clone + PartialEq,
1933        {
1934            ShardTree::new(
1935                MemoryShardStore::empty(),
1936                SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1937            )
1938        }
1939
1940        let mut shard_trees = ShardTrees {
1941            sapling: checkpoint_less(),
1942            orchard: checkpoint_less(),
1943            ironwood: checkpoint_less(),
1944        };
1945        assert_eq!(
1946            shard_trees
1947                .sapling
1948                .store()
1949                .max_checkpoint_id()
1950                .expect("infallible"),
1951            None
1952        );
1953
1954        let mut bytes = Vec::new();
1955        shard_trees.write(&mut bytes).expect("write should succeed");
1956        let roundtripped = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
1957
1958        fn assert_initialization_checkpoint<S>(store: &S)
1959        where
1960            S: ShardStore<CheckpointId = BlockHeight, Error = std::convert::Infallible>,
1961        {
1962            assert_eq!(store.checkpoint_count().expect("infallible"), 1);
1963            assert_eq!(
1964                store.max_checkpoint_id().expect("infallible"),
1965                Some(BlockHeight::from_u32(0))
1966            );
1967        }
1968        assert_initialization_checkpoint(roundtripped.sapling.store());
1969        assert_initialization_checkpoint(roundtripped.orchard.store());
1970        assert_initialization_checkpoint(roundtripped.ironwood.store());
1971    }
1972
1973    /// A pinned anchor checkpoint survives serialization even once it has aged out of the
1974    /// rolling window. The pinned set itself is not persisted, being re-derived at the start of
1975    /// every sync.
1976    #[test]
1977    fn shardtree_roundtrip_keeps_pinned_anchor_checkpoints() {
1978        use crate::shardtree_ext::ShardTreeExt as _;
1979
1980        let pinned = BlockHeight::from_u32(24);
1981        let mut shard_trees = ShardTrees::new();
1982
1983        shard_trees
1984            .sapling
1985            .store_mut()
1986            .add_retained_checkpoint(pinned)
1987            .expect("infallible");
1988        for height in 1..=APPENDED {
1989            shard_trees
1990                .sapling
1991                .append_checkpoint(BlockHeight::from_u32(height))
1992                .expect("infallible");
1993        }
1994
1995        assert!(
1996            shard_trees
1997                .sapling
1998                .store()
1999                .get_checkpoint(&pinned)
2000                .expect("infallible")
2001                .is_some(),
2002            "pruning must not evict a pinned checkpoint"
2003        );
2004
2005        let mut bytes = Vec::new();
2006        shard_trees.write(&mut bytes).expect("write should succeed");
2007        let roundtripped = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
2008
2009        let sapling_store = roundtripped.sapling.store();
2010        assert!(
2011            sapling_store
2012                .get_checkpoint(&pinned)
2013                .expect("infallible")
2014                .is_some(),
2015            "serialization must not evict a pinned checkpoint"
2016        );
2017        assert_eq!(
2018            sapling_store.checkpoint_count().expect("infallible"),
2019            SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize + 1
2020        );
2021        assert!(
2022            sapling_store
2023                .retained_checkpoints()
2024                .expect("infallible")
2025                .is_empty()
2026        );
2027    }
2028}