File size: 7,279 Bytes
ebed3db | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | %%%-------------------------------------------------------------------
%% @doc SEB P/NP Bridge — Erlang gen_server
%%
%% Watches the convergence_log.jsonl, translates each entry into a
%% SEB event, and appends it to the WAL kernel via seb_kernel_nif.
%%
%% Event type codes (from SEB_Protocol.idr EventTypeRegistry):
%% 0x0400 PROBLEM_SOLVED — positive universeSumDelta
%% 0x0401 ATTACK_DETECTED — negative universeSumDelta
%% 0x0402 CHAIN_VERIFY — periodic integrity check
%%
%% Payload layout (64 bytes, matches seb_convergence.mjs):
%% [0:8] event_type uint64 LE
%% [8:16] timestamp uint64 LE nanoseconds
%% [16:48] problem_id SHA-256 of problemId string
%% [48:56] delta float64 LE
%% [56:64] reserved zeros
%%
%% On ATTACK_DETECTED: emits the event, then calls verify_chain.
%% If verify_chain fails: supervisor escalates to Compromised state.
%% @end
%%%-------------------------------------------------------------------
-module(seb_pnp_bridge).
-behaviour(gen_server).
-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(POLL_MS, 10000). %% check convergence_log every 10s
-define(ATTACK_THRESHOLD, -1.0). %% universe sum below this = halt
-record(state, {
conv_log :: string(), %% path to convergence_log.jsonl
last_pos :: non_neg_integer(), %% byte offset read so far
kernel :: reference(), %% seb_kernel_nif handle
universe_sum :: float()
}).
%%%===================================================================
%%% API
%%%===================================================================
start_link(ConvLogPath) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [ConvLogPath], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([ConvLogPath]) ->
{ok, Handle} = seb_kernel_nif:init_kernel(4, 0), %% segment 4, seq 0 (L4 = bridge layer)
erlang:send_after(?POLL_MS, self(), poll),
{ok, #state{
conv_log = ConvLogPath,
last_pos = 0,
kernel = Handle,
universe_sum = 0.0
}}.
handle_info(poll, State) ->
NewState = poll_and_seal(State),
erlang:send_after(?POLL_MS, self(), poll),
{noreply, NewState};
handle_info(_Info, State) ->
{noreply, State}.
handle_call(_Req, _From, State) ->
{reply, ok, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%%===================================================================
%%% Internal
%%%===================================================================
poll_and_seal(#state{conv_log = Path, last_pos = Pos,
kernel = Handle, universe_sum = Sum} = State) ->
case file:open(Path, [read, binary]) of
{error, _} ->
State;
{ok, Fd} ->
{ok, _} = file:position(Fd, Pos),
{Lines, NewPos} = read_lines(Fd, Pos),
file:close(Fd),
process_entries(Lines, State#state{last_pos = NewPos})
end.
read_lines(Fd, Pos) ->
read_lines(Fd, Pos, []).
read_lines(Fd, Pos, Acc) ->
case file:read_line(Fd) of
eof -> {lists:reverse(Acc), Pos};
{error, _} -> {lists:reverse(Acc), Pos};
{ok, Line} ->
{ok, NewPos} = file:position(Fd, cur),
read_lines(Fd, NewPos, [Line | Acc])
end.
process_entries([], State) -> State;
process_entries([Line | Rest], State) ->
case catch jiffy:decode(Line, [return_maps]) of
{'EXIT', _} ->
process_entries(Rest, State);
Entry ->
NewState = seal_entry(Entry, State),
process_entries(Rest, NewState)
end.
seal_entry(Entry, #state{kernel = Handle, universe_sum = Sum} = State) ->
Delta = maps:get(<<"universeSumDelta">>, Entry, 0.0),
ProblemId = maps:get(<<"problemId">>, Entry, <<>>),
Timestamp = maps:get(<<"timestamp">>, Entry, <<>>),
NewSum = Sum + Delta,
%% Build 64-byte payload
Payload = build_payload(Entry, Delta),
%% Build minimal header (68 bytes matching seb_types.ads)
EventType = if Delta < 0 -> 16#0401; true -> 16#0400 end,
Header = build_header(EventType, byte_size(Payload)),
%% Build footer: prev_hash=tip, event_hash=circuit(tip||header[0:64]), sig=zeros
{ok, PrevTip} = seb_kernel_nif:get_tip_hash(Handle),
%% commitment computed by NIF internally — send zeros for event_hash, NIF fills it
Footer = <<PrevTip/binary, (binary:copy(<<0>>, 32))/binary, (binary:copy(<<0>>, 64))/binary>>,
case seb_kernel_nif:append_event(Handle, Header, Payload, Footer) of
{ok, Offset} ->
if Delta < 0 ->
error_logger:warning_msg(
"seb_pnp_bridge: ATTACK EVENT sealed at offset ~p, delta=~p, problem=~s~n",
[Offset, Delta, ProblemId]),
handle_attack(Handle, NewSum);
true ->
ok
end;
{error, Reason} ->
error_logger:error_msg(
"seb_pnp_bridge: failed to seal ~s: ~p~n", [ProblemId, Reason])
end,
State#state{universe_sum = NewSum}.
handle_attack(Handle, Sum) ->
%% Verify full chain integrity
case seb_kernel_nif:verify_chain(Handle) of
{ok, Count} ->
error_logger:warning_msg(
"seb_pnp_bridge: chain intact (~p records), sum=~p~n", [Count, Sum]);
{error, Reason} ->
error_logger:error_msg(
"seb_pnp_bridge: CHAIN INTEGRITY FAILURE: ~p — escalating~n", [Reason]),
exit({chain_integrity_failure, Sum})
end,
%% Hard halt if universe sum crosses threshold
if Sum < ?ATTACK_THRESHOLD ->
error_logger:error_msg(
"seb_pnp_bridge: universe_sum=~p < threshold ~p — HALT~n",
[Sum, ?ATTACK_THRESHOLD]),
exit({attack_threshold_exceeded, Sum});
true -> ok
end.
%% Build 68-byte event header
build_header(EventType, PayloadSize) ->
Timestamp = erlang:system_time(nanosecond),
<<EventType:64/little-unsigned,
Timestamp:64/little-unsigned,
0:64/little-unsigned, %% agent_id (bridge agent = 0)
PayloadSize:32/little-unsigned,
0:32/little-unsigned, %% partition_id
0:64/little-unsigned, %% prev_offset (kernel tracks)
0:64/little-unsigned, %% sequence_no
0:64/little-unsigned, %% reserved
0:32/little-unsigned, %% reserved2
0:32/little-unsigned>>. %% reserved3
%% Build 64-byte payload (matches seb_convergence.mjs layout)
build_payload(Entry, Delta) ->
EventType = if Delta < 0 -> 16#0401; true -> 16#0400 end,
Timestamp = erlang:system_time(nanosecond),
ProblemId = maps:get(<<"problemId">>, Entry, <<>>),
PidHash = crypto:hash(sha256, ProblemId),
DeltaBin = <<Delta:64/little-float>>,
Reserved = binary:copy(<<0>>, 8),
<<EventType:64/little-unsigned,
Timestamp:64/little-unsigned,
PidHash/binary,
DeltaBin/binary,
Reserved/binary>>.
|