code wiki / (root) / nx_h2_stream.nx

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}