mk_codec/key_card.rs
1//! `KeyCard` — the in-memory representation of a decoded MK card.
2//!
3//! Field semantics mirror the wire-format payload from
4//! `design/SPEC_mk_v0_1.md` §3.2. The bytecode-layer encode/decode
5//! lives in [`crate::bytecode`] (Phase 4); the string-layer wrapper
6//! (BCH + chunking) wires up the public `encode`/`decode` functions
7//! below in Phase 5.
8
9use bitcoin::bip32::{DerivationPath, Fingerprint, Xpub};
10
11use crate::error::Result;
12
13/// In-memory representation of one decoded MK card.
14///
15/// Per closure Q-8, `origin_fingerprint` is `Option<Fingerprint>`:
16/// a card encoded with the bytecode-header fingerprint flag unset
17/// (privacy-preserving mode) reconstructs to a `KeyCard` with
18/// `origin_fingerprint = None`.
19///
20/// `#[non_exhaustive]` so future versions can add fields without
21/// breaking external constructors.
22#[non_exhaustive]
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct KeyCard {
25 /// Policy ID stubs declaring which MD-encoded policy template(s)
26 /// this xpub is intended to serve. Each stub is the top 4 bytes of a
27 /// canonical, encoder-divergence-free md1 identity, FORM-AWARE: the
28 /// **WalletPolicyId** for a keyed wallet-policy md1, or the key-stable
29 /// **WalletDescriptorTemplateId** for a keyless template md1 (matching
30 /// the toolkit's `bundle_binding_stub` / mk-cli's `derive_stub_from_md1`,
31 /// toolkit #28). The vector is guaranteed non-empty after a successful
32 /// `decode` (the decoder rejects `count == 0` with
33 /// `Error::InvalidPolicyIdStubCount`).
34 pub policy_id_stubs: Vec<[u8; 4]>,
35
36 /// Master-key fingerprint identifying the seed from which `xpub`
37 /// was derived. Verbatim from BIP 380 origin notation `[fp/...]`.
38 /// Optional per closure Q-8: encoders MAY omit (set bytecode-header
39 /// bit 2 = 0) for the privacy-preserving mode.
40 pub origin_fingerprint: Option<Fingerprint>,
41
42 /// Derivation path from master to `xpub`. Encoded on the wire
43 /// either via a 1-byte standard-path indicator (BIP 44/49/84/86/
44 /// 48-segwit/48-nested/87 + testnet variants) or via the explicit
45 /// `0xFE` escape hatch with LEB128 components.
46 pub origin_path: DerivationPath,
47
48 /// The BIP 32 extended public key. The wire format carries a
49 /// 73-byte compact form (per closure Q-7); the in-memory `Xpub`
50 /// is reconstructed at decode time using the locked rule:
51 ///
52 /// ```text
53 /// depth := component_count(origin_path)
54 /// child_number := last_component(origin_path),
55 /// or Normal{0} when origin_path is empty (depth-0 / no-path key)
56 /// ```
57 pub xpub: Xpub,
58}
59
60impl KeyCard {
61 /// Construct a `KeyCard` from its four owned fields.
62 ///
63 /// `KeyCard` is `#[non_exhaustive]` so that future versions can
64 /// add fields without breaking external callers; the constructor
65 /// stays stable across additions because new fields land with
66 /// `Default`-compatible values or new constructors.
67 ///
68 /// # Field invariants enforced at encode time
69 ///
70 /// `KeyCard::new` is intentionally permissive — field-level
71 /// validation lives in [`crate::encode`] / [`crate::bytecode::encode_bytecode`].
72 /// In particular:
73 ///
74 /// - `policy_id_stubs` MUST be non-empty; the encoder rejects an
75 /// empty vector with [`crate::Error::InvalidPolicyIdStubCount`]
76 /// (per `design/SPEC_mk_v0_1.md` §4 rule 3).
77 /// - `origin_path` MUST have at most [`crate::MAX_PATH_COMPONENTS`]
78 /// = 10 components when an explicit-path encoding would be used;
79 /// exceeding that yields [`crate::Error::PathTooDeep`].
80 ///
81 /// Callers that want a fail-fast constructor should validate
82 /// these invariants before calling `new`, or simply rely on the
83 /// encoder's rejection.
84 pub fn new(
85 policy_id_stubs: Vec<[u8; 4]>,
86 origin_fingerprint: Option<Fingerprint>,
87 origin_path: DerivationPath,
88 xpub: Xpub,
89 ) -> Self {
90 Self {
91 policy_id_stubs,
92 origin_fingerprint,
93 origin_path,
94 xpub,
95 }
96 }
97
98 /// Serialize this card to its canonical pre-chunking bytecode.
99 ///
100 /// Deterministic pre-chunking bytecode; independent of the per-encode
101 /// random `chunk_set_id` (which lives in the string layer). This is the
102 /// same byte payload that [`crate::bytecode::encode_bytecode`] produces
103 /// and that the conformance corpus pins as `canonical_bytecode_hex`; it
104 /// is exposed here so a downstream consumer can obtain a stable,
105 /// round-trippable identity for the card without reaching into the
106 /// `bytecode` module or the random string framing.
107 ///
108 /// Reverse with [`KeyCard::from_canonical_payload_bytes`].
109 ///
110 /// # Errors
111 ///
112 /// Surfaces the encoder-side invariants of
113 /// [`crate::bytecode::encode_bytecode`] (e.g.
114 /// [`crate::Error::InvalidPolicyIdStubCount`],
115 /// [`crate::Error::XpubOriginPathMismatch`]).
116 pub fn canonical_payload_bytes(&self) -> Result<Vec<u8>> {
117 crate::bytecode::encode_bytecode(self)
118 }
119
120 /// Reconstruct a `KeyCard` from its canonical pre-chunking bytecode.
121 ///
122 /// Inverse of [`KeyCard::canonical_payload_bytes`]; accepts exactly the
123 /// byte payload that method emits (and that
124 /// [`crate::bytecode::decode_bytecode`] consumes). Empty or malformed
125 /// input is rejected cleanly with an [`crate::Error`] — never a panic.
126 ///
127 /// # Errors
128 ///
129 /// Surfaces the bytecode-layer validity rules of
130 /// [`crate::bytecode::decode_bytecode`] (e.g.
131 /// [`crate::Error::UnexpectedEnd`], [`crate::Error::TrailingBytes`],
132 /// [`crate::Error::UnsupportedVersion`]).
133 pub fn from_canonical_payload_bytes(bytes: &[u8]) -> Result<KeyCard> {
134 crate::bytecode::decode_bytecode(bytes)
135 }
136}
137
138/// Encode a `KeyCard` into one or more `mk1`-prefixed strings.
139///
140/// Multi-chunk encodings draw a fresh 20-bit `chunk_set_id` from the
141/// system CSPRNG. Use [`encode_with_chunk_set_id`] for byte-deterministic
142/// output (vector regeneration, conformance tests).
143pub fn encode(card: &KeyCard) -> Result<Vec<String>> {
144 crate::string_layer::encode(card)
145}
146
147/// Like [`encode`], with an explicit `chunk_set_id` override.
148///
149/// `chunk_set_id` MUST fit in 20 bits (`0..=0x000F_FFFF`); otherwise
150/// returns [`crate::Error::ChunkedHeaderMalformed`]. The override is
151/// only consulted on the chunked path; single-string encodings have no
152/// `chunk_set_id` field.
153pub fn encode_with_chunk_set_id(card: &KeyCard, chunk_set_id: u32) -> Result<Vec<String>> {
154 crate::string_layer::encode_with_chunk_set_id(card, chunk_set_id)
155}
156
157/// Decode one or more `mk1`-prefixed strings into a `KeyCard`.
158pub fn decode(strings: &[&str]) -> Result<KeyCard> {
159 crate::string_layer::decode(strings)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 /// Sanity check: type signatures compile and the public API
167 /// surface matches what the lib.rs re-exports expect. Real
168 /// round-trip coverage at this layer lands in Phase 6.
169 #[test]
170 fn types_compile() {
171 let _f: fn(&KeyCard) -> Result<Vec<String>> = encode;
172 let _g: fn(&[&str]) -> Result<KeyCard> = decode;
173 }
174}