md_codec/error.rs
1//! Error variants for the md-codec wire-format codec.
2
3use thiserror::Error;
4
5/// Operator-context kind — where in the descriptor tree an operator appears.
6/// Per SPEC v0.30 §11. Carried by [`Error::OperatorContextViolation`] to name
7/// which tree-position a forbidden tag was encountered in.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ContextKind {
10 /// Top-level descriptor position (e.g., bare `PkK` as the descriptor root).
11 TopLevel,
12 /// Inside a `tr()` tap-script leaf (BIP-342 tapscript-only operators).
13 TapLeaf,
14 /// Inside a multi-family body (non-key tag among multi children).
15 MultiBody,
16}
17
18/// Errors produced by md-codec wire-format components.
19#[derive(Debug, Error, PartialEq, Eq)]
20pub enum Error {
21 /// A read of `requested` bits was attempted but only `available` bits remained.
22 #[error("attempted to read {requested} bits with only {available} bits remaining")]
23 BitStreamTruncated {
24 /// Number of bits the caller requested.
25 requested: usize,
26 /// Number of bits actually available in the stream.
27 available: usize,
28 },
29
30 /// Wire-format version field doesn't match v0.30 (=4). Returned when a
31 /// payload or chunk-header is read with a version value outside the
32 /// accepted v0.30 set. Per SPEC v0.30 §2.4 + §2.5 + §11.1.
33 #[error("wire-format version mismatch: got {got}, expected 4")]
34 WireVersionMismatch {
35 /// Version value parsed from the wire.
36 got: u8,
37 },
38
39 /// Header malformed in a way other than version mismatch — e.g., chunked-
40 /// flag inconsistent with caller context, or chunk-header internal field
41 /// out of range. Per SPEC v0.30 §11.1.
42 #[error("malformed header: {detail}")]
43 MalformedHeader {
44 /// Free-form description of the malformedness.
45 detail: String,
46 },
47
48 /// Path depth exceeds MAX_PATH_COMPONENTS (15).
49 #[error("path depth {got} exceeds maximum {max}")]
50 PathDepthExceeded {
51 /// Actual depth of the path.
52 got: usize,
53 /// Maximum allowed depth (15).
54 max: usize,
55 },
56
57 /// Key count `n` out of range. Per SPEC v0.30 §4: `1 ≤ n ≤ 32`.
58 #[error("key count {n} out of range; require 1 ≤ n ≤ 32")]
59 KeyCountOutOfRange {
60 /// Actual key count provided.
61 n: u8,
62 },
63
64 /// Divergent path count doesn't match key count.
65 #[error("divergent path count {got} does not match key count {n}")]
66 DivergentPathCountMismatch {
67 /// Expected key count.
68 n: u8,
69 /// Actual path count provided.
70 got: usize,
71 },
72
73 /// Multipath alt-count out of range. Per SPEC v0.30 §8: `2 ≤ count ≤ 9`.
74 #[error("multipath alt-count {got} out of range; require 2 ≤ count ≤ 9")]
75 AltCountOutOfRange {
76 /// Provided alt-count.
77 got: usize,
78 },
79
80 /// Tag value outside the allocated v0.30 set: 6-bit primary in reserved
81 /// range 0x24..=0x3E, or extension prefix 0x3F followed by an unrecognized
82 /// 4-bit subcode 0x00..=0x0F (the entire extension subspace is reserved
83 /// in v0.30). `primary` carries the raw 6-bit value read off the wire
84 /// (0x3F for extension-subspace failures); the 4-bit subcode is consumed
85 /// but not reported. Per SPEC v0.30 §3.2 and §11.1.
86 #[error("tag value 0x{primary:02x} out of range")]
87 TagOutOfRange {
88 /// The raw 6-bit primary value read off the wire.
89 primary: u8,
90 },
91
92 /// Threshold `k` out of range. Per SPEC v0.30 §4: `1 ≤ k ≤ 32`.
93 #[error("threshold k={k} out of range; require 1 ≤ k ≤ 32")]
94 ThresholdOutOfRange {
95 /// Provided k value.
96 k: u8,
97 },
98
99 /// Variable-arity child count out of range. Per SPEC v0.30 §4: `1 ≤ count ≤ 32`.
100 #[error("child count {count} out of range; require 1 ≤ count ≤ 32")]
101 ChildCountOutOfRange {
102 /// Provided child count.
103 count: usize,
104 },
105
106 /// k > n in k-of-n threshold/multisig.
107 #[error("threshold k={k} exceeds child count n={n}; require k ≤ n")]
108 KGreaterThanN {
109 /// Threshold k.
110 k: u8,
111 /// Child count n.
112 n: usize,
113 },
114
115 /// TLV ordering violation: a TLV tag was followed by a smaller-or-equal tag.
116 #[error(
117 "TLV ordering violation: tag 0x{prev:02x} followed by 0x{current:02x}; require ascending"
118 )]
119 TlvOrderingViolation {
120 /// Previous tag value.
121 prev: u8,
122 /// Current tag value.
123 current: u8,
124 },
125
126 /// Placeholder index in TLV entry exceeds key count n.
127 #[error("placeholder index {idx} out of range; require idx < n={n}")]
128 PlaceholderIndexOutOfRange {
129 /// Provided index.
130 idx: u8,
131 /// Key count n.
132 n: u8,
133 },
134
135 /// Per-`@N` override entries within a TLV must be in ascending `@N`-index order.
136 #[error("override ordering violation: @{prev} followed by @{current}; require ascending")]
137 OverrideOrderViolation {
138 /// Previous index.
139 prev: u8,
140 /// Current index.
141 current: u8,
142 },
143
144 /// TLV entry has zero entries; encoder MUST omit empty TLVs per spec §7.5.
145 #[error("TLV entry tag 0x{tag:02x} has empty payload; encoder MUST omit empty TLVs")]
146 EmptyTlvEntry {
147 /// Tag of the empty entry.
148 tag: u8,
149 },
150
151 /// TLV length exceeds remaining bits in stream.
152 #[error("TLV length {length} exceeds remaining bits {remaining}")]
153 TlvLengthExceedsRemaining {
154 /// Declared length.
155 length: usize,
156 /// Available bits.
157 remaining: usize,
158 },
159
160 /// Placeholder @i was not referenced anywhere in the tree (BIP 388 well-formedness).
161 #[error("placeholder @{idx} not referenced in tree; n={n}")]
162 PlaceholderNotReferenced {
163 /// The unreferenced placeholder index.
164 idx: u8,
165 /// Key count.
166 n: u8,
167 },
168
169 /// First-occurrence ordering violated (BIP 388 well-formedness).
170 #[error(
171 "placeholder first-occurrence ordering violated: expected first={expected_first}, got first={got_first}"
172 )]
173 PlaceholderFirstOccurrenceOutOfOrder {
174 /// Expected placeholder index in canonical first-occurrence position.
175 expected_first: u8,
176 /// Actual placeholder index encountered first.
177 got_first: u8,
178 },
179
180 /// All multipaths in a template must share the same alt-count.
181 #[error("multipath alt-count mismatch: expected {expected}, got {got}")]
182 MultipathAltCountMismatch {
183 /// Expected alt-count.
184 expected: usize,
185 /// Mismatched alt-count.
186 got: usize,
187 },
188
189 /// A `use_site_path_overrides` entry was keyed on `@0`. `@0` is the
190 /// canonical baseline (`Descriptor::use_site_path`) and cannot be
191 /// overridden — an `@0` entry is a non-canonical / adversarial wire
192 /// (our encoders only push overrides for `i ≥ 1`). Per the D5(a) decode
193 /// canonical-form check (`restore-md1-per-key-use-site` SPEC §4.1).
194 #[error("use-site override keyed on baseline @{idx}; @0 cannot be overridden")]
195 BaselineUseSiteOverride {
196 /// The offending index (always 0).
197 idx: u8,
198 },
199
200 /// A `use_site_path_overrides` entry's `UseSitePath` equaled the
201 /// resolved baseline (`Descriptor::use_site_path`). A redundant override
202 /// is non-canonical (our encoders push an override only when it DIFFERS
203 /// from the baseline) and is rejected at decode. Per the D5(a) decode
204 /// canonical-form check.
205 #[error("redundant use-site override for @{idx}; equals the baseline use-site path")]
206 RedundantUseSiteOverride {
207 /// The placeholder index whose override duplicates the baseline.
208 idx: u8,
209 },
210
211 /// Tap-script-tree leaf has a tag that is forbidden per spec §6.3.1.
212 #[error("forbidden tap-script-tree leaf tag: 0x{tag:02x}")]
213 ForbiddenTapTreeLeaf {
214 /// Primary 6-bit tag code (bytecode space) of the forbidden leaf.
215 tag: u8,
216 },
217
218 /// Operator appears in a forbidden context per SPEC v0.30 §11.
219 /// `TopLevel` is enforced decoder-side at `decode_payload`; `TapLeaf` is
220 /// covered by the narrower [`Error::ForbiddenTapTreeLeaf`]; `MultiBody` is
221 /// structurally unreachable post-v0.30 Phase C (multi-family bodies carry
222 /// raw kiw-bit indices, not child tags).
223 #[error("operator {tag:?} not allowed in context {context:?}")]
224 OperatorContextViolation {
225 /// The offending operator tag.
226 tag: crate::tag::Tag,
227 /// Which tree-position the tag is forbidden in.
228 context: ContextKind,
229 },
230
231 /// Chunk count out of range. Per SPEC v0.30 §2.5: `1 ≤ count ≤ 64`.
232 #[error("chunk count {count} out of range; require 1 ≤ count ≤ 64")]
233 ChunkCountOutOfRange {
234 /// Provided count.
235 count: u8,
236 },
237
238 /// Chunk index ≥ count; require index < count.
239 #[error("chunk index {index} ≥ count {count}")]
240 ChunkIndexOutOfRange {
241 /// Provided index.
242 index: u8,
243 /// Provided count.
244 count: u8,
245 },
246
247 /// Chunk-set-id exceeds 20-bit range.
248 #[error("chunk-set-id 0x{id:x} exceeds 20-bit range")]
249 ChunkSetIdOutOfRange {
250 /// Provided ID.
251 id: u32,
252 },
253
254 /// Chunk header missing chunked-flag. Per SPEC v0.30 §2.2: bit 0 of the
255 /// first 5-bit symbol of a chunked payload is the chunked-flag (followed
256 /// by the 4-bit version field); it MUST be 1 in every chunk header.
257 #[error("chunk header chunked-flag missing; per SPEC §2.2 bit 0 of the first symbol must be 1")]
258 ChunkHeaderChunkedFlagMissing,
259
260 /// Encoding requires more chunks than the spec maximum (64).
261 #[error("encoding requires {needed} chunks; max is 64 per spec §9.8")]
262 ChunkCountExceedsMax {
263 /// Number of chunks needed.
264 needed: usize,
265 },
266
267 /// Codex32 decode error (HRP mismatch, alphabet violation, BCH verification failure).
268 #[error("codex32 decode error: {0}")]
269 Codex32DecodeError(String),
270
271 /// Codex32 encode error (BCH layer failure).
272 #[error("codex32 encode error: {0}")]
273 Codex32EncodeError(String),
274
275 /// Chunk set is empty (no strings provided to reassemble).
276 #[error("chunk set is empty (no strings provided)")]
277 ChunkSetEmpty,
278
279 /// Chunks in the set disagree on version, chunk-set-id, or count.
280 #[error("chunks in the set disagree on version, chunk-set-id, or count")]
281 ChunkSetInconsistent,
282
283 /// Chunk set incomplete: got fewer chunks than `expected`.
284 #[error("chunk set incomplete: got {got} chunks, expected {expected}")]
285 ChunkSetIncomplete {
286 /// Provided chunk count.
287 got: usize,
288 /// Expected chunk count.
289 expected: usize,
290 },
291
292 /// Chunk index gap: expected index N, got M.
293 #[error("chunk index gap: expected index {expected}, got {got}")]
294 ChunkIndexGap {
295 /// Expected index in the sequence.
296 expected: u8,
297 /// Actual index encountered.
298 got: u8,
299 },
300
301 /// Chunk-set-id mismatch between expected and reassembled-then-derived.
302 #[error("chunk-set-id mismatch: expected 0x{expected:x}, derived 0x{derived:x}")]
303 ChunkSetIdMismatch {
304 /// Expected (from chunks).
305 expected: u32,
306 /// Derived (from reassembled payload).
307 derived: u32,
308 },
309
310 /// LP4-ext varint value exceeds single-extension payload range (29 bits).
311 #[error("varint value {value} exceeds single-extension range (max 2^29 - 1)")]
312 VarintOverflow {
313 /// The offending value.
314 value: u32,
315 },
316
317 /// A non-canonical wrapper has no explicit origin path for some `@N`,
318 /// either via `OriginPathOverrides` or a populated `path_decl` entry,
319 /// and `canonical_origin(&d.tree)` is `None`. Per spec v0.13 §6.3.
320 #[error("non-canonical wrapper requires explicit origin for @{idx}, but none provided")]
321 MissingExplicitOrigin {
322 /// The placeholder index for which an explicit origin is required.
323 idx: u8,
324 },
325
326 /// `presence_byte` had non-zero reserved bits (bits 2..7) inside a
327 /// `WalletPolicyId` canonical-record preimage. Per spec v0.13 §5.3:
328 /// encoders MUST set reserved bits to 0 and decoders MUST reject
329 /// inputs with non-zero reserved bits. v0.13's encoder masks reserved
330 /// bits explicitly when building the hash preimage; the helper
331 /// [`crate::identity::validate_presence_byte`] enforces the
332 /// decoder-side contract for canonical-record consumers.
333 #[error("WalletPolicyId presence_byte has non-zero reserved bits: 0x{reserved_bits:02x}")]
334 InvalidPresenceByte {
335 /// The reserved-bit field (bits 2..7) of the offending presence byte.
336 reserved_bits: u8,
337 },
338
339 /// A `Pubkeys` TLV entry's 33-byte compressed-pubkey field (bytes
340 /// 32..65 of the 65-byte xpub payload) failed to parse as a valid
341 /// secp256k1 point. The 32-byte chain code prefix is unvalidated.
342 /// Per spec v0.13 §6.4.
343 #[error("invalid xpub bytes for @{idx}: pubkey field is not a valid secp256k1 point")]
344 InvalidXpubBytes {
345 /// The placeholder index whose xpub failed to parse.
346 idx: u8,
347 },
348 /// Address derivation requires a populated `Pubkeys` TLV entry for
349 /// every `@N`; this descriptor is missing one (template-only or
350 /// partial-keys mode). v0.14+ derivation surface only.
351 #[error(
352 "missing xpub for @{idx}; address derivation requires wallet-policy mode with all @N populated"
353 )]
354 MissingPubkey {
355 /// The placeholder index whose xpub is absent.
356 idx: u8,
357 },
358
359 /// `Descriptor::derive_address` was called with a `chain` index
360 /// outside the use-site multipath alt-count (or non-zero when no
361 /// multipath is present).
362 #[error("chain index {chain} out of range; use-site multipath alt-count is {alt_count}")]
363 ChainIndexOutOfRange {
364 /// The provided chain index.
365 chain: u32,
366 /// The number of alternatives in the use-site multipath (`0` when
367 /// no multipath component is present).
368 alt_count: usize,
369 },
370
371 /// Address derivation requires non-hardened use-site components,
372 /// but this descriptor's use-site path declares a hardened
373 /// alternative or hardened wildcard. BIP 32 forbids hardened
374 /// derivation from a public key, so an xpub-only restore cannot
375 /// produce addresses for this wallet.
376 #[error(
377 "hardened public-key derivation: use-site path requires hardened component, which BIP 32 forbids on xpub-only restore"
378 )]
379 HardenedPublicDerivation,
380
381 /// Address derivation failed at the miniscript layer (or in the
382 /// AST → miniscript converter). Carries a free-form `detail` string
383 /// describing the underlying error — typically a `miniscript::Error`,
384 /// a `Tr`/`Wsh` constructor failure (type-check / context error), or
385 /// an arity/context mismatch raised by the converter.
386 #[error("address derivation failed: {detail}")]
387 AddressDerivationFailed {
388 /// Free-form description of the underlying failure.
389 detail: String,
390 },
391
392 /// Inside a `tr()` body, `is_nums = false` was paired with a `key_index`
393 /// out of range (`key_index >= n`). Per SPEC v0.30 §7 + §11: the
394 /// placeholder-index range is `0..n` strictly; the v0.x NUMS sentinel
395 /// slot at `key_index = n` is gone (NUMS is now flag-driven via
396 /// `Body::Tr.is_nums`). Raised by `validate_placeholder_usage` when the
397 /// in-`tr()` overflow condition is hit.
398 #[error("NUMS sentinel conflict: is_nums=false with key_index out of range (SPEC §7 §11)")]
399 NUMSSentinelConflict,
400
401 /// Decode-side recursion depth exceeded the hardening cap.
402 /// `read_node` calls itself recursively for tags with child bodies
403 /// (`Tag::Sh`, `Tag::AndV`, `Tag::TapTree`, `Tag::Multi`, `Tag::Tr`,
404 /// etc.); a hostile wire payload nesting these tags arbitrarily deep
405 /// would blow the Rust stack. The cap is shared across all recursive
406 /// tags as a generic anti-DOS hardening bound. v0.19 introduced.
407 #[error("decode recursion depth {depth} exceeded maximum {max}")]
408 DecodeRecursionDepthExceeded {
409 /// Current recursion depth at which the cap fired.
410 depth: u8,
411 /// Maximum allowed depth.
412 max: u8,
413 },
414
415 /// BCH correction capacity exceeded: a chunk's syndrome pattern
416 /// indicated more than `t = 4` errors (BCH(93, 80, 8) singleton
417 /// bound `2t = 8`), so a unique correction cannot be derived.
418 /// v0.34.0 introduced; raised by [`crate::decode_with_correction`].
419 /// Atomic per plan §1 D28: any chunk failing this check fails the
420 /// whole multi-chunk call without partial output.
421 #[error("chunk {chunk_index} has more than {bound} errors; uncorrectable")]
422 TooManyErrors {
423 /// 0-indexed position of the offending chunk in the caller's
424 /// `&[&str]` slice.
425 chunk_index: usize,
426 /// The BCH singleton bound `2t = 8` (i.e. 4 correctable errors).
427 bound: u8,
428 },
429
430 /// Encode-side cap (cycle-4 H6): a single codex32 string's data part
431 /// exceeded the regular code's `REGULAR_DATA_SYMBOLS_MAX = 80` symbols.
432 /// The codex32 regular code is BCH(93, 80, 8); a single string therefore
433 /// carries at most 80 data symbols + 13 checksum = 93. `wrap_payload`
434 /// rejects an over-length payload (fail-closed) rather than emit an
435 /// un-decodable / aliasing-prone single string — callers needing more
436 /// capacity must use chunked encoding (`--force-chunked` / `split()`).
437 #[error(
438 "payload is {data_symbols} data symbols; the codex32 regular code caps single strings at {max} (use chunked encoding / --force-chunked)"
439 )]
440 PayloadTooLongForSingleString {
441 /// The over-length data-symbol count actually computed.
442 data_symbols: usize,
443 /// The maximum legal data-symbol count (80).
444 max: usize,
445 },
446
447 /// Decode-side cap, correcting path (cycle-4 M4): a chunk handed to the
448 /// BCH-correcting decoder (`decode_with_correction`) had more than 93
449 /// symbols. The codex32 regular code's generator `β` has order 93, so a
450 /// degree `d` and `d + 93` alias in `chien_search` for an over-93-symbol
451 /// word — the correcting decoder would mis-correct at an aliased root.
452 /// Reject the out-of-domain chunk before correction (fail-closed).
453 #[error(
454 "chunk {chunk_index} has {symbols} symbols; the codex32 regular code caps a string at {max}"
455 )]
456 ChunkSymbolCountOutOfRange {
457 /// 0-indexed position of the offending chunk in the caller's slice.
458 chunk_index: usize,
459 /// The over-length symbol count actually supplied.
460 symbols: usize,
461 /// The maximum legal codeword length (93).
462 max: usize,
463 },
464
465 /// Decode-side cap, non-correcting path (cycle-4 I1 / §5.2.3): a single
466 /// md1 string handed to the non-correcting primitive (`unwrap_string` /
467 /// `decode_md1_string`) had more than 93 symbols. A clean (residue == 0)
468 /// over-length word is BCH-verifiable by the length-agnostic
469 /// `bch_verify_regular` but is structurally out-of-domain for the regular
470 /// code; reject it before BCH verification (fail-closed). No chunk index
471 /// (single string, not a chunk).
472 #[error("string has {symbols} symbols; the codex32 regular code caps a string at {max}")]
473 StringSymbolCountOutOfRange {
474 /// The over-length symbol count actually supplied.
475 symbols: usize,
476 /// The maximum legal codeword length (93).
477 max: usize,
478 },
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::tag::Tag;
485
486 /// SPEC v0.30 §11: `OperatorContextViolation` carries the offending tag
487 /// + a `ContextKind` discriminator. Pins the type shape and Display
488 /// output against future drift; does NOT claim live wire reachability
489 /// (see FOLLOWUP `v0.30-phase-g-operator-context-violation-unwired`).
490 #[test]
491 fn operator_context_violation_constructs() {
492 let err = Error::OperatorContextViolation {
493 tag: Tag::Multi,
494 context: ContextKind::MultiBody,
495 };
496 let s = err.to_string();
497 assert!(s.contains("Multi"), "Display must mention tag: {s}");
498 assert!(s.contains("MultiBody"), "Display must mention context: {s}");
499 }
500
501 /// SPEC v0.30 §7 + §11: `NUMSSentinelConflict` Display pins the SPEC-cite
502 /// substring so the doc-comment + format string don't silently drift.
503 #[test]
504 fn nums_sentinel_conflict_display() {
505 let s = Error::NUMSSentinelConflict.to_string();
506 assert!(s.contains("§7"), "Display must cite SPEC §7: {s}");
507 assert!(s.contains("§11"), "Display must cite SPEC §11: {s}");
508 }
509}