nx_h2_stream.nx source
↩ module page · 381 lines · 20238 B
1// nx_h2_stream.nx -- TUTOR-BOOTSTRAP SCAFFOLD (Claude, authored under the R4-H2
2// HTTP/2-transport-ladder workflow), NOT credited as team self-authoring.
3//
4// R4-H2-004 of the sovereign HTTP/2 transport ladder
5// (knowledge/specs/2026-06-13-http2-transport-ladder.md). The HTTP/2 stream
6// state machine (RFC 9113 §5.1). Unlike the lower rungs this organ emits no
7// wire bytes -- it is the BEHAVIOURAL automaton that RFC 9113 §5.1 dictates:
8// idle -> open -> half-closed(local|remote) -> closed, driven by the HEADERS /
9// DATA / RST_STREAM / END_STREAM events the frame codec produces and consumes.
10// The TRANSITION TABLE is the allowed internet-boundary requirement (a peer that
11// disagrees about the legal transitions cannot interoperate, exactly as it must
12// agree on the frame bytes); the IMPLEMENTATION is pure NishiLang via
13// nx_cc -> nxasm_x86: no gcc, no openssl, no nghttp2.
14//
15// FOUNDED ON (composes, does not reinvent -- anti-orphan law):
16// - nx_h2_frame.nx (R4-H2-003, GREEN): we import it EXACTLY ONCE. It
17// transitively splices nx_hpack.nx -> nx_str.nx -> syscalls.nx, so we
18// inherit sys_mmap / sys_write / sys_exit / sys_openat_append / sys_close
19// from that single import (importing nx_hpack / nx_str / nx_syscalls
20// directly TOO would be the RC6 double-import landmine). The frame type
21// codes (DATA=0x00, HEADERS=0x01, RST_STREAM=0x03) and the END_STREAM flag
22// (0x01) that DRIVE this automaton are exactly the ones nx_h2_frame.nx
23// emits, so the SM and the codec cannot disagree (single source of truth).
24// No floating capability: the frame codec founds this stream machine; flow
25// control (R4-H2-005) founds on this next.
26//
27// BACK-FILL: the team RE-AUTHORS this from the DATA spec via the
28// emitter-of-emitters (X-AUT-006c/e/f) -- this hand-authored scaffold is the
29// sanctioned one-time bootstrap only (meter-integrity, mirror
30// nx_frame_codec.nx:13-16 / nx_h2_frame.nx:31-34).
31//
32// GATE (main): asserts the RFC 9113 §5.1 transition table is the "known answer".
33// The canonical client-GET no-body trajectory: new(1)==IDLE,
34// send_headers(IDLE, END_STREAM)==HC_LOCAL (open transited instantaneously per
35// the "immediately half-closed" rule), recv_headers(HC_LOCAL)==HC_LOCAL,
36// recv_data(HC_LOCAL, END_STREAM)==CLOSED. The with-body branch:
37// send_headers(IDLE, no-END_STREAM)==OPEN, send_data(OPEN, END_STREAM)==HC_LOCAL.
38// RST from any non-idle state -> CLOSED. Stream-id parity: 1/3/0x7FFFFFFF are
39// client (odd, nonzero, <=2^31-1), 0/2 are not. PLUS TAMPER / illegal-
40// transition cases on a SEPARATE state value (the canonical KAT states are never
41// mutated by the tamper checks): new(2) (even server id) < 0, new(0) < 0,
42// send_data(IDLE) (DATA before HEADERS, PROTOCOL_ERROR class) < 0,
43// recv_end_stream(CLOSED) (no transition out of closed) < 0, and any frame
44// driven from CLOSED is rejected.
45//
46// license_tier: INDEPENDENT_REDERIVE
47// genealogy_id: international-research-sources/ietf/rfc_9113
48// lineage_id: nishi_h2_stream_r4h2_004
49
50import "nx_h2_frame.nx"
51
52// =====================================================================
53// Stream states (sealed enum, RFC 9113 §5.1). reserved(local|remote) are
54// PUSH_PROMISE-only and never appear on the client-GET path, so this client SM
55// models the five GET-relevant states; an unknown integer is NOT a valid state.
56// =====================================================================
57// H2_STREAM_IDLE 0
58// H2_STREAM_OPEN 1
59// H2_STREAM_HC_LOCAL 2 (half-closed (local) -- we sent END_STREAM)
60// H2_STREAM_HC_REMOTE 3 (half-closed (remote) -- peer sent END_STREAM)
61// H2_STREAM_CLOSED 4
62// Rejection codes (negative; PROTOCOL_ERROR class per RFC 9113 §5.1):
63// -1 = illegal transition for the current state
64// -2 = invalid stream id (parity / range)
65// -3 = invalid input state
66
67func h2_stream_state_is_valid(state: i64) -> i64 {
68 if state == 0 { return 1 }
69 if state == 1 { return 1 }
70 if state == 2 { return 1 }
71 if state == 3 { return 1 }
72 if state == 4 { return 1 }
73 return 0
74}
75
76// Stream ids are unsigned 31-bit (§5.1.1). Client-initiated MUST be odd,
77// nonzero, and <= 2^31-1 (0x7FFFFFFF). Stream 0 is connection control (not a
78// request); even ids are server-initiated.
79func h2_stream_id_is_client(stream_id: i64) -> i64 {
80 if stream_id <= 0 { return 0 } // 0 = connection control; negative invalid
81 if stream_id > 0x7fffffff { return 0 } // exceeds the 31-bit space
82 if (stream_id & 1) == 0 { return 0 } // even = server-initiated
83 return 1
84}
85
86// new: create a client stream. Validates the id is a legal client id; the
87// stream begins in IDLE (§5.1). Returns H2_STREAM_IDLE (0) or < 0 (-2 bad id).
88func h2_stream_new(stream_id: i64) -> i64 {
89 if h2_stream_id_is_client(stream_id) == 0 { return 0 - 2 }
90 return 0 // H2_STREAM_IDLE
91}
92
93// send HEADERS (§5.1). Legal ONLY from idle (the client opens the stream).
94// idle --[HEADERS, END_STREAM]--> half-closed(local) (open transited
95// instantaneously: "An endpoint sending an END_STREAM flag causes the
96// stream state to become half-closed", §5.1)
97// idle --[HEADERS, no END_STREAM]--> open
98// From any other state HEADERS-send is illegal here (-1).
99func h2_stream_send_headers(state: i64, end_stream: i64) -> i64 {
100 if state != 0 { return 0 - 1 } // HEADERS-send only from IDLE
101 if end_stream != 0 { return 2 } // -> HC_LOCAL
102 return 1 // -> OPEN
103}
104
105// recv HEADERS (§5.1). For a client GET the response HEADERS arrive while the
106// stream is half-closed(local); they do NOT change our local state unless they
107// carry END_STREAM. Legal from open, half-closed(local), half-closed(remote).
108// half-closed(local) --[HEADERS, no END_STREAM]--> half-closed(local) (stays)
109// half-closed(local) --[HEADERS, END_STREAM]----> closed
110// open --[HEADERS, no END_STREAM]--> open
111// open --[HEADERS, END_STREAM]----> half-closed(remote)
112// Illegal from idle (server cannot open our client stream) and from closed.
113func h2_stream_recv_headers(state: i64, end_stream: i64) -> i64 {
114 if state == 2 { // HC_LOCAL
115 if end_stream != 0 { return 4 } // -> CLOSED
116 return 2 // stays HC_LOCAL
117 }
118 if state == 1 { // OPEN
119 if end_stream != 0 { return 3 } // -> HC_REMOTE
120 return 1 // stays OPEN
121 }
122 if state == 3 { // HC_REMOTE: peer already done; END_STREAM again illegal
123 if end_stream != 0 { return 0 - 1 }
124 return 3
125 }
126 return 0 - 1 // idle / closed: illegal
127}
128
129// send DATA (§5.1 / §6.1). DATA is legal only when WE may still send (open).
130// open --[DATA, END_STREAM]--> half-closed(local)
131// open --[DATA, no END_STREAM]--> open
132// DATA before HEADERS (state idle) is a PROTOCOL_ERROR; DATA from
133// half-closed(local) (we already sent END_STREAM) or closed is illegal.
134func h2_stream_send_data(state: i64, end_stream: i64) -> i64 {
135 if state != 1 { return 0 - 1 } // DATA-send only from OPEN
136 if end_stream != 0 { return 2 } // -> HC_LOCAL
137 return 1 // stays OPEN
138}
139
140// recv DATA (§5.1 / §6.1). The peer may send DATA while WE are half-closed
141// (local) or while the stream is open.
142// half-closed(local) --[DATA, END_STREAM]--> closed
143// half-closed(local) --[DATA, no END_STREAM]--> half-closed(local)
144// open --[DATA, END_STREAM]--> half-closed(remote)
145// open --[DATA, no END_STREAM]--> open
146// DATA on idle (before any HEADERS) is a PROTOCOL_ERROR; DATA on closed is
147// STREAM_CLOSED -- both rejected (-1).
148func h2_stream_recv_data(state: i64, end_stream: i64) -> i64 {
149 if state == 2 { // HC_LOCAL
150 if end_stream != 0 { return 4 } // -> CLOSED
151 return 2 // stays HC_LOCAL
152 }
153 if state == 1 { // OPEN
154 if end_stream != 0 { return 3 } // -> HC_REMOTE
155 return 1 // stays OPEN
156 }
157 return 0 - 1 // idle / hc_remote / closed: illegal
158}
159
160// recv END_STREAM as a standalone event (§5.1): the peer signalled it is done.
161// This is the closing edge for the canonical GET (the response's final frame
162// carries END_STREAM).
163// half-closed(local) --[recv END_STREAM]--> closed
164// open --[recv END_STREAM]--> half-closed(remote)
165// From idle (nothing started), half-closed(remote) (peer already done), or
166// closed there is no further transition (-1).
167func h2_stream_recv_end_stream(state: i64) -> i64 {
168 if state == 2 { return 4 } // HC_LOCAL -> CLOSED
169 if state == 1 { return 3 } // OPEN -> HC_REMOTE
170 return 0 - 1 // idle / hc_remote / closed: no transition
171}
172
173// send RST_STREAM (§5.1 / §6.4): an abrupt close. Legal from any NON-idle
174// state (you cannot RST a stream you never opened); always -> closed.
175func h2_stream_send_rst(state: i64) -> i64 {
176 if state == 0 { return 0 - 1 } // RST on IDLE illegal
177 if h2_stream_state_is_valid(state) == 0 { return 0 - 3 }
178 return 4 // -> CLOSED
179}
180
181// =====================================================================
182// GATE (main): the RFC 9113 §5.1 transition table is the known-answer. Each
183// check increments tot; pass increments only on the exact expected transition.
184// =====================================================================
185func st_puts(s: *u8) -> i64 {
186 var n: i64 = 0
187 while s[n] != (0 as u8) { n = n + 1 }
188 sys_write(1, s, n)
189 return 0
190}
191func st_putn(v: i64) -> i64 {
192 let bb: *u8 = sys_mmap(28)
193 var m: i64 = v
194 if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) }
195 let t: *u8 = sys_mmap(28)
196 var k: i64 = 0
197 if m == 0 { t[0] = 48 as u8; k = 1 }
198 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
199 var i: i64 = 0
200 while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }
201 sys_write(1, bb, k)
202 return 0
203}
204// fd-aware decimal writer (the h2_fdn pattern from nx_h2_frame.nx:85) -- used
205// ONLY by the durable status-line append at the end of the gate.
206func st_fdn(fd: i64, v: i64) -> i64 {
207 let bb: *u8 = sys_mmap(28)
208 let t: *u8 = sys_mmap(28)
209 var m: i64 = v
210 if m < 0 { m = 0 - m }
211 var k: i64 = 0
212 if m == 0 { t[0] = 48 as u8; k = 1 }
213 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
214 var i: i64 = 0
215 while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }
216 sys_write(fd, bb, k)
217 return 0
218}
219// one labelled equality check: PASS iff got==exp.
220func st_check(name: *u8, got: i64, exp: i64) -> i64 {
221 if got == exp {
222 st_puts(" PASS " as *u8); st_puts(name)
223 st_puts(" (== " as *u8); st_putn(exp); st_puts(")\n" as *u8)
224 return 1
225 }
226 st_puts(" FAIL " as *u8); st_puts(name)
227 st_puts(" got=" as *u8); st_putn(got)
228 st_puts(" exp=" as *u8); st_putn(exp); st_puts("\n" as *u8)
229 return 0
230}
231
232func main() -> i64 {
233 var pass: i64 = 0
234 var tot: i64 = 0
235 st_puts("nx_h2_stream gate (RFC 9113 5.1 stream state machine, FOUNDED on nx_h2_frame)\n" as *u8)
236
237 // names for readability in the asserts
238 let IDLE: i64 = 0
239 let OPEN: i64 = 1
240 let HC_LOCAL: i64 = 2
241 let HC_REMOTE: i64 = 3
242 let CLOSED: i64 = 4
243
244 // ---- stream-id parity (§5.1.1) ----
245 pass = pass + st_check("id_is_client(1)" as *u8, h2_stream_id_is_client(1), 1); tot = tot + 1
246 pass = pass + st_check("id_is_client(3)" as *u8, h2_stream_id_is_client(3), 1); tot = tot + 1
247 pass = pass + st_check("id_is_client(2) [server even]" as *u8, h2_stream_id_is_client(2), 0); tot = tot + 1
248 pass = pass + st_check("id_is_client(0) [conn ctrl]" as *u8, h2_stream_id_is_client(0), 0); tot = tot + 1
249 pass = pass + st_check("id_is_client(0x7FFFFFFF) [max]" as *u8, h2_stream_id_is_client(0x7fffffff), 1); tot = tot + 1
250
251 // ---- state validity ----
252 pass = pass + st_check("state_is_valid(IDLE)" as *u8, h2_stream_state_is_valid(IDLE), 1); tot = tot + 1
253 pass = pass + st_check("state_is_valid(CLOSED)" as *u8, h2_stream_state_is_valid(CLOSED), 1); tot = tot + 1
254 pass = pass + st_check("state_is_valid(99) [unknown]" as *u8, h2_stream_state_is_valid(99), 0); tot = tot + 1
255
256 // =================================================================
257 // CANONICAL CLIENT-GET (no-body) TRAJECTORY -- the headline KAT. Drive a
258 // single stream through the exact RFC §5.1 sequence and assert each state.
259 // new(1)==IDLE
260 // send HEADERS(END_STREAM) IDLE -> HC_LOCAL
261 // recv HEADERS(no END_STREAM) HC_LOCAL -> HC_LOCAL (response :status 200)
262 // recv DATA(END_STREAM) HC_LOCAL -> CLOSED
263 // =================================================================
264 var s: i64 = h2_stream_new(1)
265 pass = pass + st_check("GET: new(1)==IDLE" as *u8, s, IDLE); tot = tot + 1
266 s = h2_stream_send_headers(s, 1)
267 pass = pass + st_check("GET: send_HEADERS(END_STREAM)==HC_LOCAL" as *u8, s, HC_LOCAL); tot = tot + 1
268 s = h2_stream_recv_headers(s, 0)
269 pass = pass + st_check("GET: recv_HEADERS(no-END)==HC_LOCAL (stays)" as *u8, s, HC_LOCAL); tot = tot + 1
270 s = h2_stream_recv_data(s, 1)
271 pass = pass + st_check("GET: recv_DATA(END_STREAM)==CLOSED" as *u8, s, CLOSED); tot = tot + 1
272
273 // alternate close edge: recv_end_stream from HC_LOCAL -> CLOSED (when the
274 // closing response frame is a HEADERS-trailer or empty DATA carrying END_STREAM
275 // handled via the standalone END_STREAM event).
276 var s2: i64 = h2_stream_send_headers(h2_stream_new(1), 1) // IDLE -> HC_LOCAL
277 pass = pass + st_check("GET-alt: setup HC_LOCAL" as *u8, s2, HC_LOCAL); tot = tot + 1
278 s2 = h2_stream_recv_end_stream(s2)
279 pass = pass + st_check("GET-alt: recv_end_stream(HC_LOCAL)==CLOSED" as *u8, s2, CLOSED); tot = tot + 1
280
281 // =================================================================
282 // WITH-BODY (POST-style) BRANCH -- HEADERS without END_STREAM opens the
283 // stream, then DATA with END_STREAM half-closes (local).
284 // send HEADERS(no END_STREAM) IDLE -> OPEN
285 // send DATA(END_STREAM) OPEN -> HC_LOCAL
286 // =================================================================
287 var sb: i64 = h2_stream_new(1)
288 sb = h2_stream_send_headers(sb, 0)
289 pass = pass + st_check("BODY: send_HEADERS(no-END)==OPEN" as *u8, sb, OPEN); tot = tot + 1
290 sb = h2_stream_send_data(sb, 1)
291 pass = pass + st_check("BODY: send_DATA(END_STREAM)==HC_LOCAL" as *u8, sb, HC_LOCAL); tot = tot + 1
292
293 // RST from non-idle states -> CLOSED (§6.4)
294 pass = pass + st_check("send_rst(OPEN)==CLOSED" as *u8, h2_stream_send_rst(OPEN), CLOSED); tot = tot + 1
295 pass = pass + st_check("send_rst(HC_LOCAL)==CLOSED" as *u8, h2_stream_send_rst(HC_LOCAL), CLOSED); tot = tot + 1
296 pass = pass + st_check("send_rst(HC_REMOTE)==CLOSED" as *u8, h2_stream_send_rst(HC_REMOTE), CLOSED); tot = tot + 1
297
298 // =================================================================
299 // TAMPER / ILLEGAL-TRANSITION CASES -- each evaluated on FRESH state values;
300 // the canonical KAT states above (s, s2, sb) are NEVER mutated by these
301 // checks (defect-(c) discipline: the KAT path and the rejection path must not
302 // be entangled). A rejection is a strictly-negative return.
303 // =================================================================
304
305 // T1: new(2) -- even (server) id used by a client -> rejected (< 0)
306 let t1: i64 = h2_stream_new(2)
307 var t1ok: i64 = 0
308 if t1 < 0 { t1ok = 1 }
309 if t1ok == 1 { st_puts(" PASS tamper new(2) [even server id] rejected (rc=" as *u8); st_putn(t1); st_puts(")\n" as *u8); pass = pass + 1 }
310 if t1ok == 0 { st_puts(" FAIL tamper new(2) NOT rejected (rc=" as *u8); st_putn(t1); st_puts(")\n" as *u8) }
311 tot = tot + 1
312
313 // T2: new(0) -- stream 0 is connection control, never a request -> rejected
314 let t2: i64 = h2_stream_new(0)
315 var t2ok: i64 = 0
316 if t2 < 0 { t2ok = 1 }
317 if t2ok == 1 { st_puts(" PASS tamper new(0) [conn-ctrl id] rejected (rc=" as *u8); st_putn(t2); st_puts(")\n" as *u8); pass = pass + 1 }
318 if t2ok == 0 { st_puts(" FAIL tamper new(0) NOT rejected (rc=" as *u8); st_putn(t2); st_puts(")\n" as *u8) }
319 tot = tot + 1
320
321 // T3: send_data(IDLE) -- DATA before HEADERS (PROTOCOL_ERROR class) -> rejected
322 let t3: i64 = h2_stream_send_data(IDLE, 1)
323 var t3ok: i64 = 0
324 if t3 < 0 { t3ok = 1 }
325 if t3ok == 1 { st_puts(" PASS tamper send_DATA(IDLE) [DATA before HEADERS] rejected (rc=" as *u8); st_putn(t3); st_puts(")\n" as *u8); pass = pass + 1 }
326 if t3ok == 0 { st_puts(" FAIL tamper send_DATA(IDLE) NOT rejected (rc=" as *u8); st_putn(t3); st_puts(")\n" as *u8) }
327 tot = tot + 1
328
329 // T4: recv_end_stream(CLOSED) -- no transition out of closed -> rejected
330 let t4: i64 = h2_stream_recv_end_stream(CLOSED)
331 var t4ok: i64 = 0
332 if t4 < 0 { t4ok = 1 }
333 if t4ok == 1 { st_puts(" PASS tamper recv_end_stream(CLOSED) [frame after closed] rejected (rc=" as *u8); st_putn(t4); st_puts(")\n" as *u8); pass = pass + 1 }
334 if t4ok == 0 { st_puts(" FAIL tamper recv_end_stream(CLOSED) NOT rejected (rc=" as *u8); st_putn(t4); st_puts(")\n" as *u8) }
335 tot = tot + 1
336
337 // T5: any frame driven from CLOSED is rejected -- recv_data on CLOSED -> < 0
338 let t5: i64 = h2_stream_recv_data(CLOSED, 1)
339 var t5ok: i64 = 0
340 if t5 < 0 { t5ok = 1 }
341 if t5ok == 1 { st_puts(" PASS tamper recv_DATA(CLOSED) [STREAM_CLOSED] rejected (rc=" as *u8); st_putn(t5); st_puts(")\n" as *u8); pass = pass + 1 }
342 if t5ok == 0 { st_puts(" FAIL tamper recv_DATA(CLOSED) NOT rejected (rc=" as *u8); st_putn(t5); st_puts(")\n" as *u8) }
343 tot = tot + 1
344
345 // T6: send_rst(IDLE) -- cannot RST a stream you never opened -> rejected
346 let t6: i64 = h2_stream_send_rst(IDLE)
347 var t6ok: i64 = 0
348 if t6 < 0 { t6ok = 1 }
349 if t6ok == 1 { st_puts(" PASS tamper send_rst(IDLE) rejected (rc=" as *u8); st_putn(t6); st_puts(")\n" as *u8); pass = pass + 1 }
350 if t6ok == 0 { st_puts(" FAIL tamper send_rst(IDLE) NOT rejected (rc=" as *u8); st_putn(t6); st_puts(")\n" as *u8) }
351 tot = tot + 1
352
353 // sanity: the canonical states were NOT corrupted by the tamper block
354 pass = pass + st_check("KAT-untouched: GET path still CLOSED" as *u8, s, CLOSED); tot = tot + 1
355
356 st_puts("---- h2_stream gate: passed " as *u8); st_putn(pass); st_puts(" / " as *u8); st_putn(tot); st_puts("\n" as *u8)
357 if pass == tot {
358 // DURABLE EVIDENCE (mirror nx_h2_frame.nx:586): stdout evaporates and
359 // cannot anchor a row_markers entry -- append ONE marker/reconcile-
360 // recognised status line. Reached ONLY when every KAT + all six tamper
361 // cases pass (pass == tot).
362 let lfd: i64 = sys_openat_append("knowledge/status/h2_nx_h2_stream.log" as *u8, 0x1a4)
363 if lfd >= 0 {
364 sys_write(lfd, "R4-H2-004-GATE organ=nx_h2_stream kats=" as *u8, 39)
365 st_fdn(lfd, pass); sys_write(lfd, "/" as *u8, 1); st_fdn(lfd, tot)
366 sys_write(lfd, " tamper=ok verdict=GREEN\n" as *u8, 25)
367 sys_close(lfd)
368 }
369 sys_exit(0)
370 }
371 // RED path: still write a durable line so the failure is anchored, then exit 1.
372 let rfd: i64 = sys_openat_append("knowledge/status/h2_nx_h2_stream.log" as *u8, 0x1a4)
373 if rfd >= 0 {
374 sys_write(rfd, "R4-H2-004-GATE organ=nx_h2_stream kats=" as *u8, 39)
375 st_fdn(rfd, pass); sys_write(rfd, "/" as *u8, 1); st_fdn(rfd, tot)
376 sys_write(rfd, " tamper=?? verdict=RED\n" as *u8, 23)
377 sys_close(rfd)
378 }
379 sys_exit(1)
380 return 0
381}