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
27 /// of the policy's `SHA-256(canonical_bytecode)`. The vector is
28 /// guaranteed non-empty after a successful `decode` (the decoder
29 /// rejects `count == 0` with `Error::InvalidPolicyIdStubCount`).
30 pub policy_id_stubs: Vec<[u8; 4]>,
31
32 /// Master-key fingerprint identifying the seed from which `xpub`
33 /// was derived. Verbatim from BIP 380 origin notation `[fp/...]`.
34 /// Optional per closure Q-8: encoders MAY omit (set bytecode-header
35 /// bit 2 = 0) for the privacy-preserving mode.
36 pub origin_fingerprint: Option<Fingerprint>,
37
38 /// Derivation path from master to `xpub`. Encoded on the wire
39 /// either via a 1-byte standard-path indicator (BIP 44/49/84/86/
40 /// 48-segwit/48-nested/87 + testnet variants) or via the explicit
41 /// `0xFE` escape hatch with LEB128 components.
42 pub origin_path: DerivationPath,
43
44 /// The BIP 32 extended public key. The wire format carries a
45 /// 73-byte compact form (per closure Q-7); the in-memory `Xpub`
46 /// is reconstructed at decode time using the locked rule:
47 ///
48 /// ```text
49 /// depth := component_count(origin_path)
50 /// child_number := last_component(origin_path),
51 /// or Normal{0} when origin_path is empty (depth-0 / no-path key)
52 /// ```
53 pub xpub: Xpub,
54}
55
56impl KeyCard {
57 /// Construct a `KeyCard` from its four owned fields.
58 ///
59 /// `KeyCard` is `#[non_exhaustive]` so that future versions can
60 /// add fields without breaking external callers; the constructor
61 /// stays stable across additions because new fields land with
62 /// `Default`-compatible values or new constructors.
63 ///
64 /// # Field invariants enforced at encode time
65 ///
66 /// `KeyCard::new` is intentionally permissive — field-level
67 /// validation lives in [`crate::encode`] / [`crate::bytecode::encode_bytecode`].
68 /// In particular:
69 ///
70 /// - `policy_id_stubs` MUST be non-empty; the encoder rejects an
71 /// empty vector with [`crate::Error::InvalidPolicyIdStubCount`]
72 /// (per `design/SPEC_mk_v0_1.md` §4 rule 3).
73 /// - `origin_path` MUST have at most [`crate::MAX_PATH_COMPONENTS`]
74 /// = 10 components when an explicit-path encoding would be used;
75 /// exceeding that yields [`crate::Error::PathTooDeep`].
76 ///
77 /// Callers that want a fail-fast constructor should validate
78 /// these invariants before calling `new`, or simply rely on the
79 /// encoder's rejection.
80 pub fn new(
81 policy_id_stubs: Vec<[u8; 4]>,
82 origin_fingerprint: Option<Fingerprint>,
83 origin_path: DerivationPath,
84 xpub: Xpub,
85 ) -> Self {
86 Self {
87 policy_id_stubs,
88 origin_fingerprint,
89 origin_path,
90 xpub,
91 }
92 }
93}
94
95/// Encode a `KeyCard` into one or more `mk1`-prefixed strings.
96///
97/// Multi-chunk encodings draw a fresh 20-bit `chunk_set_id` from the
98/// system CSPRNG. Use [`encode_with_chunk_set_id`] for byte-deterministic
99/// output (vector regeneration, conformance tests).
100pub fn encode(card: &KeyCard) -> Result<Vec<String>> {
101 crate::string_layer::encode(card)
102}
103
104/// Like [`encode`], with an explicit `chunk_set_id` override.
105///
106/// `chunk_set_id` MUST fit in 20 bits (`0..=0x000F_FFFF`); otherwise
107/// returns [`crate::Error::ChunkedHeaderMalformed`]. The override is
108/// only consulted on the chunked path; single-string encodings have no
109/// `chunk_set_id` field.
110pub fn encode_with_chunk_set_id(card: &KeyCard, chunk_set_id: u32) -> Result<Vec<String>> {
111 crate::string_layer::encode_with_chunk_set_id(card, chunk_set_id)
112}
113
114/// Decode one or more `mk1`-prefixed strings into a `KeyCard`.
115pub fn decode(strings: &[&str]) -> Result<KeyCard> {
116 crate::string_layer::decode(strings)
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 /// Sanity check: type signatures compile and the public API
124 /// surface matches what the lib.rs re-exports expect. Real
125 /// round-trip coverage at this layer lands in Phase 6.
126 #[test]
127 fn types_compile() {
128 let _f: fn(&KeyCard) -> Result<Vec<String>> = encode;
129 let _g: fn(&[&str]) -> Result<KeyCard> = decode;
130 }
131}