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>>.