nx_substrate_manifest.nx source
↩ module page · 274 lines · 10762 B
1// nx_substrate_manifest.nx -- declared expected-blob set + completion
2// tracking for the spore-up NETWORK phase.
3//
4// EM-3 partial milestone of NISHI_ECOSYSTEM_EVOLUTION_ROADMAP.md.
5// The spore germinates with a "Merkle root" -- a set of expected
6// blob hashes that together comprise the substrate it will assemble.
7// This primitive captures that expected set + tracks which blobs
8// have been received + verified, so the germination loop can
9// terminate when all expected blobs are present.
10//
11// Transport-agnostic: this primitive doesn't care HOW bytes arrive
12// (HTTPS / sneakernet / file / serial / Bluetooth). The caller
13// hands received bytes + the hash they CLAIM the bytes match;
14// this primitive verifies + stores + tracks.
15//
16// Composes:
17// nx_blob_store -- where verified blobs land
18// nx_sha256 -- hash verification (inside nx_blob_store_put)
19//
20// V1 scope:
21// - Fixed-array expected-hash set (up to NX_SM_MAX_EXPECTED)
22// - Per-hash received_flag: 0 (not yet) / 1 (received + verified)
23// - Ingest verifies: hash matches expected AND expected is in
24// manifest set (refuses unknown hashes -- closes the "peer can
25// push arbitrary content" attack vector)
26// - Idempotent re-ingest: ALREADY_RECEIVED is OK, not error
27// - Canary-bracketed
28//
29// Deferred per roadmap:
30// - Merkle-tree (not just flat set) for O(log n) inclusion proofs
31// - Per-blob freshness timestamps (when did we receive it?)
32// - Per-blob source attribution (which peer served it?)
33// - Larger expected set (V1 caps at 64 expected blobs)
34//
35// genealogy_id: git_pack_index_2005 + ipfs_pinning_2015 +
36// bittorrent_torrent_file_2001 +
37// cardinal_2026-05-20_ecosystem_evolution
38// lineage_id: substrate_manifest_v1
39//
40// nx_capability_manifest:
41// variant_class: substrate_manifest
42// variant_id: substrate_manifest_v1_flat_set
43// requires_isa: [rv64imac, x86_64, cortex_m, armv7a, aarch64]
44// requires_syscalls: [mmap]
45// requires_ram_min_b: 8192
46// tier_floor: NX_TIER_INF_MCU
47// tier_ceiling: NX_TIER_INF_HPC
48// cost_model:
49// flops_per_n: 50.0 // SHA-256 per ingest
50// bytes_per_n: 1.0 // received bytes pass-through
51// syscalls_per_n: 0.0
52// adversary_class: THREAT_AI_ADVERSARY
53//
54// nx_safety_envelope:
55// intended_use: "Tracks the expected-blob set for substrate
56// assembly; refuses unknown hashes; integrates
57// L0 blob_store"
58// sil_target: SIL2
59// evidence: [canary_bracketed, hash_verified_ingest,
60// unknown_hash_refused, idempotent_reingest]
61// verdict: NOT_YET_EVALUATED
62
63import "nx_syscalls.nx"
64import "nx_blob_store.nx"
65
66// ===== Constants =================================================
67const NX_SM_MAX_EXPECTED: i64 = 64
68
69// Canary distinct from all prior primitive canaries.
70const NX_SM_CANARY_PRE: i64 = 0x4E58534D414E5000 // "NXSMANP\0"
71const NX_SM_CANARY_POST: i64 = 0x4E58534D414E454E // "NXSMANEN"
72
73// ===== Verdicts ==================================================
74const NX_SM_INGESTED: i64 = 0 // first successful receive
75const NX_SM_ALREADY_RECEIVED: i64 = 1 // idempotent re-ingest
76const NX_SM_NOT_IN_MANIFEST: i64 = 2 // hash not in expected set
77const NX_SM_HASH_MISMATCH: i64 = 3 // bytes don't hash to claimed
78const NX_SM_BAD_INPUT: i64 = 4
79const NX_SM_BLOB_STORE_FULL: i64 = 5
80const NX_SM_TAMPER: i64 = 6
81const NX_SM_FULL: i64 = 7 // expected set too large
82const NX_SM_N_VERDICTS: i64 = 8
83
84func nx_sm_verdict_is_valid(v: i64) -> i64 {
85 if v < 0 { return 0 }
86 if v >= NX_SM_N_VERDICTS { return 0 }
87 return 1
88}
89
90// ===== NxSubstrateManifest =======================================
91struct NxSubstrateManifest {
92 canary_pre: i64,
93 n_expected: i64,
94 max_expected: i64,
95 n_received: i64,
96 n_rejected_unknown: i64,
97 n_rejected_tamper: i64,
98 expected_hashes: *i64, // *i64 array of NxBlobHash pointers
99 received_flags: *i64, // *i64 array of 0/1 per expected slot
100 blob_store: *NxBlobStore, // L0 backing
101 canary_post: i64,
102}
103
104// ===== Construction ==============================================
105func nx_substrate_manifest_new(blob_store: *NxBlobStore) -> *NxSubstrateManifest {
106 if (blob_store as i64) == 0 { return (0 as i64) as *NxSubstrateManifest }
107 let m: *NxSubstrateManifest = (sys_mmap(96)) as *NxSubstrateManifest
108 m.canary_pre = NX_SM_CANARY_PRE
109 m.canary_post = NX_SM_CANARY_POST
110 m.n_expected = 0
111 m.max_expected = NX_SM_MAX_EXPECTED
112 m.n_received = 0
113 m.n_rejected_unknown = 0
114 m.n_rejected_tamper = 0
115 m.expected_hashes = (sys_mmap(NX_SM_MAX_EXPECTED * 8)) as *i64
116 m.received_flags = (sys_mmap(NX_SM_MAX_EXPECTED * 8)) as *i64
117 var i: i64 = 0
118 while i < NX_SM_MAX_EXPECTED {
119 m.expected_hashes[i] = 0
120 m.received_flags[i] = 0
121 i = i + 1
122 }
123 m.blob_store = blob_store
124 return m
125}
126
127// ===== Validity gate ============================================
128func nx_substrate_manifest_is_valid(m: *NxSubstrateManifest) -> i64 {
129 if (m as i64) == 0 { return 0 }
130 if m.canary_pre != NX_SM_CANARY_PRE { return 0 }
131 if m.canary_post != NX_SM_CANARY_POST { return 0 }
132 if m.n_expected < 0 { return 0 }
133 if m.n_expected > m.max_expected { return 0 }
134 if m.n_received < 0 { return 0 }
135 if m.n_received > m.n_expected { return 0 }
136 if (m.blob_store as i64) == 0 { return 0 }
137 return 1
138}
139
140// ===== Add an expected hash (manifest authoring) ================
141// Operator declares "this hash is expected to arrive." Adds to the
142// manifest. Refuses on duplicate / full / null.
143func nx_substrate_manifest_add_expected(
144 m: *NxSubstrateManifest,
145 hash: *NxBlobHash
146) -> i64 {
147 if nx_substrate_manifest_is_valid(m) != 1 { return NX_SM_TAMPER }
148 if (hash as i64) == 0 { return NX_SM_BAD_INPUT }
149 if m.n_expected >= m.max_expected { return NX_SM_FULL }
150
151 // Refuse duplicate.
152 var i: i64 = 0
153 while i < m.n_expected {
154 let h_addr: i64 = m.expected_hashes[i]
155 if h_addr != 0 {
156 let h: *NxBlobHash = h_addr as *NxBlobHash
157 if nx_blob_hash_eq(h, hash) == 1 {
158 return NX_SM_ALREADY_RECEIVED // re-add is idempotent
159 }
160 }
161 i = i + 1
162 }
163 // Append. Store a COPY of the hash so manifest survives caller
164 // freeing the original.
165 let copy: *NxBlobHash = nx_blob_hash_new()
166 copy.w0 = hash.w0
167 copy.w1 = hash.w1
168 copy.w2 = hash.w2
169 copy.w3 = hash.w3
170 m.expected_hashes[m.n_expected] = copy as i64
171 m.received_flags[m.n_expected] = 0
172 m.n_expected = m.n_expected + 1
173 return NX_SM_INGESTED
174}
175
176// ===== Look up expected slot index by hash =====================
177// Returns slot index 0..n_expected-1 on hit, -1 on miss.
178func _sm_find_expected_idx(m: *NxSubstrateManifest, hash: *NxBlobHash) -> i64 {
179 var i: i64 = 0
180 while i < m.n_expected {
181 let h_addr: i64 = m.expected_hashes[i]
182 if h_addr != 0 {
183 let h: *NxBlobHash = h_addr as *NxBlobHash
184 if nx_blob_hash_eq(h, hash) == 1 { return i }
185 }
186 i = i + 1
187 }
188 return -1
189}
190
191// ===== Ingest =================================================
192// Caller hands bytes + the hash they claim the bytes match.
193//
194// Decision tree:
195// - Validate inputs + canary
196// - Verify the bytes ACTUALLY hash to claimed_hash (rejects
197// corrupted transit). We do this by put_blob into the
198// blob_store which computes SHA-256, then compare returned
199// hash to claimed.
200// - Verify claimed_hash is in the expected set (rejects
201// adversarial peer pushing arbitrary content).
202// - On success: set received_flags[i]=1; increment n_received
203// (only on first receipt -- idempotent re-ingest).
204func nx_substrate_manifest_ingest(
205 m: *NxSubstrateManifest,
206 claimed_hash: *NxBlobHash,
207 bytes: *u8, len: i64
208) -> i64 {
209 if nx_substrate_manifest_is_valid(m) != 1 { return NX_SM_TAMPER }
210 if (claimed_hash as i64) == 0 { return NX_SM_BAD_INPUT }
211 if len < 0 { return NX_SM_BAD_INPUT }
212 if len > 0 {
213 if (bytes as i64) == 0 { return NX_SM_BAD_INPUT }
214 }
215
216 // Step 1: check if claimed_hash is in the expected set.
217 let slot: i64 = _sm_find_expected_idx(m, claimed_hash)
218 if slot < 0 {
219 m.n_rejected_unknown = m.n_rejected_unknown + 1
220 return NX_SM_NOT_IN_MANIFEST
221 }
222
223 // Step 2: hash the bytes via L0 put_blob (which computes SHA-256
224 // and dedups). Compare the computed hash to claimed.
225 let computed_hash: *NxBlobHash = nx_blob_hash_new()
226 let rc_put: i64 = nx_blob_store_put(m.blob_store, bytes, len, computed_hash)
227 if rc_put == NX_BLOB_FULL { return NX_SM_BLOB_STORE_FULL }
228 if rc_put != NX_BLOB_OK { return NX_SM_BAD_INPUT }
229
230 if nx_blob_hash_eq(computed_hash, claimed_hash) != 1 {
231 // Bytes don't hash to claimed -- transit corruption or
232 // adversarial peer. put_blob already stored the corrupt
233 // bytes under their TRUE hash; that's fine (dedup blob store
234 // is content-addressed; the bad bytes are content-named by
235 // their actual hash, not the claimed one). We just refuse
236 // to mark the manifest slot as received.
237 m.n_rejected_tamper = m.n_rejected_tamper + 1
238 return NX_SM_HASH_MISMATCH
239 }
240
241 // Step 3: success. Idempotent: re-ingest is ALREADY_RECEIVED,
242 // not error.
243 if m.received_flags[slot] == 1 {
244 return NX_SM_ALREADY_RECEIVED
245 }
246 m.received_flags[slot] = 1
247 m.n_received = m.n_received + 1
248 return NX_SM_INGESTED
249}
250
251// ===== Completion query =========================================
252func nx_substrate_manifest_is_complete(m: *NxSubstrateManifest) -> i64 {
253 if nx_substrate_manifest_is_valid(m) != 1 { return 0 }
254 if m.n_expected == 0 { return 0 } // empty manifest is not "complete"
255 if m.n_received == m.n_expected { return 1 }
256 return 0
257}
258
259// ===== Missing count ============================================
260func nx_substrate_manifest_n_missing(m: *NxSubstrateManifest) -> i64 {
261 if nx_substrate_manifest_is_valid(m) != 1 { return -1 }
262 return m.n_expected - m.n_received
263}
264
265// ===== Audit accessors ==========================================
266func nx_substrate_manifest_n_rejected_unknown(m: *NxSubstrateManifest) -> i64 {
267 if nx_substrate_manifest_is_valid(m) != 1 { return -1 }
268 return m.n_rejected_unknown
269}
270
271func nx_substrate_manifest_n_rejected_tamper(m: *NxSubstrateManifest) -> i64 {
272 if nx_substrate_manifest_is_valid(m) != 1 { return -1 }
273 return m.n_rejected_tamper
274}