Skip to main content

nodedb_types/backup_envelope/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Shared types, constants, and error definitions for the backup envelope.
4
5use thiserror::Error;
6
7pub const MAGIC: &[u8; 4] = b"NDBB";
8
9/// Backup envelope version stamped in byte 4 of every envelope header.
10/// Both plaintext and encrypted envelopes use version 1; the presence
11/// of the crypto block (68 bytes after the header) distinguishes them.
12pub const VERSION: u8 = 1;
13
14/// Header is fixed-size — 52 bytes (48 framed + 4 crc).
15///
16/// The header grew by 8 bytes when `tenant_id` was widened from u32 to u64
17/// (format v1, pre-launch format break).
18pub const HEADER_LEN: usize = 52;
19/// Per-section framing overhead: origin(8) + len(4) + crc(4).
20pub const SECTION_OVERHEAD: usize = 16;
21/// Trailing crc.
22pub const TRAILER_LEN: usize = 4;
23
24/// Default cap on total envelope size: 16 GiB. Tunable per call.
25pub const DEFAULT_MAX_TOTAL_BYTES: u64 = 16 * 1024 * 1024 * 1024;
26/// Default cap on a single section body: 16 GiB.
27pub const DEFAULT_MAX_SECTION_BYTES: u64 = 16 * 1024 * 1024 * 1024;
28
29/// Sentinel `origin_node_id` values that mark sections carrying
30/// metadata rather than per-node engine data. Restore handlers
31/// recognize the sentinel and route the body to the correct
32/// catalog writer. Section CRCs validate independently of whether
33/// the reader acts on the section body.
34pub const SECTION_ORIGIN_CATALOG_ROWS: u64 = 0xFFFF_FFFF_FFFF_FFF0;
35pub const SECTION_ORIGIN_SOURCE_TOMBSTONES: u64 = 0xFFFF_FFFF_FFFF_FFF1;
36
37/// Single catalog-row entry in a catalog-rows section. The outer
38/// container is `Vec<StoredCollectionBlob>` msgpack-encoded into the
39/// section body. Bytes are the zerompk-encoded `StoredCollection`
40/// from the `nodedb` crate — `nodedb-types` intentionally doesn't
41/// depend on the `nodedb` catalog types, so the blob is opaque here.
42#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
43pub struct StoredCollectionBlob {
44    pub name: String,
45    /// zerompk-encoded `StoredCollection`.
46    pub bytes: Vec<u8>,
47}
48
49/// Single source-side tombstone entry. `purge_lsn` is the Origin WAL
50/// LSN at which the hard-delete committed — restore uses it as a
51/// per-collection replay barrier so rows older than the purge don't
52/// resurrect.
53#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
54pub struct SourceTombstoneEntry {
55    pub collection: String,
56    pub purge_lsn: u64,
57}
58
59#[derive(Debug, Error, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum EnvelopeError {
62    #[error("invalid backup format")]
63    BadMagic,
64    #[error("unsupported backup version: {0}")]
65    UnsupportedVersion(u8),
66    #[error("invalid backup format")]
67    HeaderCrcMismatch,
68    #[error("invalid backup format")]
69    BodyCrcMismatch,
70    #[error("invalid backup format")]
71    TrailerCrcMismatch,
72    #[error("backup truncated")]
73    Truncated,
74    #[error("backup tenant mismatch: expected {expected}, got {actual}")]
75    TenantMismatch { expected: u64, actual: u64 },
76    #[error("backup exceeds size cap of {cap} bytes")]
77    OverSizeTotal { cap: u64 },
78    #[error("backup section exceeds size cap of {cap} bytes")]
79    OverSizeSection { cap: u64 },
80    #[error("too many sections: {0}")]
81    TooManySections(u16),
82    /// The KEK presented at restore time does not match the KEK fingerprint
83    /// embedded in the envelope. Surfaces before any decryption attempt so
84    /// the caller receives a clear, actionable error rather than an opaque
85    /// authentication failure.
86    #[error("wrong backup KEK: presented key fingerprint does not match envelope")]
87    WrongBackupKek,
88    /// AES-256-GCM authentication tag verification failed. Either the
89    /// ciphertext or the key is corrupt.
90    #[error("backup decryption failed: authentication tag mismatch")]
91    DecryptionFailed,
92    /// AES-256-GCM encryption failed (e.g. plaintext exceeds the per-message
93    /// limit of 2^36 - 31 bytes). Distinct from `DecryptionFailed` so callers
94    /// can tell which side of the crypto pipeline produced the error.
95    #[error("backup encryption failed")]
96    EncryptionFailed,
97    /// `getrandom` returned an error when generating nonces or the DEK.
98    #[error("backup encryption failed: {0}")]
99    RandomFailure(String),
100}
101
102/// Header metadata captured at backup time.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct EnvelopeMeta {
105    pub tenant_id: u64,
106    pub source_vshard_count: u16,
107    pub hash_seed: u64,
108    pub snapshot_watermark: u64,
109}
110
111/// One contiguous body produced by one origin node.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct Section {
114    pub origin_node_id: u64,
115    pub body: Vec<u8>,
116}
117
118/// Decoded envelope.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct Envelope {
121    pub meta: EnvelopeMeta,
122    pub sections: Vec<Section>,
123}
124
125// ── byte helpers ─────────────────────────────────────────────────────────────
126
127pub fn read2(s: &[u8]) -> [u8; 2] {
128    [s[0], s[1]]
129}
130pub fn read4(s: &[u8]) -> [u8; 4] {
131    [s[0], s[1], s[2], s[3]]
132}
133pub fn read8(s: &[u8]) -> [u8; 8] {
134    [s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]
135}