package keeper import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/oy/openyield/x/bearers/types" ) // transport.go holds the store-backed BearerTransport impl (P2-02-01, // REQ-034, A-522). The v0.2 BearerTransport Go interface (Send, Receive, // Status) gains a store-backed runtime impl: the keeper IS the transport // for simtest purposes — no hardware/RF Go libraries (D-054). // // The transport wraps the keeper's session store. Send appends a frame to // the session's Frames slice. Receive marks the frame received (and // transitions the session Open → Active on first ack). Status reports // whether the session is Open or Active (i.e., still carrying traffic). // // Surveillance-resistant invariant (A-522): the transport carries NO // geolocation / sender physical location fields. The surveillance-resistant // locked const on OYSATLink/OYLRLink is a runtime invariant — the transport // MUST NOT emit geolocation in events. A negative simtest asserts the event // set contains NO geolocation fields. // StoreTransport is the store-backed BearerTransport impl. It wraps a // Keeper + the sdk.Context (bound at construction so the BearerTransport // interface methods can stay parameterless per the v0.2 interface contract). // The transport operates on a single session-id (a transport instance is // scoped to one session — the bearer is a per-session handle in the simtest // runtime). type StoreTransport struct { keeper Keeper ctx sdk.Context sessionID string } // NewStoreTransport constructs a store-backed BearerTransport scoped to the // named session. The session must already exist (Open or Active). The // transport reads/writes the session's Frames slice via the keeper store. func NewStoreTransport(k Keeper, ctx sdk.Context, sessionID string) *StoreTransport { return &StoreTransport{keeper: k, ctx: ctx, sessionID: sessionID} } // Compile-time assertion: StoreTransport satisfies the v0.2 BearerTransport // interface (D-029, REQ-034). The interface contract is Send/Receive/Status // (parameterless except Send takes a payload). var _ types.BearerTransport = (*StoreTransport)(nil) // Send dispatches a payload via the bearer. The store-backed impl appends // the payload as a new Frame on the session's Frames slice. Returns an // error if the session is not found or is terminal (Closed/Revoked) — a // terminal session rejects further Send calls. func (t *StoreTransport) Send(payload []byte) error { s, ok := t.keeper.GetSession(t.ctx, t.sessionID) if !ok { return fmt.Errorf("bearers: session %q not found", t.sessionID) } if s.IsTerminal() { return fmt.Errorf("bearers: session %q is terminal (%s), rejects Send", t.sessionID, s.Status) } frame := types.Frame{ FrameID: fmt.Sprintf("%s-frame-%d", t.sessionID, len(s.Frames)+1), SenderReach: s.InitiatorReach, PayloadBytes: payload, SentAt: t.ctx.BlockTime().Unix(), } s.Frames = append(s.Frames, frame) t.keeper.SetSession(t.ctx, s) t.ctx.EventManager().EmitEvent(sdk.NewEvent( "bearers.frame_sent", sdk.NewAttribute("session_id", t.sessionID), sdk.NewAttribute("frame_id", frame.FrameID), sdk.NewAttribute("sender_reach", frame.SenderReach), // NO geolocation (A-522 surveillance-resistant invariant). )) return nil } // Receive accepts an inbound payload from the bearer. The store-backed impl // marks the first unreceived frame as Received and transitions the session // Open → Active on the first ack. Returns the payload and an error if the // bearer has no inbound (unreceived) payload or the session is terminal. func (t *StoreTransport) Receive() ([]byte, error) { s, ok := t.keeper.GetSession(t.ctx, t.sessionID) if !ok { return nil, fmt.Errorf("bearers: session %q not found", t.sessionID) } if s.IsTerminal() { return nil, fmt.Errorf("bearers: session %q is terminal (%s), rejects Receive", t.sessionID, s.Status) } // Find the first unreceived frame. var received *types.Frame for i := range s.Frames { if !s.Frames[i].Received { s.Frames[i].Received = true received = &s.Frames[i] break } } if received == nil { return nil, fmt.Errorf("bearers: no inbound frame on session %q", t.sessionID) } // Open → Active on the first ack. if s.Status == types.SessionOpen { s.Status = types.SessionActive } t.keeper.SetSession(t.ctx, s) t.ctx.EventManager().EmitEvent(sdk.NewEvent( "bearers.frame_received", sdk.NewAttribute("session_id", t.sessionID), sdk.NewAttribute("frame_id", received.FrameID), sdk.NewAttribute("status", string(s.Status)), // NO geolocation (A-522 surveillance-resistant invariant). )) return received.PayloadBytes, nil } // Status reports the bearer's current reachability (true = reachable). The // store-backed impl reports true iff the session exists and is Open or // Active (still carrying traffic). A terminal or missing session is // unreachable. func (t *StoreTransport) Status() bool { s, ok := t.keeper.GetSession(t.ctx, t.sessionID) if !ok { return false } return s.Status == types.SessionOpen || s.Status == types.SessionActive }