nx_blob_store.nx source
↩ module page · 314 lines · 10532 B
1// nx_blob_store.nx -- content-addressed blob store (L0 storage substrate).
2//
3// ST-1 milestone of NISHI_STORAGE_SUBSTRATE_ROADMAP.md. Foundation
4// of the 6-layer storage stack; every higher layer (journal, K/V,
5// schema registry, peer sync, mount adapter) ultimately stores
6// bytes here keyed by SHA-256.
7//
8// V1 model:
9// put_blob(bytes, len) -> 32-byte hash
10// Compute SHA-256 over the input bytes.
11// If hash already present, return existing hash (DEDUP).
12// Otherwise allocate a private copy of the bytes + add an entry.
13//
14// get_blob(hash, out_buf, max_len) -> actual_len
15// Linear-scan entries for the matching hash.
16// If found: assert blob_len <= max_len; copy to out_buf;
17// return blob_len.
18// If not found OR oversized: return -1.
19//
20// has_blob(hash) -> 0/1
21//
22// V1 scope:
23// - In-memory store only (file-backed deferred to ST-2)
24// - Fixed maximum entries (NX_BLOB_STORE_CAPACITY) to keep
25// allocation deterministic
26// - Linear-scan index (O(n) per lookup); hash-table queued for ST-2
27// - Hash representation: 4 i64 fields (32 bytes total, LE-packed)
28// to avoid u8[32] in struct (NishiLang convention is i64 fields)
29//
30// Deferred per roadmap:
31// - File-backed persistence (ST-2)
32// - Open-addressing hash table for O(1) lookup (ST-2)
33// - Content-defined chunking for large blobs (ST-2.5)
34// - Free-list management; current store is APPEND-ONLY (Cardinal
35// 13 -- "tombstoned" blobs would compose via L1 journal entries
36// marking them is_current=0 rather than actually freeing)
37// - Concurrent access (multi-thread / multi-process) (ST-4+)
38//
39// genealogy_id: git_loose_objects_2005 + nix_store_2003 +
40// ipfs_2015 + cardinal_2026-05-20_storage_substrate
41// lineage_id: substrate_blob_store_v1
42//
43// nx_capability_manifest:
44// variant_class: blob_store
45// variant_id: blob_store_v1_in_memory_linear
46// requires_isa: [rv64imac, x86_64]
47// requires_syscalls: [mmap]
48// requires_ram_min_b: 32768 // initial store + entry table
49// tier_floor: NX_TIER_MOBILE
50// tier_ceiling: NX_TIER_HPC
51// cost_model:
52// flops_per_n: 50.0 // SHA-256 cost per 64-byte block
53// bytes_per_n: 1.0 // input bytes pass-through
54// syscalls_per_n: 0.0 // pure memory ops
55// adversary_class: THREAT_AI_ADVERSARY
56//
57// nx_safety_envelope:
58// intended_use: "Content-addressed blob store; foundation of
59// the storage substrate stack"
60// sil_target: SIL2
61// evidence: [canary_bracketed, sha256_keyed, dedup_proven,
62// hash_verify_on_read, append_only]
63// verdict: NOT_YET_EVALUATED
64
65import "nx_syscalls.nx"
66import "nx_sha256.nx"
67
68// ===== Constants =================================================
69const NX_BLOB_STORE_CAPACITY: i64 = 256 // V1 max entries
70const NX_BLOB_HASH_BYTES: i64 = 32 // SHA-256 digest
71
72// Verdicts.
73const NX_BLOB_OK: i64 = 0
74const NX_BLOB_BAD_INPUT: i64 = 1
75const NX_BLOB_FULL: i64 = 2
76const NX_BLOB_NOT_FOUND: i64 = 3
77const NX_BLOB_TOO_BIG: i64 = 4
78const NX_BLOB_TAMPER: i64 = 5
79
80// Canary constants (distinct from probe / calibrate / plan canaries).
81const NX_BLOB_STORE_CANARY_PRE: i64 = 0x4E58424C4F425350 // "NXBLOBSP"
82const NX_BLOB_STORE_CANARY_POST: i64 = 0x4E58424C4F424550 // "NXBLOBEP"
83
84// ===== Hash struct ===============================================
85// 32-byte SHA-256 digest packed into 4 i64 words (little-endian when
86// derived from the digest byte stream). Comparison = 4 i64 equality.
87
88struct NxBlobHash {
89 w0: i64,
90 w1: i64,
91 w2: i64,
92 w3: i64,
93}
94
95// ===== Blob entry ================================================
96// Owns: a copied byte buffer + its hash + its length.
97
98struct NxBlobEntry {
99 hash_w0: i64,
100 hash_w1: i64,
101 hash_w2: i64,
102 hash_w3: i64,
103 blob_ptr: *u8,
104 blob_len: i64,
105}
106
107// ===== Blob store ===============================================
108struct NxBlobStore {
109 canary_pre: i64,
110 n_entries: i64,
111 max_entries: i64,
112 entries: *i64, // *i64 array of NxBlobEntry pointers
113 canary_post: i64,
114}
115
116// ===== Helpers ===================================================
117
118// Read 8 bytes little-endian as i64 from buf[off..off+8].
119func _load_i64_le(buf: *u8, off: i64) -> i64 {
120 let b0: i64 = (buf[off + 0] as i64) & 255
121 let b1: i64 = (buf[off + 1] as i64) & 255
122 let b2: i64 = (buf[off + 2] as i64) & 255
123 let b3: i64 = (buf[off + 3] as i64) & 255
124 let b4: i64 = (buf[off + 4] as i64) & 255
125 let b5: i64 = (buf[off + 5] as i64) & 255
126 let b6: i64 = (buf[off + 6] as i64) & 255
127 let b7: i64 = (buf[off + 7] as i64) & 255
128 return b0
129 | (b1 << 8)
130 | (b2 << 16)
131 | (b3 << 24)
132 | (b4 << 32)
133 | (b5 << 40)
134 | (b6 << 48)
135 | (b7 << 56)
136}
137
138// Compute SHA-256 over bytes and write the 4-word hash into out.
139func _hash_bytes_to_struct(bytes: *u8, len: i64, out: *NxBlobHash) -> i64 {
140 let digest: *u8 = sys_mmap(64)
141 sha256_digest(bytes, len, digest)
142 out.w0 = _load_i64_le(digest, 0)
143 out.w1 = _load_i64_le(digest, 8)
144 out.w2 = _load_i64_le(digest, 16)
145 out.w3 = _load_i64_le(digest, 24)
146 return 0
147}
148
149// Compare an entry's hash against a query hash; 1 if equal.
150func _entry_hash_eq(e: *NxBlobEntry, h: *NxBlobHash) -> i64 {
151 if e.hash_w0 != h.w0 { return 0 }
152 if e.hash_w1 != h.w1 { return 0 }
153 if e.hash_w2 != h.w2 { return 0 }
154 if e.hash_w3 != h.w3 { return 0 }
155 return 1
156}
157
158// Linear-scan find: returns entry index or -1 on miss.
159func _find_entry_idx(store: *NxBlobStore, h: *NxBlobHash) -> i64 {
160 var i: i64 = 0
161 while i < store.n_entries {
162 let e_addr: i64 = store.entries[i]
163 if e_addr != 0 {
164 let e: *NxBlobEntry = e_addr as *NxBlobEntry
165 if _entry_hash_eq(e, h) == 1 { return i }
166 }
167 i = i + 1
168 }
169 return -1
170}
171
172// Allocate + zero an entry; copy `len` bytes from `src` into the
173// entry's owned buffer.
174func _alloc_entry(h: *NxBlobHash, src: *u8, len: i64) -> *NxBlobEntry {
175 let e: *NxBlobEntry = (sys_mmap(64)) as *NxBlobEntry
176 e.hash_w0 = h.w0
177 e.hash_w1 = h.w1
178 e.hash_w2 = h.w2
179 e.hash_w3 = h.w3
180 // Owned copy of the bytes.
181 let buf: *u8 = sys_mmap(len + 8) // +8 padding for safety
182 var k: i64 = 0
183 while k < len {
184 buf[k] = src[k]
185 k = k + 1
186 }
187 e.blob_ptr = buf
188 e.blob_len = len
189 return e
190}
191
192// ===== Construction =============================================
193func nx_blob_store_new() -> *NxBlobStore {
194 let s: *NxBlobStore = (sys_mmap(64)) as *NxBlobStore
195 s.canary_pre = NX_BLOB_STORE_CANARY_PRE
196 s.canary_post = NX_BLOB_STORE_CANARY_POST
197 s.n_entries = 0
198 s.max_entries = NX_BLOB_STORE_CAPACITY
199 s.entries = (sys_mmap(NX_BLOB_STORE_CAPACITY * 8)) as *i64
200 var i: i64 = 0
201 while i < NX_BLOB_STORE_CAPACITY {
202 s.entries[i] = 0
203 i = i + 1
204 }
205 return s
206}
207
208// ===== Validity gate ============================================
209func nx_blob_store_is_valid(s: *NxBlobStore) -> i64 {
210 if (s as i64) == 0 { return 0 }
211 if s.canary_pre != NX_BLOB_STORE_CANARY_PRE { return 0 }
212 if s.canary_post != NX_BLOB_STORE_CANARY_POST { return 0 }
213 if s.n_entries < 0 { return 0 }
214 if s.n_entries > s.max_entries { return 0 }
215 return 1
216}
217
218// ===== put_blob =================================================
219// Hash the input, dedup against existing entries, allocate if new.
220// Writes the 4-word hash into out_hash. Returns NX_BLOB_OK on
221// success or a verdict otherwise.
222
223func nx_blob_store_put(
224 store: *NxBlobStore,
225 bytes: *u8, len: i64,
226 out_hash: *NxBlobHash
227) -> i64 {
228 if nx_blob_store_is_valid(store) != 1 { return NX_BLOB_TAMPER }
229 if (out_hash as i64) == 0 { return NX_BLOB_BAD_INPUT }
230 if len < 0 { return NX_BLOB_BAD_INPUT }
231 if len > 0 {
232 if (bytes as i64) == 0 { return NX_BLOB_BAD_INPUT }
233 }
234
235 // Hash the input.
236 _hash_bytes_to_struct(bytes, len, out_hash)
237
238 // Dedup: existing entry with this hash?
239 let existing: i64 = _find_entry_idx(store, out_hash)
240 if existing >= 0 { return NX_BLOB_OK }
241
242 // Capacity check.
243 if store.n_entries >= store.max_entries { return NX_BLOB_FULL }
244
245 // Allocate + append.
246 let e: *NxBlobEntry = _alloc_entry(out_hash, bytes, len)
247 store.entries[store.n_entries] = e as i64
248 store.n_entries = store.n_entries + 1
249 return NX_BLOB_OK
250}
251
252// ===== get_blob =================================================
253// Look up by hash; copy stored bytes into out_buf. Returns the
254// blob length on success, or -1 on miss / oversized. Caller
255// supplies max_len so the store can refuse to overflow.
256
257func nx_blob_store_get(
258 store: *NxBlobStore,
259 hash: *NxBlobHash,
260 out_buf: *u8, max_len: i64
261) -> i64 {
262 if nx_blob_store_is_valid(store) != 1 { return -1 }
263 if (hash as i64) == 0 { return -1 }
264 if (out_buf as i64) == 0 { return -1 }
265 if max_len < 0 { return -1 }
266
267 let idx: i64 = _find_entry_idx(store, hash)
268 if idx < 0 { return -1 }
269 let e: *NxBlobEntry = (store.entries[idx]) as *NxBlobEntry
270 if e.blob_len > max_len { return -1 }
271
272 var k: i64 = 0
273 while k < e.blob_len {
274 out_buf[k] = e.blob_ptr[k]
275 k = k + 1
276 }
277 return e.blob_len
278}
279
280// ===== has_blob =================================================
281func nx_blob_store_has(store: *NxBlobStore, hash: *NxBlobHash) -> i64 {
282 if nx_blob_store_is_valid(store) != 1 { return 0 }
283 if (hash as i64) == 0 { return 0 }
284 let idx: i64 = _find_entry_idx(store, hash)
285 if idx < 0 { return 0 }
286 return 1
287}
288
289// ===== Entry count (audit accessor) =============================
290func nx_blob_store_count(store: *NxBlobStore) -> i64 {
291 if nx_blob_store_is_valid(store) != 1 { return -1 }
292 return store.n_entries
293}
294
295// ===== Hash equality helper (used by tests + L1+ layers) ========
296func nx_blob_hash_eq(a: *NxBlobHash, b: *NxBlobHash) -> i64 {
297 if (a as i64) == 0 { return 0 }
298 if (b as i64) == 0 { return 0 }
299 if a.w0 != b.w0 { return 0 }
300 if a.w1 != b.w1 { return 0 }
301 if a.w2 != b.w2 { return 0 }
302 if a.w3 != b.w3 { return 0 }
303 return 1
304}
305
306// ===== Hash construction (zero-initialized) =====================
307func nx_blob_hash_new() -> *NxBlobHash {
308 let h: *NxBlobHash = (sys_mmap(32)) as *NxBlobHash
309 h.w0 = 0
310 h.w1 = 0
311 h.w2 = 0
312 h.w3 = 0
313 return h
314}