nx_journal_log_test.nx source
↩ module page · 195 lines · 7876 B
1// nx_journal_log_test.nx -- smoke for nx_journal_log ST-3 MVP.
2//
3// Exercises:
4// 1. Allocation + canary + initial state
5// 2. Append + read roundtrip on 3 distinct payloads
6// 3. Count increments correctly
7// 4. read returns correct schema_id per entry
8// 5. read with oversized buffer; read out-of-range seq_no -> -1
9// 6. Empty log -> verify_chain returns -1 (trivially intact);
10// latest_hash returns NX_JOURNAL_NOT_FOUND
11// 7. Filled log -> verify_chain returns -1 (chain intact)
12// 8. latest_hash deterministic across two journals with same
13// payload sequence (cross-journal hash determinism)
14// 9. TAMPER detection: stomp entry K's blob_w0 directly ->
15// verify_chain returns K+1 (next entry's prev_hash no longer
16// matches the recomputed hash of the tampered entry)
17// 10. Canary tamper on log struct -> all ops refuse
18// 11. Bad-input gates (null log / null payload / negative len /
19// out-of-range seq_no)
20
21import "nx_syscalls.nx"
22import "nx_blob_store.nx"
23import "nx_journal_log.nx"
24
25func _fill_pattern(buf: *u8, len: i64, seed: i64) -> i64 {
26 var i: i64 = 0
27 while i < len {
28 buf[i] = (((i * 17) + seed) & 255) as u8
29 i = i + 1
30 }
31 return 0
32}
33
34func main() -> i64 {
35 let store: *NxBlobStore = nx_blob_store_new()
36 let j: *NxJournalLog = nx_journal_log_new(store)
37
38 // ----- 1. Initial state -----
39 if nx_journal_log_is_valid(j) != 1 { return 1 }
40 if nx_journal_log_count(j) != 0 { return 2 }
41
42 // ----- 2. Empty log: verify_chain = -1, latest_hash = NOT_FOUND -----
43 if nx_journal_log_verify_chain(j) != -1 { return 3 }
44 let lw0: *i64 = (sys_mmap(8)) as *i64
45 let lw1: *i64 = (sys_mmap(8)) as *i64
46 let lw2: *i64 = (sys_mmap(8)) as *i64
47 let lw3: *i64 = (sys_mmap(8)) as *i64
48 if nx_journal_log_latest_hash(j, lw0, lw1, lw2, lw3)
49 != NX_JOURNAL_NOT_FOUND { return 4 }
50
51 // ----- 3. Append 3 distinct payloads -----
52 let p1: *u8 = sys_mmap(64)
53 _fill_pattern(p1, 32, 11)
54 let seq1: i64 = nx_journal_log_append(j, p1, 32, 1001)
55 if seq1 != 0 { return 5 }
56 if nx_journal_log_count(j) != 1 { return 6 }
57
58 let p2: *u8 = sys_mmap(64)
59 _fill_pattern(p2, 32, 22)
60 let seq2: i64 = nx_journal_log_append(j, p2, 32, 1002)
61 if seq2 != 1 { return 7 }
62 if nx_journal_log_count(j) != 2 { return 8 }
63
64 let p3: *u8 = sys_mmap(64)
65 _fill_pattern(p3, 32, 33)
66 let seq3: i64 = nx_journal_log_append(j, p3, 32, 1003)
67 if seq3 != 2 { return 9 }
68 if nx_journal_log_count(j) != 3 { return 10 }
69
70 // ----- 4. Read each back; verify payload + schema_id -----
71 let out1: *u8 = sys_mmap(64)
72 let sid1: *i64 = (sys_mmap(8)) as *i64
73 let n1: i64 = nx_journal_log_read(j, 0, out1, 64, sid1)
74 if n1 != 32 { return 11 }
75 if *sid1 != 1001 { return 12 }
76 var k1: i64 = 0
77 while k1 < 32 {
78 if (out1[k1] as i64) != (p1[k1] as i64) { return 13 }
79 k1 = k1 + 1
80 }
81
82 let out2: *u8 = sys_mmap(64)
83 let sid2: *i64 = (sys_mmap(8)) as *i64
84 let n2: i64 = nx_journal_log_read(j, 1, out2, 64, sid2)
85 if n2 != 32 { return 14 }
86 if *sid2 != 1002 { return 15 }
87 var k2: i64 = 0
88 while k2 < 32 {
89 if (out2[k2] as i64) != (p2[k2] as i64) { return 16 }
90 k2 = k2 + 1
91 }
92
93 let out3: *u8 = sys_mmap(64)
94 let sid3: *i64 = (sys_mmap(8)) as *i64
95 let n3: i64 = nx_journal_log_read(j, 2, out3, 64, sid3)
96 if n3 != 32 { return 17 }
97 if *sid3 != 1003 { return 18 }
98
99 // ----- 5. Read out-of-range + oversized buffer -----
100 if nx_journal_log_read(j, -1, out1, 64, (0 as i64) as *i64) != -1 { return 19 }
101 if nx_journal_log_read(j, 3, out1, 64, (0 as i64) as *i64) != -1 { return 20 }
102 if nx_journal_log_read(j, 999, out1, 64, (0 as i64) as *i64) != -1 { return 21 }
103 let tiny_buf: *u8 = sys_mmap(8)
104 if nx_journal_log_read(j, 0, tiny_buf, 8, (0 as i64) as *i64) != -1 { return 22 }
105
106 // ----- 6. verify_chain on filled log: -1 (intact) -----
107 if nx_journal_log_verify_chain(j) != -1 { return 23 }
108
109 // ----- 7. latest_hash returns non-zero -----
110 if nx_journal_log_latest_hash(j, lw0, lw1, lw2, lw3)
111 != NX_JOURNAL_OK { return 24 }
112 // At least one of the four words must be non-zero.
113 if (*lw0 == 0) {
114 if (*lw1 == 0) {
115 if (*lw2 == 0) {
116 if (*lw3 == 0) { return 25 }
117 }
118 }
119 }
120
121 // ----- 8. Cross-journal hash determinism -----
122 // Build a second journal with the same 3 payloads in the same
123 // order; latest_hash should match (modulo ts_us differences --
124 // entry_hash INCLUDES ts_us so cross-journal hashes will differ
125 // by ts. Instead assert STRUCTURAL determinism: count + schemas
126 // + payloads + chain-intact all match).
127 let store2: *NxBlobStore = nx_blob_store_new()
128 let j2: *NxJournalLog = nx_journal_log_new(store2)
129 nx_journal_log_append(j2, p1, 32, 1001)
130 nx_journal_log_append(j2, p2, 32, 1002)
131 nx_journal_log_append(j2, p3, 32, 1003)
132 if nx_journal_log_count(j2) != 3 { return 26 }
133 if nx_journal_log_verify_chain(j2) != -1 { return 27 }
134 // Reading j2 entries returns same payloads.
135 let out2_b: *u8 = sys_mmap(64)
136 let sid2_b: *i64 = (sys_mmap(8)) as *i64
137 let n2_b: i64 = nx_journal_log_read(j2, 1, out2_b, 64, sid2_b)
138 if n2_b != 32 { return 28 }
139 if *sid2_b != 1002 { return 29 }
140
141 // ----- 9. TAMPER detection via direct entry mutation -----
142 // Stomp entry 1's blob_w0 -- entry_hash(1) changes, so entry 2's
143 // prev_w0 (which holds the OLD entry_hash(1)) no longer matches
144 // the recomputed entry_hash(1). verify_chain catches at K=2.
145 let entry1: *NxJournalEntry = (j.entries[1]) as *NxJournalEntry
146 let original_blob_w0: i64 = entry1.blob_w0
147 entry1.blob_w0 = 0xCAFEBABE
148 let broken_at: i64 = nx_journal_log_verify_chain(j)
149 if broken_at != 2 { return 30 }
150 // Restore so subsequent asserts work.
151 entry1.blob_w0 = original_blob_w0
152 if nx_journal_log_verify_chain(j) != -1 { return 31 }
153
154 // Also: stomp prev_w0 of an entry directly -- entry_hash changes
155 // -> next entry's prev_hash mismatches.
156 let entry0: *NxJournalEntry = (j.entries[0]) as *NxJournalEntry
157 let orig_prev_w0: i64 = entry0.prev_w0
158 entry0.prev_w0 = 0xDEADBEEF
159 let broken_at2: i64 = nx_journal_log_verify_chain(j)
160 if broken_at2 != 1 { return 32 }
161 entry0.prev_w0 = orig_prev_w0
162 if nx_journal_log_verify_chain(j) != -1 { return 33 }
163
164 // ----- 10. Canary tamper on the log struct itself -----
165 let tamper_store: *NxBlobStore = nx_blob_store_new()
166 let tj: *NxJournalLog = nx_journal_log_new(tamper_store)
167 nx_journal_log_append(tj, p1, 32, 100)
168 tj.canary_post = 0xDEADBEEF
169 if nx_journal_log_is_valid(tj) != 0 { return 34 }
170 // All ops refuse.
171 if nx_journal_log_append(tj, p1, 32, 100)
172 != (0 - NX_JOURNAL_TAMPER) { return 35 }
173 if nx_journal_log_count(tj) != -1 { return 36 }
174 let scratch_buf: *u8 = sys_mmap(64)
175 if nx_journal_log_read(tj, 0, scratch_buf, 64, (0 as i64) as *i64) != -1 { return 37 }
176
177 // ----- 11. Bad-input gates -----
178 let null_payload: *u8 = (0 as i64) as *u8
179 let null_log: *NxJournalLog = (0 as i64) as *NxJournalLog
180 if nx_journal_log_append(null_log, p1, 32, 0)
181 != (0 - NX_JOURNAL_TAMPER) { return 38 }
182 if nx_journal_log_append(j, null_payload, 16, 0)
183 != (0 - NX_JOURNAL_BAD_INPUT) { return 39 }
184 if nx_journal_log_append(j, p1, -1, 0)
185 != (0 - NX_JOURNAL_BAD_INPUT) { return 40 }
186 // Empty payload (len=0) IS allowed (composes with empty-blob
187 // dedup in L0).
188 let empty_buf: *u8 = sys_mmap(8)
189 let seq_empty: i64 = nx_journal_log_append(j, empty_buf, 0, 9999)
190 if seq_empty != 3 { return 41 }
191 if nx_journal_log_count(j) != 4 { return 42 }
192 if nx_journal_log_verify_chain(j) != -1 { return 43 }
193
194 return 0
195}