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/// Section carrying the tenant's PK→surrogate identity bindings. The surrogate
37/// map is DATA-derived per-node state that the per-node data sections do NOT
38/// carry (the Data-Plane snapshot handler has no catalog access), so without
39/// this section a restored node has documents but cannot resolve PK
40/// point-lookups (`WHERE id=<pk>`). The body is a msgpack-encoded
41/// `Vec<SurrogateBindBlob>`.
42pub const SECTION_ORIGIN_SURROGATE_PK: u64 = 0xFFFF_FFFF_FFFF_FFF2;
43
44/// Single catalog-row entry in a catalog-rows section. The outer
45/// container is `Vec<StoredCollectionBlob>` msgpack-encoded into the
46/// section body. Bytes are the zerompk-encoded `StoredCollection`
47/// from the `nodedb` crate — `nodedb-types` intentionally doesn't
48/// depend on the `nodedb` catalog types, so the blob is opaque here.
49#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
50pub struct StoredCollectionBlob {
51 pub name: String,
52 /// zerompk-encoded `StoredCollection`.
53 pub bytes: Vec<u8>,
54}
55
56/// Single source-side tombstone entry. `purge_lsn` is the Origin WAL
57/// LSN at which the hard-delete committed — restore uses it as a
58/// per-collection replay barrier so rows older than the purge don't
59/// resurrect.
60#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
61pub struct SourceTombstoneEntry {
62 pub collection: String,
63 pub purge_lsn: u64,
64}
65
66/// Single PK→surrogate binding carried in a `SECTION_ORIGIN_SURROGATE_PK`
67/// section. The outer container is `Vec<SurrogateBindBlob>` msgpack-encoded into
68/// the section body. Mirrors one row of the source catalog's `surrogate_pk_v3`
69/// table for one `(tenant_id, collection)`; rebound on the restore side so PK
70/// point-lookups resolve.
71#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
72pub struct SurrogateBindBlob {
73 pub tenant_id: u64,
74 pub collection: String,
75 pub pk: Vec<u8>,
76 pub surrogate: u32,
77}
78
79#[derive(Debug, Error, PartialEq, Eq)]
80#[non_exhaustive]
81pub enum EnvelopeError {
82 #[error("invalid backup format")]
83 BadMagic,
84 #[error("unsupported backup version: {0}")]
85 UnsupportedVersion(u8),
86 #[error("invalid backup format")]
87 HeaderCrcMismatch,
88 #[error("invalid backup format")]
89 BodyCrcMismatch,
90 #[error("invalid backup format")]
91 TrailerCrcMismatch,
92 #[error("backup truncated")]
93 Truncated,
94 #[error("backup tenant mismatch: expected {expected}, got {actual}")]
95 TenantMismatch { expected: u64, actual: u64 },
96 #[error("backup exceeds size cap of {cap} bytes")]
97 OverSizeTotal { cap: u64 },
98 #[error("backup section exceeds size cap of {cap} bytes")]
99 OverSizeSection { cap: u64 },
100 #[error("too many sections: {0}")]
101 TooManySections(u16),
102 /// The KEK presented at restore time does not match the KEK fingerprint
103 /// embedded in the envelope. Surfaces before any decryption attempt so
104 /// the caller receives a clear, actionable error rather than an opaque
105 /// authentication failure.
106 #[error("wrong backup KEK: presented key fingerprint does not match envelope")]
107 WrongBackupKek,
108 /// AES-256-GCM authentication tag verification failed. Either the
109 /// ciphertext or the key is corrupt.
110 #[error("backup decryption failed: authentication tag mismatch")]
111 DecryptionFailed,
112 /// AES-256-GCM encryption failed (e.g. plaintext exceeds the per-message
113 /// limit of 2^36 - 31 bytes). Distinct from `DecryptionFailed` so callers
114 /// can tell which side of the crypto pipeline produced the error.
115 #[error("backup encryption failed")]
116 EncryptionFailed,
117 /// `getrandom` returned an error when generating nonces or the DEK.
118 #[error("backup encryption failed: {0}")]
119 RandomFailure(String),
120}
121
122/// Header metadata captured at backup time.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct EnvelopeMeta {
125 pub tenant_id: u64,
126 pub source_vshard_count: u16,
127 pub hash_seed: u64,
128 pub snapshot_watermark: u64,
129}
130
131/// One contiguous body produced by one origin node.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct Section {
134 pub origin_node_id: u64,
135 pub body: Vec<u8>,
136}
137
138/// Decoded envelope.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct Envelope {
141 pub meta: EnvelopeMeta,
142 pub sections: Vec<Section>,
143}
144
145// ── byte helpers ─────────────────────────────────────────────────────────────
146
147pub fn read2(s: &[u8]) -> [u8; 2] {
148 [s[0], s[1]]
149}
150pub fn read4(s: &[u8]) -> [u8; 4] {
151 [s[0], s[1], s[2], s[3]]
152}
153pub fn read8(s: &[u8]) -> [u8; 8] {
154 [s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]
155}