code wiki / (root) / nx_blob_store.nx

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}