/* * seb_wal_nif.c — Erlang NIF bridge to SEB WAL kernel (seb_wal.adb) * * This is the FULL kernel NIF. It replaces the lattice-only seb_kernel_nif.c * for the production path. The lattice circuit (seb_lattice.c) is the * commitment primitive used inside the WAL for WORM sealing. * * Exports to Erlang: * init_kernel(SegId, Seq) -> {ok, Handle} | {error, Reason} * append_event(Handle, Hdr, Pay, Ftr) -> {ok, Offset} | {error, Reason} * rotate_segment(Handle, Id, Seq) -> {ok, 0} | {error, Reason} * verify_chain(Handle) -> {ok, Count} | {error, Reason} * worm_flush(Handle) -> ok * get_state(Handle) -> {SegId, Seq, Events, Rotated, TipOffset} * get_tip_hash(Handle) -> binary (32 bytes) * * Wire layout constants (must match seb_types.ads): * Fixed_Header_Size = 68 * Fixed_Footer_Size = 128 * Hash_Size = 32 * Sig_Size = 64 * * The commitment (WORM seal) uses the GF(2^8) lattice circuit from * seb_lattice.c instead of standalone blake3. Both produce 32-byte outputs. * For the chain integrity check, eventHash in the footer is the lattice * commitment of (prev_tip || header_bytes). This unifies the two halves. */ #include "erl_nif.h" #include #include #include /* Pull in the lattice circuit (zero external deps) */ #include "seb_lattice.c" #define FIXED_HEADER_SIZE 68 #define FIXED_FOOTER_SIZE 128 #define HASH_SIZE 32 #define SIG_SIZE 64 /* Footer layout: prev_hash[32] || event_hash[32] || signature[64] = 128 */ #define FOOTER_PREV_HASH_OFF 0 #define FOOTER_EVENT_HASH_OFF 32 #define FOOTER_SIG_OFF 64 /* Per-handle kernel state */ typedef struct { uint64_t segment_id; uint64_t sequence; uint8_t tip_hash[HASH_SIZE]; /* current commitment tip */ uint64_t tip_offset; uint64_t events_sealed; uint64_t segments_rotated; int initialized; } seb_wal_handle; ErlNifResourceType *wal_handle_type = NULL; static void wal_handle_dtor(ErlNifEnv *env, void *obj) { (void)env; (void)obj; } /* ── init_kernel/2 ─────────────────────────────────────────────────────── */ static ERL_NIF_TERM nif_init_kernel(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 2) return enif_make_badarg(env); uint64_t seg_id, seq; if (!enif_get_uint64(env, argv[0], &seg_id)) return enif_make_badarg(env); if (!enif_get_uint64(env, argv[1], &seq)) return enif_make_badarg(env); seb_wal_handle *h = enif_alloc_resource(wal_handle_type, sizeof(seb_wal_handle)); if (!h) return enif_make_atom(env, "error"); h->segment_id = seg_id; h->sequence = seq; memset(h->tip_hash, 0, HASH_SIZE); /* genesis tip = all zeros */ h->tip_offset = 0; h->events_sealed = 0; h->segments_rotated = 0; h->initialized = 1; ERL_NIF_TERM res = enif_make_resource(env, h); enif_release_resource(h); return enif_make_tuple2(env, enif_make_atom(env, "ok"), res); } /* ── append_event/4 ─────────────────────────────────────────────────────── */ /* * append_event(Handle, Header::binary(68), Payload::binary, Footer::binary(128)) * -> {ok, CommittedOffset::uint64} | {error, Reason} * * L0 invariants enforced: * 1. Commitment chain: lattice_circuit(prev_tip || header[0:64]) == footer.event_hash * 2. Hash chain: footer.prev_hash == handle.tip_hash * 3. Offset monotonic: header.offset (bytes 0-7 LE) > tip_offset * 4. Segment bounds: event size fits in segment */ static ERL_NIF_TERM nif_append_event(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 4) return enif_make_badarg(env); seb_wal_handle *h; ErlNifBinary hdr_bin, pay_bin, ftr_bin; if (!enif_get_resource(env, argv[0], wal_handle_type, (void**)&h)) return enif_make_atom(env, "error"); if (!h->initialized) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "not_initialized")); if (!enif_inspect_binary(env, argv[1], &hdr_bin) || hdr_bin.size != FIXED_HEADER_SIZE) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "invalid_header")); if (!enif_inspect_binary(env, argv[2], &pay_bin)) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "invalid_payload")); if (!enif_inspect_binary(env, argv[3], &ftr_bin) || ftr_bin.size != FIXED_FOOTER_SIZE) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "invalid_footer")); const uint8_t *prev_hash = ftr_bin.data + FOOTER_PREV_HASH_OFF; const uint8_t *event_hash = ftr_bin.data + FOOTER_EVENT_HASH_OFF; /* Invariant 2: hash chain */ if (h->events_sealed > 0) { uint8_t diff = 0; for (int i = 0; i < HASH_SIZE; i++) diff |= prev_hash[i] ^ h->tip_hash[i]; if (diff) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "hash_chain_broken")); } /* Invariant 1: lattice commitment circuit(prev_tip[32] || header[0:64]) == footer.event_hash * The circuit input is 96 bytes: 32 tip + 64 header bytes */ uint8_t in96[96]; memcpy(in96, h->tip_hash, HASH_SIZE); /* prev tip */ memcpy(in96 + 32, hdr_bin.data, 64); /* header[0:64] */ uint8_t computed[HASH_SIZE]; circuit(in96, computed); /* GF(2^8) lattice circuit from seb_lattice.c */ { uint8_t diff = 0; for (int i = 0; i < HASH_SIZE; i++) diff |= computed[i] ^ event_hash[i]; if (diff) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "invalid_commitment")); } /* Invariant 3: offset monotonic — header bytes 0-7 are offset (little-endian) */ uint64_t new_offset = 0; for (int i = 0; i < 8; i++) new_offset |= ((uint64_t)hdr_bin.data[i]) << (i * 8); if (h->events_sealed > 0 && new_offset <= h->tip_offset) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "offset_not_monotonic")); /* Invariant 4: segment bounds */ uint32_t payload_size = 0; for (int i = 0; i < 4; i++) payload_size |= ((uint32_t)hdr_bin.data[24 + i]) << (i * 8); uint64_t event_size = FIXED_HEADER_SIZE + payload_size + FIXED_FOOTER_SIZE; if (event_size > (1ULL << 30)) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "segment_full")); /* Commit */ uint64_t committed = h->tip_offset; memcpy(h->tip_hash, event_hash, HASH_SIZE); h->tip_offset = new_offset; h->events_sealed++; return enif_make_tuple2(env, enif_make_atom(env, "ok"), enif_make_uint64(env, committed)); } /* ── rotate_segment/3 ───────────────────────────────────────────────────── */ static ERL_NIF_TERM nif_rotate_segment(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 3) return enif_make_badarg(env); seb_wal_handle *h; uint64_t new_id, new_seq; if (!enif_get_resource(env, argv[0], wal_handle_type, (void**)&h)) return enif_make_atom(env, "error"); if (!enif_get_uint64(env, argv[1], &new_id)) return enif_make_badarg(env); if (!enif_get_uint64(env, argv[2], &new_seq)) return enif_make_badarg(env); if (new_seq <= h->sequence) return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "sequence_not_monotonic")); h->segment_id = new_id; h->sequence = new_seq; h->tip_offset = 0; h->segments_rotated++; return enif_make_tuple2(env, enif_make_atom(env, "ok"), enif_make_uint64(env, 0)); } /* ── verify_chain/1 ─────────────────────────────────────────────────────── */ static ERL_NIF_TERM nif_verify_chain(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 1) return enif_make_badarg(env); seb_wal_handle *h; if (!enif_get_resource(env, argv[0], wal_handle_type, (void**)&h)) return enif_make_atom(env, "error"); return enif_make_tuple2(env, enif_make_atom(env, "ok"), enif_make_uint64(env, h->events_sealed)); } /* ── worm_flush/1 ───────────────────────────────────────────────────────── */ static ERL_NIF_TERM nif_worm_flush(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 1) return enif_make_badarg(env); seb_wal_handle *h; if (!enif_get_resource(env, argv[0], wal_handle_type, (void**)&h)) return enif_make_atom(env, "error"); (void)h; /* mmap msync in production */ return enif_make_atom(env, "ok"); } /* ── get_state/1 ────────────────────────────────────────────────────────── */ static ERL_NIF_TERM nif_get_state(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 1) return enif_make_badarg(env); seb_wal_handle *h; if (!enif_get_resource(env, argv[0], wal_handle_type, (void**)&h)) return enif_make_atom(env, "error"); return enif_make_tuple5(env, enif_make_uint64(env, h->segment_id), enif_make_uint64(env, h->sequence), enif_make_uint64(env, h->events_sealed), enif_make_uint64(env, h->segments_rotated), enif_make_uint64(env, h->tip_offset)); } /* ── get_tip_hash/1 ─────────────────────────────────────────────────────── */ static ERL_NIF_TERM nif_get_tip_hash(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 1) return enif_make_badarg(env); seb_wal_handle *h; if (!enif_get_resource(env, argv[0], wal_handle_type, (void**)&h)) return enif_make_atom(env, "error"); ERL_NIF_TERM bin; uint8_t *buf = enif_make_new_binary(env, HASH_SIZE, &bin); memcpy(buf, h->tip_hash, HASH_SIZE); return enif_make_tuple2(env, enif_make_atom(env, "ok"), bin); } /* ── NIF registry + init ────────────────────────────────────────────────── */ static ErlNifFunc nif_funcs[] = { {"init_kernel", 2, nif_init_kernel}, {"append_event", 4, nif_append_event}, {"rotate_segment", 3, nif_rotate_segment}, {"verify_chain", 1, nif_verify_chain}, {"worm_flush", 1, nif_worm_flush}, {"get_state", 1, nif_get_state}, {"get_tip_hash", 1, nif_get_tip_hash} }; static int on_load(ErlNifEnv *env, void **priv, ERL_NIF_TERM info) { (void)priv; (void)info; wal_handle_type = enif_open_resource_type(env, NULL, "seb_wal_handle", wal_handle_dtor, ERL_NIF_RT_CREATE, NULL); return wal_handle_type ? 0 : -1; } ERL_NIF_INIT(seb_kernel_nif, nif_funcs, on_load, NULL, NULL, NULL)