code wiki / (root) / nx_gcode_manifest.nx

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}