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| {
1064                write_string(
1065                    w,
1066                    &unified_address.encode_receiver_preserving(consensus_parameters),
1067                )
1068            },
1069        )?;
1070
1071        Ok(())
1072    }
1073}
1074
1075/// Shared reader for the Orchard-protocol outgoing note layout. Orchard and
1076/// Ironwood outgoing notes serialize identically, differing only in the note
1077/// version fixed at construction.
1078fn read_orchard_protocol_outgoing_note<R: Read, P>(
1079    mut reader: R,
1080    consensus_parameters: &impl consensus::Parameters,
1081    note_version: orchard::note::NoteVersion,
1082) -> std::io::Result<OutgoingNote<orchard::Note, P>> {
1083    let version = read_version(
1084        &mut reader,
1085        "OutgoingNote",
1086        OutgoingNote::<orchard::Note, P>::serialized_version(),
1087    )?;
1088
1089    let txid = TxId::read(&mut reader)?;
1090    let output_index = if version >= 1 {
1091        reader.read_u32::<LittleEndian>()?
1092    } else {
1093        u32::from(reader.read_u16::<LittleEndian>()?)
1094    };
1095
1096    let account_id =
1097        zip32::AccountId::try_from(reader.read_u32::<LittleEndian>()?).map_err(|e| {
1098            std::io::Error::new(
1099                std::io::ErrorKind::InvalidData,
1100                format!("failed to read account id. {e}"),
1101            )
1102        })?;
1103    let scope = match reader.read_u8()? {
1104        0 => Ok(zip32::Scope::External),
1105        1 => Ok(zip32::Scope::Internal),
1106        _ => Err(std::io::Error::new(
1107            std::io::ErrorKind::InvalidData,
1108            "invalid scope value",
1109        )),
1110    }?;
1111
1112    let mut address_bytes = [0u8; 43];
1113    reader.read_exact(&mut address_bytes)?;
1114    let recipient = orchard::Address::from_raw_address_bytes(&address_bytes)
1115        .expect("should be a valid address");
1116    let value = orchard::value::NoteValue::from_raw(reader.read_u64::<LittleEndian>()?);
1117    let mut rho_bytes = [0u8; 32];
1118    reader.read_exact(&mut rho_bytes)?;
1119    let rho = orchard::note::Rho::from_bytes(&rho_bytes).expect("should be valid rho bytes");
1120    let mut rseed_bytes = [0u8; 32];
1121    reader.read_exact(&mut rseed_bytes)?;
1122    let rseed = orchard::note::RandomSeed::from_bytes(rseed_bytes, &rho)
1123        .expect("should be valid random seed bytes");
1124
1125    let mut memo_bytes = [0u8; 512];
1126    reader.read_exact(&mut memo_bytes)?;
1127    let memo = Memo::from_bytes(&memo_bytes).map_err(|e| {
1128        std::io::Error::new(
1129            std::io::ErrorKind::InvalidData,
1130            format!("failed to read memo. {e}"),
1131        )
1132    })?;
1133
1134    let recipient_unified_address = Optional::read(&mut reader, |r| {
1135        let encoded_address = read_string(r)?;
1136
1137        decode_unified_address(consensus_parameters, &encoded_address)
1138    })?;
1139
1140    Ok(OutgoingNote {
1141        output_id: OutputId::new(txid, output_index),
1142        key_id: KeyId::from_parts(account_id, scope),
1143        note: orchard::note::Note::from_parts(recipient, value, rho, rseed, note_version)
1144            .expect("should be a valid orchard note"),
1145        memo,
1146        recipient_full_unified_address: recipient_unified_address,
1147        marker: std::marker::PhantomData,
1148    })
1149}
1150
1151/// Shared writer for the Orchard-protocol outgoing note layout.
1152fn write_orchard_protocol_outgoing_note<W: Write, P>(
1153    note: &OutgoingNote<orchard::Note, P>,
1154    mut writer: W,
1155    consensus_parameters: &impl consensus::Parameters,
1156) -> std::io::Result<()> {
1157    writer.write_u8(OutgoingNote::<orchard::Note, P>::serialized_version())?;
1158
1159    note.output_id.txid().write(&mut writer)?;
1160    writer.write_u32::<LittleEndian>(note.output_id.output_index())?;
1161
1162    writer.write_u32::<LittleEndian>(note.key_id.account_id.into())?;
1163    writer.write_u8(note.key_id.scope as u8)?;
1164
1165    writer.write_all(&note.note.recipient().to_raw_address_bytes())?;
1166    writer.write_u64::<LittleEndian>(note.note.value().inner())?;
1167    writer.write_all(&note.note.rho().to_bytes())?;
1168    writer.write_all(note.note.rseed().as_bytes())?;
1169
1170    writer.write_all(note.memo.encode().as_array())?;
1171    Optional::write(
1172        &mut writer,
1173        note.recipient_full_unified_address.as_ref(),
1174        |w, unified_address| {
1175            write_string(
1176                w,
1177                &unified_address.encode_receiver_preserving(consensus_parameters),
1178            )
1179        },
1180    )?;
1181
1182    Ok(())
1183}
1184
1185impl OutgoingOrchardNote {
1186    /// Deserialize into `reader`
1187    pub fn read<R: Read>(
1188        reader: R,
1189        consensus_parameters: &impl consensus::Parameters,
1190    ) -> std::io::Result<Self> {
1191        read_orchard_protocol_outgoing_note(
1192            reader,
1193            consensus_parameters,
1194            orchard::note::NoteVersion::V2,
1195        )
1196    }
1197
1198    /// Serialize into `writer`
1199    pub fn write<W: Write>(
1200        &self,
1201        writer: W,
1202        consensus_parameters: &impl consensus::Parameters,
1203    ) -> std::io::Result<()> {
1204        write_orchard_protocol_outgoing_note(self, writer, consensus_parameters)
1205    }
1206}
1207
1208impl OutgoingIronwoodNote {
1209    /// Deserialize into `reader`
1210    pub fn read<R: Read>(
1211        reader: R,
1212        consensus_parameters: &impl consensus::Parameters,
1213    ) -> std::io::Result<Self> {
1214        read_orchard_protocol_outgoing_note(
1215            reader,
1216            consensus_parameters,
1217            orchard::note::NoteVersion::V3,
1218        )
1219    }
1220
1221    /// Serialize into `writer`
1222    pub fn write<W: Write>(
1223        &self,
1224        writer: W,
1225        consensus_parameters: &impl consensus::Parameters,
1226    ) -> std::io::Result<()> {
1227        write_orchard_protocol_outgoing_note(self, writer, consensus_parameters)
1228    }
1229}
1230
1231impl ShardTrees {
1232    fn serialized_version() -> u8 {
1233        // Version 1 appends the Ironwood shard tree after the Orchard one.
1234        1
1235    }
1236
1237    /// Deserialize into `reader`
1238    pub fn read<R: Read>(mut reader: R) -> std::io::Result<Self> {
1239        let version = read_version(&mut reader, "ShardTrees", Self::serialized_version())?;
1240        let sapling = Self::read_shardtree(&mut reader)?;
1241        let orchard = Self::read_shardtree(&mut reader)?;
1242        let ironwood = if version >= 1 {
1243            Self::read_shardtree(&mut reader)?
1244        } else {
1245            // Pre-Ironwood wallet files: start with an empty Ironwood tree.
1246            Self::new().ironwood
1247        };
1248
1249        Ok(Self {
1250            sapling,
1251            orchard,
1252            ironwood,
1253        })
1254    }
1255
1256    /// Serialize into `writer`
1257    pub fn write<W: Write>(&mut self, mut writer: W) -> std::io::Result<()> {
1258        writer.write_u8(Self::serialized_version())?;
1259        Self::write_shardtree(&mut writer, &mut self.sapling)?;
1260        Self::write_shardtree(&mut writer, &mut self.orchard)?;
1261        Self::write_shardtree(&mut writer, &mut self.ironwood)?;
1262
1263        Ok(())
1264    }
1265
1266    fn read_shardtree<
1267        H: Hashable + Clone + HashSer + Eq,
1268        C: Ord + std::fmt::Debug + Copy + From<u32>,
1269        R: Read,
1270        const DEPTH: u8,
1271        const SHARD_HEIGHT: u8,
1272    >(
1273        mut reader: R,
1274    ) -> std::io::Result<ShardTree<MemoryShardStore<H, C>, DEPTH, SHARD_HEIGHT>> {
1275        let shards = Vector::read(&mut reader, |r| {
1276            let level = incrementalmerkletree::Level::from(r.read_u8()?);
1277            let index = r.read_u64::<LittleEndian>()?;
1278            let root_addr = incrementalmerkletree::Address::from_parts(level, index);
1279            let shard = read_shard(r)?;
1280
1281            LocatedPrunableTree::from_parts(root_addr, shard).map_err(|addr| {
1282                std::io::Error::new(
1283                    std::io::ErrorKind::InvalidData,
1284                    format!("parent node in root has level 0 relative to root address: {addr:?}"),
1285                )
1286            })
1287        })?;
1288        let mut store = MemoryShardStore::empty();
1289        for shard in shards {
1290            store.put_shard(shard).expect("infallible");
1291        }
1292        let checkpoints = Vector::read(&mut reader, |r| {
1293            let checkpoint_id = C::from(r.read_u32::<LittleEndian>()?);
1294            let tree_state = match r.read_u8()? {
1295                0 => TreeState::Empty,
1296                1 => TreeState::AtPosition(Position::from(r.read_u64::<LittleEndian>()?)),
1297                otherwise => {
1298                    return Err(std::io::Error::new(
1299                        std::io::ErrorKind::InvalidData,
1300                        format!(
1301                            "failed to read TreeState. expected boolean value, found {otherwise}"
1302                        ),
1303                    ));
1304                }
1305            };
1306            let marks_removed =
1307                Vector::read(r, |r| r.read_u64::<LittleEndian>().map(Position::from))?;
1308            Ok((
1309                checkpoint_id,
1310                Checkpoint::from_parts(tree_state, marks_removed.into_iter().collect()),
1311            ))
1312        })?;
1313        if checkpoints.is_empty() {
1314            store
1315                .add_checkpoint(C::from(0), Checkpoint::tree_empty())
1316                .expect("Infallible");
1317        }
1318        for (checkpoint_id, checkpoint) in checkpoints {
1319            store
1320                .add_checkpoint(checkpoint_id, checkpoint)
1321                .expect("Infallible");
1322        }
1323        store.put_cap(read_shard(reader)?).expect("Infallible");
1324
1325        Ok(shardtree::ShardTree::new(
1326            store,
1327            SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1328        ))
1329    }
1330
1331    /// Write memory-backed shardstore, represented tree.
1332    fn write_shardtree<
1333        H: Hashable + Clone + Eq + HashSer,
1334        C: Ord + std::fmt::Debug + Copy,
1335        W: Write,
1336        const DEPTH: u8,
1337        const SHARD_HEIGHT: u8,
1338    >(
1339        mut writer: W,
1340        shardtree: &mut ShardTree<MemoryShardStore<H, C>, DEPTH, SHARD_HEIGHT>,
1341    ) -> std::io::Result<()>
1342    where
1343        u32: From<C>,
1344    {
1345        fn write_shards<W, H, C>(
1346            mut writer: W,
1347            store: &MemoryShardStore<H, C>,
1348        ) -> std::io::Result<()>
1349        where
1350            H: Hashable + Clone + Eq + HashSer,
1351            C: Ord + std::fmt::Debug + Copy,
1352            W: Write,
1353        {
1354            let roots = store.get_shard_roots().expect("Infallible");
1355            Vector::write(&mut writer, &roots, |w, root| {
1356                w.write_u8(root.level().into())?;
1357                w.write_u64::<LittleEndian>(root.index())?;
1358                let shard = store
1359                    .get_shard(*root)
1360                    .expect("Infallible")
1361                    .expect("cannot find root that shard store claims to have");
1362                write_shard(w, shard.root())
1363            })
1364        }
1365
1366        fn write_checkpoints<W, Cid>(
1367            mut writer: W,
1368            checkpoints: &[(Cid, Checkpoint)],
1369        ) -> std::io::Result<()>
1370        where
1371            W: Write,
1372            Cid: Ord + std::fmt::Debug + Copy,
1373            u32: From<Cid>,
1374        {
1375            Vector::write(
1376                &mut writer,
1377                checkpoints,
1378                |mut w, (checkpoint_id, checkpoint)| {
1379                    w.write_u32::<LittleEndian>(u32::from(*checkpoint_id))?;
1380                    match checkpoint.tree_state() {
1381                        shardtree::store::TreeState::Empty => w.write_u8(0),
1382                        shardtree::store::TreeState::AtPosition(pos) => {
1383                            w.write_u8(1)?;
1384                            w.write_u64::<LittleEndian>(<u64 as From<Position>>::from(pos))
1385                        }
1386                    }?;
1387                    Vector::write(
1388                        &mut w,
1389                        &checkpoint.marks_removed().iter().collect::<Vec<_>>(),
1390                        |w, mark| {
1391                            w.write_u64::<LittleEndian>(<u64 as From<Position>>::from(**mark))
1392                        },
1393                    )
1394                },
1395            )
1396        }
1397
1398        // Replace original tree with empty tree, and mutate new version into store.
1399        let mut store = std::mem::replace(
1400            shardtree,
1401            shardtree::ShardTree::new(MemoryShardStore::empty(), 0),
1402        )
1403        .into_store();
1404
1405        macro_rules! write_with_error_handling {
1406            ($writer: ident, $from: ident) => {
1407                if let Err(e) = $writer(&mut writer, &$from) {
1408                    *shardtree = shardtree::ShardTree::new(
1409                        store,
1410                        SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1411                    );
1412                    return Err(e);
1413                }
1414            };
1415        }
1416
1417        // Write located prunable trees
1418        write_with_error_handling!(write_shards, store);
1419
1420        // Write checkpoints
1421        let mut checkpoints = Vec::new();
1422        let checkpoint_count = store.checkpoint_count().expect("Infallible");
1423        store
1424            .with_checkpoints(checkpoint_count, |checkpoint_id, checkpoint| {
1425                checkpoints.push((*checkpoint_id, checkpoint.clone()));
1426                Ok(())
1427            })
1428            .expect("Infallible");
1429        write_with_error_handling!(write_checkpoints, checkpoints);
1430
1431        // Write cap
1432        let cap = store.get_cap().expect("Infallible");
1433        write_with_error_handling!(write_shard, cap);
1434
1435        *shardtree =
1436            shardtree::ShardTree::new(store, SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize);
1437
1438        Ok(())
1439    }
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444    use crate::{sync::MAX_SHARDTREE_CHECKPOINTS, witness::ANCHOR_RETENTION_INTERVALS};
1445
1446    use super::*;
1447
1448    fn written(scan_target: &ScanTarget) -> Vec<u8> {
1449        let mut bytes = Vec::new();
1450        scan_target.write(&mut bytes).unwrap();
1451        bytes
1452    }
1453
1454    #[test]
1455    fn scan_target_round_trips_with_its_pools_and_origin() {
1456        let txid = TxId::from_bytes([3; 32]);
1457        for scan_target in [
1458            ScanTarget::new(BlockHeight::from_u32(10), None),
1459            ScanTarget::new(BlockHeight::from_u32(10), Some(txid)),
1460            ScanTarget::internal(BlockHeight::from_u32(20), txid)
1461                .with_pool(zcash_protocol::ShieldedPool::Sapling)
1462                .with_pool(zcash_protocol::ShieldedPool::Ironwood),
1463        ] {
1464            let read = ScanTarget::read(written(&scan_target).as_slice()).unwrap();
1465            assert_eq!(read, scan_target);
1466            assert_eq!(read.txid(), scan_target.txid());
1467            assert_eq!(read.pools(), scan_target.pools());
1468            assert_eq!(read.origin(), scan_target.origin());
1469        }
1470    }
1471
1472    // A version 0 target carried a zero txid where no particular transaction was wanted, and a narrow scan area
1473    // flag. It is read as an external target with no pool.
1474    #[test]
1475    fn scan_target_version_zero_is_read_as_external() {
1476        let v0 = |txid: [u8; 32]| {
1477            let mut bytes = vec![0u8];
1478            bytes.write_u32::<LittleEndian>(30).unwrap();
1479            bytes.extend_from_slice(&txid);
1480            bytes.push(1);
1481            bytes
1482        };
1483
1484        let without_transaction = ScanTarget::read(v0([0; 32]).as_slice()).unwrap();
1485        assert_eq!(
1486            without_transaction,
1487            ScanTarget::new(BlockHeight::from_u32(30), None)
1488        );
1489        assert_eq!(without_transaction.txid(), None);
1490        assert_eq!(without_transaction.origin(), ScanTargetOrigin::External);
1491        assert!(without_transaction.pools().is_empty());
1492
1493        let with_transaction = ScanTarget::read(v0([5; 32]).as_slice()).unwrap();
1494        assert_eq!(with_transaction.txid(), Some(TxId::from_bytes([5; 32])));
1495    }
1496
1497    #[test]
1498    fn scan_target_with_an_unknown_pool_or_origin_is_refused() {
1499        let mut unknown_pool = written(&ScanTarget::new(BlockHeight::from_u32(10), None));
1500        *unknown_pool.last_mut().unwrap() = 0;
1501        let pools_index = unknown_pool.len() - 2;
1502        unknown_pool[pools_index] = 1 << 3;
1503        assert_eq!(
1504            ScanTarget::read(unknown_pool.as_slice())
1505                .unwrap_err()
1506                .kind(),
1507            std::io::ErrorKind::InvalidData
1508        );
1509
1510        let mut unknown_origin = written(&ScanTarget::new(BlockHeight::from_u32(10), None));
1511        *unknown_origin.last_mut().unwrap() = 2;
1512        assert_eq!(
1513            ScanTarget::read(unknown_origin.as_slice())
1514                .unwrap_err()
1515                .kind(),
1516            std::io::ErrorKind::InvalidData
1517        );
1518    }
1519
1520    // Helper: build a minimal v3 SyncState byte blob (no ironwood_shard_ranges).
1521    // Format: version(1) | scan_ranges[0] | sapling_shard_ranges[0] |
1522    //         orchard_shard_ranges[0] | scan_targets[0]
1523    fn v3_sync_state_bytes() -> Vec<u8> {
1524        let mut out = Vec::new();
1525        out.write_u8(3).unwrap();
1526        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1527        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1528        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1529        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1530        out
1531    }
1532
1533    #[test]
1534    fn sync_state_v3_reads_with_empty_ironwood_ranges() {
1535        let bytes = v3_sync_state_bytes();
1536        let sync_state = SyncState::read(bytes.as_slice()).expect("v3 should read cleanly");
1537        assert!(sync_state.ironwood_shard_ranges().is_empty());
1538    }
1539
1540    #[test]
1541    fn sync_state_v4_roundtrip_preserves_ironwood_ranges() {
1542        let mut state = SyncState::new();
1543        state.ironwood_shard_ranges = vec![
1544            BlockHeight::from_u32(100)..BlockHeight::from_u32(200),
1545            BlockHeight::from_u32(300)..BlockHeight::from_u32(400),
1546        ];
1547        state.scan_ranges.push(ScanRange::from_parts(
1548            BlockHeight::from_u32(100)..BlockHeight::from_u32(400),
1549            ScanPriority::Historic,
1550        ));
1551        let mut bytes = Vec::new();
1552        state.write(&mut bytes).expect("write should succeed");
1553        let recovered = SyncState::read(bytes.as_slice()).expect("read should succeed");
1554        assert_eq!(recovered.ironwood_shard_ranges, state.ironwood_shard_ranges);
1555        assert_eq!(recovered.scan_ranges, state.scan_ranges);
1556    }
1557
1558    // Helper: build a minimal v4 SyncState byte blob (no transparent scan floor).
1559    // Format: version(1) | scan_ranges[0] | sapling_shard_ranges[0] |
1560    //         orchard_shard_ranges[0] | ironwood_shard_ranges[0] | scan_targets[0]
1561    fn v4_sync_state_bytes() -> Vec<u8> {
1562        let mut out = Vec::new();
1563        out.write_u8(4).unwrap();
1564        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1565        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1566        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1567        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1568        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1569        out
1570    }
1571
1572    #[test]
1573    fn sync_state_v4_reads_with_unset_transparent_scan_floor() {
1574        let bytes = v4_sync_state_bytes();
1575        let sync_state = SyncState::read(bytes.as_slice()).expect("v4 should read cleanly");
1576        assert_eq!(sync_state.transparent_scan_floor, None);
1577    }
1578
1579    #[test]
1580    fn sync_state_v5_roundtrip_preserves_transparent_scan_floor() {
1581        for transparent_scan_floor in [None, Some(BlockHeight::from_u32(100))] {
1582            let mut state = SyncState::new();
1583            state.transparent_scan_floor = transparent_scan_floor;
1584            let mut bytes = Vec::new();
1585            state.write(&mut bytes).expect("write should succeed");
1586            let recovered = SyncState::read(bytes.as_slice()).expect("read should succeed");
1587            assert_eq!(recovered.transparent_scan_floor, transparent_scan_floor);
1588        }
1589    }
1590
1591    mod read_version {
1592        use super::super::read_version;
1593
1594        /// The current version of a made-up type. It follows no type's serialized version.
1595        const ARBITRARY_CURRENT_VERSION: u8 = 2;
1596
1597        #[test]
1598        fn accepts_versions_up_to_the_current_version() {
1599            for version in [0, ARBITRARY_CURRENT_VERSION] {
1600                assert_eq!(
1601                    read_version([version].as_slice(), "Type", ARBITRARY_CURRENT_VERSION).unwrap(),
1602                    version
1603                );
1604            }
1605        }
1606
1607        #[test]
1608        fn refuses_a_version_above_the_current_version() {
1609            let error = read_version(
1610                [ARBITRARY_CURRENT_VERSION + 1].as_slice(),
1611                "Type",
1612                ARBITRARY_CURRENT_VERSION,
1613            )
1614            .expect_err("a version above the current version is refused");
1615            assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
1616        }
1617    }
1618
1619    /// Each reader is given only a serialized version above the one its type writes. A reader that refuses the
1620    /// version returns invalid data. A reader that read on would report the end of the input.
1621    #[test]
1622    fn readers_refuse_serialized_versions_above_their_own() {
1623        let consensus_parameters = zcash_protocol::consensus::MAIN_NETWORK;
1624        let newer_scan_target = [ScanTarget::serialized_version() + 1];
1625        let newer_sync_state = [SyncState::serialized_version() + 1];
1626        let newer_tree_bounds = [TreeBounds::serialized_version() + 1];
1627        let newer_nullifier_map = [NullifierMap::serialized_version() + 1];
1628        let newer_wallet_block = [WalletBlock::serialized_version() + 1];
1629        let newer_wallet_transaction = [WalletTransaction::serialized_version() + 1];
1630        let newer_transparent_coin = [TransparentCoin::serialized_version() + 1];
1631        let newer_wallet_note = [SaplingNote::serialized_version() + 1];
1632        let newer_outgoing_note = [OutgoingSaplingNote::serialized_version() + 1];
1633        let newer_shard_trees = [ShardTrees::serialized_version() + 1];
1634
1635        for (type_name, read) in [
1636            (
1637                "ScanTarget",
1638                ScanTarget::read(newer_scan_target.as_slice()).map(drop),
1639            ),
1640            (
1641                "SyncState",
1642                SyncState::read(newer_sync_state.as_slice()).map(drop),
1643            ),
1644            (
1645                "TreeBounds",
1646                TreeBounds::read(newer_tree_bounds.as_slice()).map(drop),
1647            ),
1648            (
1649                "NullifierMap",
1650                NullifierMap::read(newer_nullifier_map.as_slice()).map(drop),
1651            ),
1652            (
1653                "WalletBlock",
1654                WalletBlock::read(newer_wallet_block.as_slice()).map(drop),
1655            ),
1656            (
1657                "WalletTransaction",
1658                WalletTransaction::read(newer_wallet_transaction.as_slice(), &consensus_parameters)
1659                    .map(drop),
1660            ),
1661            (
1662                "TransparentCoin",
1663                TransparentCoin::read(newer_transparent_coin.as_slice()).map(drop),
1664            ),
1665            (
1666                "SaplingNote",
1667                SaplingNote::read(newer_wallet_note.as_slice()).map(drop),
1668            ),
1669            (
1670                "OrchardNote",
1671                OrchardNote::read(newer_wallet_note.as_slice()).map(drop),
1672            ),
1673            (
1674                "IronwoodNote",
1675                IronwoodNote::read(newer_wallet_note.as_slice()).map(drop),
1676            ),
1677            (
1678                "OutgoingSaplingNote",
1679                OutgoingSaplingNote::read(newer_outgoing_note.as_slice(), &consensus_parameters)
1680                    .map(drop),
1681            ),
1682            (
1683                "OutgoingOrchardNote",
1684                OutgoingOrchardNote::read(newer_outgoing_note.as_slice(), &consensus_parameters)
1685                    .map(drop),
1686            ),
1687            (
1688                "OutgoingIronwoodNote",
1689                OutgoingIronwoodNote::read(newer_outgoing_note.as_slice(), &consensus_parameters)
1690                    .map(drop),
1691            ),
1692            (
1693                "ShardTrees",
1694                ShardTrees::read(newer_shard_trees.as_slice()).map(drop),
1695            ),
1696        ] {
1697            assert_eq!(
1698                read.expect_err(type_name).kind(),
1699                std::io::ErrorKind::InvalidData,
1700                "{type_name}"
1701            );
1702        }
1703    }
1704
1705    // Helper: build a minimal v1 NullifierMap byte blob (no ironwood BTreeMap).
1706    fn v1_nullifier_map_bytes() -> Vec<u8> {
1707        let mut out = Vec::new();
1708        out.write_u8(1).unwrap();
1709        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1710        Vector::write(&mut out, &[] as &[()], |_, _| Ok(())).unwrap();
1711        out
1712    }
1713
1714    #[test]
1715    fn nullifier_map_v1_reads_with_empty_ironwood_map() {
1716        let bytes = v1_nullifier_map_bytes();
1717        let map = NullifierMap::read(bytes.as_slice()).expect("v1 should read cleanly");
1718        assert!(map.ironwood.is_empty());
1719    }
1720
1721    #[test]
1722    fn nullifier_map_v2_roundtrip_preserves_ironwood() {
1723        let map = NullifierMap::new();
1724        let mut bytes = Vec::new();
1725        map.write(&mut bytes).expect("write should succeed");
1726        let recovered = NullifierMap::read(bytes.as_slice()).expect("read should succeed");
1727        assert!(recovered.ironwood.is_empty());
1728    }
1729
1730    // Helper: build a v0 TreeBounds byte blob (no ironwood tree sizes).
1731    fn v0_tree_bounds_bytes(
1732        sapling_initial: u32,
1733        sapling_final: u32,
1734        orchard_initial: u32,
1735        orchard_final: u32,
1736    ) -> Vec<u8> {
1737        let mut out = Vec::new();
1738        out.write_u8(0).unwrap();
1739        out.write_u32::<LittleEndian>(sapling_initial).unwrap();
1740        out.write_u32::<LittleEndian>(sapling_final).unwrap();
1741        out.write_u32::<LittleEndian>(orchard_initial).unwrap();
1742        out.write_u32::<LittleEndian>(orchard_final).unwrap();
1743        out
1744    }
1745
1746    #[test]
1747    fn tree_bounds_v0_reads_with_zero_ironwood_sizes() {
1748        let bytes = v0_tree_bounds_bytes(10, 20, 30, 40);
1749        let bounds = TreeBounds::read(bytes.as_slice()).expect("v0 should read cleanly");
1750        assert_eq!(bounds.sapling_initial_tree_size, 10);
1751        assert_eq!(bounds.orchard_final_tree_size, 40);
1752        assert_eq!(bounds.ironwood_initial_tree_size, 0);
1753        assert_eq!(bounds.ironwood_final_tree_size, 0);
1754    }
1755
1756    #[test]
1757    fn tree_bounds_v1_roundtrip() {
1758        let bounds = TreeBounds {
1759            sapling_initial_tree_size: 1,
1760            sapling_final_tree_size: 2,
1761            orchard_initial_tree_size: 3,
1762            orchard_final_tree_size: 4,
1763            ironwood_initial_tree_size: 5,
1764            ironwood_final_tree_size: 6,
1765        };
1766        let mut bytes = Vec::new();
1767        bounds.write(&mut bytes).expect("write should succeed");
1768        let recovered = TreeBounds::read(bytes.as_slice()).expect("read should succeed");
1769        assert_eq!(recovered.ironwood_initial_tree_size, 5);
1770        assert_eq!(recovered.ironwood_final_tree_size, 6);
1771    }
1772
1773    /// The checkpoint set of a synced wallet decomposes into exactly two parts:
1774    /// [`SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE`] rolling checkpoints, which serve ordinary reorg handling and
1775    /// near-tip spends, plus [`ANCHOR_RETENTION_INTERVALS`] pinned ZIP 318 grid boundaries,
1776    /// which serve pool crossings.
1777    ///
1778    /// The two parts are disjoint and independently bounded: pinning a boundary must not
1779    /// consume a rolling slot (that would shrink the reorg window), and the rolling budget must
1780    /// not displace a boundary (that would break crossings).
1781    #[test]
1782    fn checkpoint_set_is_reorg_window_plus_pinned_boundaries() {
1783        use crate::shardtree_ext::ShardTreeExt as _;
1784        use crate::witness::{anchor_retention_window, repin_anchor_checkpoints};
1785        use zcash_client_backend::data_api::anchor_retention::{
1786            AnchorRetention, AnchorRetentionInterval,
1787        };
1788
1789        const TIP: u32 = 100_000;
1790        const INTERVAL: u32 = 144;
1791        let policy = AnchorRetention::new(
1792            BlockHeight::from_u32(90_000),
1793            AnchorRetentionInterval::default(),
1794        );
1795        let mut shard_trees = ShardTrees::new();
1796
1797        for height in (TIP - 2000)..=TIP {
1798            let height = BlockHeight::from_u32(height);
1799            let window = anchor_retention_window(&policy, height);
1800            repin_anchor_checkpoints(&policy, &window, shard_trees.orchard.store_mut());
1801            shard_trees
1802                .orchard
1803                .append_checkpoint(height)
1804                .expect("infallible");
1805        }
1806
1807        let store = shard_trees.orchard.store();
1808        let total = store.checkpoint_count().expect("infallible");
1809        let pinned_ids = store.retained_checkpoints().expect("infallible");
1810        let mut pinned = Vec::new();
1811        let mut rolling = Vec::new();
1812        store
1813            .for_each_checkpoint(total, |id, _| {
1814                if pinned_ids.contains(id) {
1815                    pinned.push(u32::from(*id));
1816                } else {
1817                    rolling.push(u32::from(*id));
1818                }
1819                Ok(())
1820            })
1821            .expect("infallible");
1822
1823        assert!(
1824            pinned.iter().all(|height| height % INTERVAL == 0),
1825            "every pinned checkpoint must be a grid boundary, got {pinned:?}"
1826        );
1827        assert_eq!(rolling.last().copied(), Some(TIP));
1828        assert_eq!(
1829            (rolling.len(), pinned.len(), total),
1830            (
1831                SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1832                ANCHOR_RETENTION_INTERVALS as usize,
1833                MAX_SHARDTREE_CHECKPOINTS as usize,
1834            ),
1835            "(rolling, pinned, total): the pinned boundaries must not be part of the \
1836             SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE total"
1837        );
1838
1839        for height in (TIP - SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE + 1)..=TIP {
1840            assert!(
1841                store
1842                    .get_checkpoint(&BlockHeight::from_u32(height))
1843                    .expect("infallible")
1844                    .is_some(),
1845                "reorg window is missing height {height}"
1846            );
1847        }
1848
1849        assert!(
1850            rolling
1851                .iter()
1852                .all(|height| *height >= TIP - SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE),
1853            "a rolling checkpoint survived below the reorg window: {rolling:?}"
1854        );
1855
1856        let mut bytes = Vec::new();
1857        shard_trees.write(&mut bytes).expect("write should succeed");
1858        let reloaded = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
1859        let reloaded_store = reloaded.orchard.store();
1860        for boundary in &pinned {
1861            assert!(
1862                reloaded_store
1863                    .get_checkpoint(&BlockHeight::from_u32(*boundary))
1864                    .expect("infallible")
1865                    .is_some(),
1866                "boundary {boundary} lost on reload; a crossing anchored there cannot be built"
1867            );
1868        }
1869        assert_eq!(
1870            reloaded_store.checkpoint_count().expect("infallible"),
1871            MAX_SHARDTREE_CHECKPOINTS as usize
1872        );
1873    }
1874
1875    /// Serialization preserves the checkpoints pruning left instead of imposing a cap.
1876    #[test]
1877    fn shardtree_roundtrip_keeps_newest_checkpoints() {
1878        use crate::shardtree_ext::ShardTreeExt as _;
1879
1880        let mut shard_trees = ShardTrees::new();
1881
1882        for height in 1..=150 {
1883            let height = BlockHeight::from_u32(height);
1884            shard_trees
1885                .sapling
1886                .append_checkpoint(height)
1887                .expect("infallible");
1888            shard_trees
1889                .orchard
1890                .append_checkpoint(height)
1891                .expect("infallible");
1892        }
1893
1894        let mut bytes = Vec::new();
1895        shard_trees.write(&mut bytes).expect("write should succeed");
1896        let roundtripped = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
1897
1898        let sapling_store = roundtripped.sapling.store();
1899        let orchard_store = roundtripped.orchard.store();
1900
1901        let oldest_kept = BlockHeight::from_u32(150 - SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE + 1);
1902        fn assert_window<S>(store: &S, oldest_kept: BlockHeight)
1903        where
1904            S: ShardStore<CheckpointId = BlockHeight, Error = std::convert::Infallible>,
1905        {
1906            assert_eq!(
1907                store.checkpoint_count().expect("infallible"),
1908                SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize
1909            );
1910            assert_eq!(
1911                store.min_checkpoint_id().expect("infallible"),
1912                Some(oldest_kept)
1913            );
1914            assert_eq!(
1915                store.max_checkpoint_id().expect("infallible"),
1916                Some(BlockHeight::from_u32(150))
1917            );
1918            assert!(
1919                store
1920                    .get_checkpoint(&(oldest_kept - 1))
1921                    .expect("infallible")
1922                    .is_none()
1923            );
1924        }
1925        assert_window(sapling_store, oldest_kept);
1926        assert_window(orchard_store, oldest_kept);
1927    }
1928
1929    /// A blob with no checkpoint reads back with the height-zero checkpoint.
1930    #[test]
1931    fn shardtree_read_adds_initialization_checkpoint_when_blob_has_none() {
1932        fn checkpoint_less<H, const DEPTH: u8, const SHARD_HEIGHT: u8>()
1933        -> ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>
1934        where
1935            H: Hashable + Clone + PartialEq,
1936        {
1937            ShardTree::new(
1938                MemoryShardStore::empty(),
1939                SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
1940            )
1941        }
1942
1943        let mut shard_trees = ShardTrees {
1944            sapling: checkpoint_less(),
1945            orchard: checkpoint_less(),
1946            ironwood: checkpoint_less(),
1947        };
1948        assert_eq!(
1949            shard_trees
1950                .sapling
1951                .store()
1952                .max_checkpoint_id()
1953                .expect("infallible"),
1954            None
1955        );
1956
1957        let mut bytes = Vec::new();
1958        shard_trees.write(&mut bytes).expect("write should succeed");
1959        let roundtripped = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
1960
1961        fn assert_initialization_checkpoint<S>(store: &S)
1962        where
1963            S: ShardStore<CheckpointId = BlockHeight, Error = std::convert::Infallible>,
1964        {
1965            assert_eq!(store.checkpoint_count().expect("infallible"), 1);
1966            assert_eq!(
1967                store.max_checkpoint_id().expect("infallible"),
1968                Some(BlockHeight::from_u32(0))
1969            );
1970        }
1971        assert_initialization_checkpoint(roundtripped.sapling.store());
1972        assert_initialization_checkpoint(roundtripped.orchard.store());
1973        assert_initialization_checkpoint(roundtripped.ironwood.store());
1974    }
1975
1976    /// A pinned anchor checkpoint survives serialization even once it has aged out of the
1977    /// rolling window. The pinned set itself is not persisted, being re-derived at the start of
1978    /// every sync.
1979    #[test]
1980    fn shardtree_roundtrip_keeps_pinned_anchor_checkpoints() {
1981        use crate::shardtree_ext::ShardTreeExt as _;
1982
1983        let pinned = BlockHeight::from_u32(24);
1984        let mut shard_trees = ShardTrees::new();
1985
1986        shard_trees
1987            .sapling
1988            .store_mut()
1989            .add_retained_checkpoint(pinned)
1990            .expect("infallible");
1991        for height in 1..=150 {
1992            shard_trees
1993                .sapling
1994                .append_checkpoint(BlockHeight::from_u32(height))
1995                .expect("infallible");
1996        }
1997
1998        assert!(
1999            shard_trees
2000                .sapling
2001                .store()
2002                .get_checkpoint(&pinned)
2003                .expect("infallible")
2004                .is_some(),
2005            "pruning must not evict a pinned checkpoint"
2006        );
2007
2008        let mut bytes = Vec::new();
2009        shard_trees.write(&mut bytes).expect("write should succeed");
2010        let roundtripped = ShardTrees::read(bytes.as_slice()).expect("read should succeed");
2011
2012        let sapling_store = roundtripped.sapling.store();
2013        assert!(
2014            sapling_store
2015                .get_checkpoint(&pinned)
2016                .expect("infallible")
2017                .is_some(),
2018            "serialization must not evict a pinned checkpoint"
2019        );
2020        assert_eq!(
2021            sapling_store.checkpoint_count().expect("infallible"),
2022            SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize + 1
2023        );
2024        assert!(
2025            sapling_store
2026                .retained_checkpoints()
2027                .expect("infallible")
2028                .is_empty()
2029        );
2030    }
2031}