nx_blob_store_mcu.nx source
↩ module page · 264 lines · 9187 B
1// nx_blob_store_mcu.nx -- MCU-tier variant of blob_store class.
2//
3// EM-6 milestone of NISHI_ECOSYSTEM_EVOLUTION_ROADMAP.md. Same API
4// shape as L0 nx_blob_store but with footprint tuned for MCU-class
5// devices (capacity 4, max blob 256 bytes). Declared as variant
6// of class `blob_store`, so the SA-arc selector can pick THIS for
7// MCU-tier targets and the regular nx_blob_store for laptop-class+.
8//
9// This is the FIRST class to carry multiple variants (`blob_store`
10// V1 + V1_MCU), exercising the cross-tier selection path the SA
11// arc architecture promised but had no real test for until now.
12//
13// V1 footprint envelope:
14// - Capacity: 4 entries (vs 256 in regular L0)
15// - Max blob size: 256 bytes (vs 16 MiB in regular L0)
16// - Index footprint: ~80 B struct + 4 * (32+8) = 240 B
17// - Total RAM (excluding blob bodies): ~320 B
18// - Blob bodies still mmap'd separately (caller-allocated)
19//
20// This fits comfortably on Arduino Uno (2 KB SRAM), STM32F0 family,
21// nRF52 series, ESP8266, and most Cortex-M0+ devices. AVR backend
22// (EM-10) will produce binary that consumes < 1 KB.
23//
24// API mirrors nx_blob_store with `_mcu` suffix to coexist without
25// symbol collision. Future variant-resolver work can use the
26// substrate's manifest system to dispatch by tier without caller
27// awareness.
28//
29// genealogy_id: cardinal_2026-05-20_ecosystem_evolution +
30// nx_blob_store_v1 lineage
31// lineage_id: substrate_blob_store_mcu_v1
32//
33// nx_capability_manifest:
34// variant_class: blob_store
35// variant_id: blob_store_v1_mcu_inline
36// requires_isa: [rv32i, rv32imac, rv64imac, x86_64, cortex_m, armv7a, aarch64, avr]
37// requires_syscalls: [mmap]
38// requires_ram_min_b: 512
39// tier_floor: NX_TIER_INF_MCU
40// tier_ceiling: NX_TIER_INF_MOBILE
41// cost_model:
42// flops_per_n: 50.0 // SHA-256 round per 64-byte block (dominant cost on MCU)
43// bytes_per_n: 1.0 // input bytes pass-through
44// syscalls_per_n: 0.0
45// adversary_class: THREAT_AI_ADVERSARY
46//
47// nx_safety_envelope:
48// intended_use: "MCU-tier variant of blob_store class; smaller
49// footprint, same content-addressed guarantees"
50// sil_target: SIL2
51// evidence: [canary_bracketed, sha256_keyed, dedup_proven,
52// hash_verify_on_read, append_only, capacity_gated]
53// verdict: NOT_YET_EVALUATED
54
55import "nx_syscalls.nx"
56import "nx_sha256.nx"
57import "nx_blob_store.nx" // reuse NxBlobHash struct + helpers
58
59// ===== Constants =================================================
60const NX_BLOB_STORE_MCU_CAPACITY: i64 = 4
61const NX_BLOB_STORE_MCU_MAX_BLOB_BYTES: i64 = 256
62
63// Verdicts (parallel to NX_BLOB_*).
64const NX_BLOB_MCU_OK: i64 = 0
65const NX_BLOB_MCU_BAD_INPUT: i64 = 1
66const NX_BLOB_MCU_FULL: i64 = 2
67const NX_BLOB_MCU_NOT_FOUND: i64 = 3
68const NX_BLOB_MCU_TOO_BIG: i64 = 4
69const NX_BLOB_MCU_TAMPER: i64 = 5
70const NX_BLOB_MCU_N_VERDICTS: i64 = 6
71
72func nx_blob_mcu_verdict_is_valid(v: i64) -> i64 {
73 if v < 0 { return 0 }
74 if v >= NX_BLOB_MCU_N_VERDICTS { return 0 }
75 return 1
76}
77
78// Canaries distinct from the regular L0 store + all other primitives.
79const NX_BLOB_STORE_MCU_CANARY_PRE: i64 = 0x4E58424C4D435550 // "NXBLMCUP"
80const NX_BLOB_STORE_MCU_CANARY_POST: i64 = 0x4E58424C4D43554E // "NXBLMCUN"
81
82// ===== Entry + Store ============================================
83// Reuses NxBlobHash from nx_blob_store.nx (4 i64 = 32 bytes,
84// little-endian-packed SHA-256 digest). NxBlobEntryMcu mirrors
85// NxBlobEntry but is independent for clarity + clean variant
86// separation.
87
88struct NxBlobEntryMcu {
89 hash_w0: i64,
90 hash_w1: i64,
91 hash_w2: i64,
92 hash_w3: i64,
93 blob_ptr: *u8,
94 blob_len: i64,
95}
96
97struct NxBlobStoreMcu {
98 canary_pre: i64,
99 n_entries: i64,
100 max_entries: i64,
101 entries: *i64, // *i64 array of NxBlobEntryMcu pointers
102 canary_post: i64,
103}
104
105// ===== Helpers (private; mirror regular L0 internals) ===========
106
107func _mcu_load_i64_le(buf: *u8, off: i64) -> i64 {
108 let b0: i64 = (buf[off + 0] as i64) & 255
109 let b1: i64 = (buf[off + 1] as i64) & 255
110 let b2: i64 = (buf[off + 2] as i64) & 255
111 let b3: i64 = (buf[off + 3] as i64) & 255
112 let b4: i64 = (buf[off + 4] as i64) & 255
113 let b5: i64 = (buf[off + 5] as i64) & 255
114 let b6: i64 = (buf[off + 6] as i64) & 255
115 let b7: i64 = (buf[off + 7] as i64) & 255
116 return b0
117 | (b1 << 8)
118 | (b2 << 16)
119 | (b3 << 24)
120 | (b4 << 32)
121 | (b5 << 40)
122 | (b6 << 48)
123 | (b7 << 56)
124}
125
126func _mcu_hash_bytes_to_struct(bytes: *u8, len: i64, out: *NxBlobHash) -> i64 {
127 let digest: *u8 = sys_mmap(64)
128 sha256_digest(bytes, len, digest)
129 out.w0 = _mcu_load_i64_le(digest, 0)
130 out.w1 = _mcu_load_i64_le(digest, 8)
131 out.w2 = _mcu_load_i64_le(digest, 16)
132 out.w3 = _mcu_load_i64_le(digest, 24)
133 return 0
134}
135
136func _mcu_entry_hash_eq(e: *NxBlobEntryMcu, h: *NxBlobHash) -> i64 {
137 if e.hash_w0 != h.w0 { return 0 }
138 if e.hash_w1 != h.w1 { return 0 }
139 if e.hash_w2 != h.w2 { return 0 }
140 if e.hash_w3 != h.w3 { return 0 }
141 return 1
142}
143
144func _mcu_find_entry_idx(s: *NxBlobStoreMcu, h: *NxBlobHash) -> i64 {
145 var i: i64 = 0
146 while i < s.n_entries {
147 let e_addr: i64 = s.entries[i]
148 if e_addr != 0 {
149 let e: *NxBlobEntryMcu = e_addr as *NxBlobEntryMcu
150 if _mcu_entry_hash_eq(e, h) == 1 { return i }
151 }
152 i = i + 1
153 }
154 return -1
155}
156
157func _mcu_alloc_entry(h: *NxBlobHash, src: *u8, len: i64) -> *NxBlobEntryMcu {
158 let e: *NxBlobEntryMcu = (sys_mmap(64)) as *NxBlobEntryMcu
159 e.hash_w0 = h.w0
160 e.hash_w1 = h.w1
161 e.hash_w2 = h.w2
162 e.hash_w3 = h.w3
163 let buf: *u8 = sys_mmap(len + 8)
164 var k: i64 = 0
165 while k < len {
166 buf[k] = src[k]
167 k = k + 1
168 }
169 e.blob_ptr = buf
170 e.blob_len = len
171 return e
172}
173
174// ===== Construction =============================================
175func nx_blob_store_mcu_new() -> *NxBlobStoreMcu {
176 let s: *NxBlobStoreMcu = (sys_mmap(64)) as *NxBlobStoreMcu
177 s.canary_pre = NX_BLOB_STORE_MCU_CANARY_PRE
178 s.canary_post = NX_BLOB_STORE_MCU_CANARY_POST
179 s.n_entries = 0
180 s.max_entries = NX_BLOB_STORE_MCU_CAPACITY
181 s.entries = (sys_mmap(NX_BLOB_STORE_MCU_CAPACITY * 8)) as *i64
182 var i: i64 = 0
183 while i < NX_BLOB_STORE_MCU_CAPACITY {
184 s.entries[i] = 0
185 i = i + 1
186 }
187 return s
188}
189
190// ===== Validity gate ============================================
191func nx_blob_store_mcu_is_valid(s: *NxBlobStoreMcu) -> i64 {
192 if (s as i64) == 0 { return 0 }
193 if s.canary_pre != NX_BLOB_STORE_MCU_CANARY_PRE { return 0 }
194 if s.canary_post != NX_BLOB_STORE_MCU_CANARY_POST { return 0 }
195 if s.n_entries < 0 { return 0 }
196 if s.n_entries > s.max_entries { return 0 }
197 return 1
198}
199
200// ===== put_blob =================================================
201// Adds the MCU-tier oversized-blob gate (refuses bytes > 256).
202func nx_blob_store_mcu_put(
203 store: *NxBlobStoreMcu,
204 bytes: *u8, len: i64,
205 out_hash: *NxBlobHash
206) -> i64 {
207 if nx_blob_store_mcu_is_valid(store) != 1 { return NX_BLOB_MCU_TAMPER }
208 if (out_hash as i64) == 0 { return NX_BLOB_MCU_BAD_INPUT }
209 if len < 0 { return NX_BLOB_MCU_BAD_INPUT }
210 if len > NX_BLOB_STORE_MCU_MAX_BLOB_BYTES { return NX_BLOB_MCU_TOO_BIG }
211 if len > 0 {
212 if (bytes as i64) == 0 { return NX_BLOB_MCU_BAD_INPUT }
213 }
214
215 _mcu_hash_bytes_to_struct(bytes, len, out_hash)
216
217 let existing: i64 = _mcu_find_entry_idx(store, out_hash)
218 if existing >= 0 { return NX_BLOB_MCU_OK }
219
220 if store.n_entries >= store.max_entries { return NX_BLOB_MCU_FULL }
221
222 let e: *NxBlobEntryMcu = _mcu_alloc_entry(out_hash, bytes, len)
223 store.entries[store.n_entries] = e as i64
224 store.n_entries = store.n_entries + 1
225 return NX_BLOB_MCU_OK
226}
227
228// ===== get_blob =================================================
229func nx_blob_store_mcu_get(
230 store: *NxBlobStoreMcu,
231 hash: *NxBlobHash,
232 out_buf: *u8, max_len: i64
233) -> i64 {
234 if nx_blob_store_mcu_is_valid(store) != 1 { return -1 }
235 if (hash as i64) == 0 { return -1 }
236 if (out_buf as i64) == 0 { return -1 }
237 if max_len < 0 { return -1 }
238
239 let idx: i64 = _mcu_find_entry_idx(store, hash)
240 if idx < 0 { return -1 }
241 let e: *NxBlobEntryMcu = (store.entries[idx]) as *NxBlobEntryMcu
242 if e.blob_len > max_len { return -1 }
243
244 var k: i64 = 0
245 while k < e.blob_len {
246 out_buf[k] = e.blob_ptr[k]
247 k = k + 1
248 }
249 return e.blob_len
250}
251
252// ===== has_blob =================================================
253func nx_blob_store_mcu_has(store: *NxBlobStoreMcu, hash: *NxBlobHash) -> i64 {
254 if nx_blob_store_mcu_is_valid(store) != 1 { return 0 }
255 if (hash as i64) == 0 { return 0 }
256 if _mcu_find_entry_idx(store, hash) < 0 { return 0 }
257 return 1
258}
259
260// ===== count ====================================================
261func nx_blob_store_mcu_count(store: *NxBlobStoreMcu) -> i64 {
262 if nx_blob_store_mcu_is_valid(store) != 1 { return -1 }
263 return store.n_entries
264}