nx_gcode_manifest.nx source
↩ module page · 195 lines · 7448 B
1// nx_gcode_manifest.nx -- per-print provenance manifest emitted as
2// G-code header comments. Carries a BLAKE2b digest of every input
3// to the slicer (mesh, config, machine graph, material profile,
4// slicer commit) so a printed artefact -- or a failed print -- maps
5// deterministically back to the exact slicer state.
6//
7// Per cardinal feedback-end-to-end-bit-traceability-architecture +
8// roadmap NISHI_3D_PRINT_ROADMAP ยง2.5: no SOTA slicer in May 2026
9// (OrcaSlicer, PrusaSlicer, Bambu Studio, SuperSlicer) embeds
10// source-mesh provenance in their G-code output. This is a clean-
11// room EXCEED axis with no prior art to match against.
12//
13// Conceptually distinct from:
14// - nx_provenance.nx -- license-class source gate (different role)
15// - nx_attest.nx -- substrate-wide Merkle chain log (this
16// primitive can be COMPOSED to feed the
17// chain in a later phase; see P0.2 notes)
18//
19// Manifest format (human-readable text, valid G-code comments):
20// ; NX_GCODE_MANIFEST_V1
21// ; mesh.blake2b <128 hex chars>
22// ; config.blake2b <128 hex chars>
23// ; machine.blake2b <128 hex chars>
24// ; material.blake2b <128 hex chars>
25// ; slicer.sha <substrate build hash, caller-supplied>
26// ; manifest.blake2b <128 hex chars over all preceding lines>
27//
28// All hashes are BLAKE2b-512 (per substrate convention --
29// nx_blake2b is the shipped primitive; nx_blake3 is queued for a
30// later session). Final "manifest.blake2b" line covers exactly the
31// bytes of every preceding line in the buffer, making the manifest
32// self-attesting: tampering with any earlier line breaks the final
33// hash check.
34//
35// G-code consumers (Klipper, Marlin, RepRapFirmware, Bambu firmware)
36// uniformly skip semicolon-prefixed lines -- this manifest has zero
37// effect on print behaviour.
38//
39// license_tier: ORIGINAL
40
41import "nx_syscalls.nx"
42import "nx_blake2b.nx"
43
44const NX_GMANIFEST_MAX_BYTES: i64 = 4096
45const NX_GMANIFEST_DIGEST_LEN: i64 = 64 // BLAKE2b-512
46
47// ===== struct =====================================================
48
49struct NxGcodeManifest {
50 buf: *u8,
51 len: i64,
52 capacity: i64,
53 final_hash: *u8,
54 finalized: i64,
55}
56
57const NX_GMANIFEST_BYTES: i64 = 40
58
59// Helper: allocate caller-owned scratch + call nx_blake2b_hash with
60// the substrate's caller-owns-scratch convention. Used by both
61// add_blob (per-entry hash) and finalize (manifest-self-hash).
62func gmanifest_blake2b_oneshot(msg: *u8, msg_len: i64,
63 out: *u8, out_len: i64) -> i64 {
64 let ctx: *NxBlake2b = (sys_mmap(NX_BLAKE2B_CTX_BYTES)) as *NxBlake2b
65 let buf128: *u8 = sys_mmap(NX_BLAKE2B_BLOCK_BYTES)
66 let scratch_v: *i64 = (sys_mmap(128)) as *i64
67 let scratch_m: *i64 = (sys_mmap(128)) as *i64
68 return nx_blake2b_hash(msg, msg_len, 0 as *u8, 0,
69 out, out_len, ctx, buf128,
70 scratch_v, scratch_m)
71}
72
73func nx_gmanifest_new() -> *NxGcodeManifest {
74 let m: *NxGcodeManifest = (sys_mmap(NX_GMANIFEST_BYTES)) as *NxGcodeManifest
75 m.buf = sys_mmap(NX_GMANIFEST_MAX_BYTES)
76 m.len = 0
77 m.capacity = NX_GMANIFEST_MAX_BYTES
78 m.final_hash = sys_mmap(NX_GMANIFEST_DIGEST_LEN)
79 m.finalized = 0
80 // Inline magic-line write (NishiLang forbids forward function
81 // refs, so we can't call nx_gmanifest_append_cstr here -- it's
82 // defined below).
83 let magic: *u8 = "; NX_GCODE_MANIFEST_V1\n"
84 var i: i64 = 0
85 while magic[i] != 0 {
86 m.buf[m.len] = magic[i]
87 m.len = m.len + 1
88 i = i + 1
89 }
90 return m
91}
92
93// ===== buffer helpers =============================================
94
95func nx_gmanifest_append_byte(m: *NxGcodeManifest, b: i64) -> i64 {
96 if m.len >= m.capacity { return -1 }
97 m.buf[m.len] = b & 0xff
98 m.len = m.len + 1
99 return 0
100}
101
102func nx_gmanifest_append_cstr(m: *NxGcodeManifest, s: *u8) -> i64 {
103 var i: i64 = 0
104 while s[i] != 0 {
105 if m.len >= m.capacity { return -1 }
106 m.buf[m.len] = s[i]
107 m.len = m.len + 1
108 i = i + 1
109 }
110 return 0
111}
112
113func nx_gmanifest_append_bytes(m: *NxGcodeManifest, src: *u8, n: i64) -> i64 {
114 var i: i64 = 0
115 while i < n {
116 if m.len >= m.capacity { return -1 }
117 m.buf[m.len] = src[i]
118 m.len = m.len + 1
119 i = i + 1
120 }
121 return 0
122}
123
124// Single nibble -> lowercase hex char.
125func nx_gmanifest_nibble_hex(n: i64) -> i64 {
126 let v: i64 = n & 0xf
127 if v < 10 { return v + 48 } // '0'..'9'
128 return v + 87 // 'a'..'f'
129}
130
131// Append n bytes as 2n lowercase hex chars.
132func nx_gmanifest_append_hex(m: *NxGcodeManifest, src: *u8, n: i64) -> i64 {
133 var i: i64 = 0
134 while i < n {
135 let b: i64 = src[i] & 0xff
136 if m.len + 1 >= m.capacity { return -1 }
137 m.buf[m.len] = nx_gmanifest_nibble_hex(b >> 4) & 0xff
138 m.buf[m.len + 1] = nx_gmanifest_nibble_hex(b) & 0xff
139 m.len = m.len + 2
140 i = i + 1
141 }
142 return 0
143}
144
145// ===== entry emission ============================================
146
147// Emit one entry line: "; <key> <hex-digest>\n". `digest_len` must
148// be the byte length of the digest (typically 64 for BLAKE2b-512,
149// 32 for SHA-256 in the slicer.sha case).
150func nx_gmanifest_add(m: *NxGcodeManifest, key: *u8, digest: *u8, digest_len: i64) -> i64 {
151 if m.finalized == 1 { return -2 }
152 if nx_gmanifest_append_cstr(m, "; ") != 0 { return -1 }
153 if nx_gmanifest_append_cstr(m, key) != 0 { return -1 }
154 if nx_gmanifest_append_cstr(m, " ") != 0 { return -1 }
155 if nx_gmanifest_append_hex(m, digest, digest_len) != 0 { return -1 }
156 if nx_gmanifest_append_byte(m, 10) != 0 { return -1 } // '\n'
157 return 0
158}
159
160// Convenience: hash arbitrary input bytes via BLAKE2b-512 and emit
161// the named entry. Composes nx_blake2b directly so callers don't
162// need to manage scratch buffers.
163func nx_gmanifest_add_blob(m: *NxGcodeManifest, key: *u8, blob: *u8, blob_len: i64) -> i64 {
164 let digest: *u8 = sys_mmap(NX_GMANIFEST_DIGEST_LEN)
165 let rc: i64 = gmanifest_blake2b_oneshot(blob, blob_len, digest, NX_GMANIFEST_DIGEST_LEN)
166 if rc != 0 { return rc }
167 return nx_gmanifest_add(m, key, digest, NX_GMANIFEST_DIGEST_LEN)
168}
169
170// ===== finalize ===================================================
171
172// Compute the manifest's self-hash over every byte accumulated so
173// far, then append "; manifest.blake2b <hex>\n". After finalize,
174// no more entries may be added.
175func nx_gmanifest_finalize(m: *NxGcodeManifest) -> i64 {
176 if m.finalized == 1 { return -2 }
177 let rc: i64 = gmanifest_blake2b_oneshot(m.buf, m.len, m.final_hash, NX_GMANIFEST_DIGEST_LEN)
178 if rc != 0 { return rc }
179 if nx_gmanifest_append_cstr(m, "; manifest.blake2b ") != 0 { return -1 }
180 if nx_gmanifest_append_hex(m, m.final_hash, NX_GMANIFEST_DIGEST_LEN) != 0 { return -1 }
181 if nx_gmanifest_append_byte(m, 10) != 0 { return -1 }
182 m.finalized = 1
183 return 0
184}
185
186// Copy manifest bytes into a caller-provided output buffer. Returns
187// bytes written, or -1 if dst_capacity is insufficient. Used by the
188// G-code emitter to splice the manifest into the header of the
189// output file.
190func nx_gmanifest_write(m: *NxGcodeManifest, dst: *u8, dst_capacity: i64) -> i64 {
191 if m.len > dst_capacity { return -1 }
192 var i: i64 = 0
193 while i < m.len { dst[i] = m.buf[i]; i = i + 1 }
194 return m.len
195}