Merge phase/02 into milestone/v0.5-bearers-runtime (P2 complete → v0.4.2)
---ci--- project: oy phase: 2 milestone: v0.5 status: complete requirements: covered: [REQ-034] partial: [] ---/ci---
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/oy/openyield/x/bearers/types"
|
||||
)
|
||||
|
||||
// keeper.go holds the store-backed Keeper for the bearers module (P2-02-01,
|
||||
// REQ-034).
|
||||
//
|
||||
// The Keeper wraps an sdk.KVStore via a storeKey. It holds the Session
|
||||
// records (by session-id) and the OYQRCode records (by qr-id). The Keeper
|
||||
// also holds the expected-keeper shim (BreadKeeper for the OY-QR consume
|
||||
// transfer effect). The shim is an interface (G-003 — no struct import of
|
||||
// x/bread/types); the concrete x/bread keeper satisfies it structurally.
|
||||
//
|
||||
// State-machine ordering (vision §7, enforced in every handler):
|
||||
// ValidateBasic → keeper authz → state mutation → ctx.EventManager().EmitEvent
|
||||
//
|
||||
// Surveillance-resistant invariant (A-522): the Keeper carries NO
|
||||
// geolocation fields; the handlers emit NO geolocation in events.
|
||||
|
||||
// Keeper is the store-backed bearers keeper.
|
||||
type Keeper struct {
|
||||
cdc codec.Codec
|
||||
storeKey storetypes.StoreKey
|
||||
breadKeeper types.BreadKeeper
|
||||
}
|
||||
|
||||
// NewKeeper constructs a new store-backed bearers Keeper. The BreadKeeper
|
||||
// expected-keeper shim is injected (nil-able for partial tests; the
|
||||
// ConsumeOYQR handler guards a nil shim and skips the transfer effect,
|
||||
// still flipping the consumed flag — the A-521 state-write-first invariant
|
||||
// holds regardless).
|
||||
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, bk types.BreadKeeper) Keeper {
|
||||
return Keeper{
|
||||
cdc: cdc,
|
||||
storeKey: storeKey,
|
||||
breadKeeper: bk,
|
||||
}
|
||||
}
|
||||
|
||||
// SetBreadKeeper sets the BreadKeeper expected-keeper shim (for
|
||||
// post-construction wiring, e.g., app wiring or test setup).
|
||||
func (k *Keeper) SetBreadKeeper(bk types.BreadKeeper) { k.breadKeeper = bk }
|
||||
|
||||
// --- Session store -----------------------------------------------------------
|
||||
|
||||
var sessionKeyPrefix = []byte("session/")
|
||||
|
||||
func sessionKey(sessionID string) []byte {
|
||||
return append(sessionKeyPrefix, []byte(sessionID)...)
|
||||
}
|
||||
|
||||
// GetSession loads a Session by session-id. Returns the session and true
|
||||
// if found, or zero value + false if not.
|
||||
func (k Keeper) GetSession(ctx sdk.Context, sessionID string) (types.Session, bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(sessionKey(sessionID))
|
||||
if bz == nil {
|
||||
return types.Session{}, false
|
||||
}
|
||||
var s types.Session
|
||||
if err := json.Unmarshal(bz, &s); err != nil {
|
||||
return types.Session{}, false
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
// SetSession persists a Session by session-id.
|
||||
func (k Keeper) SetSession(ctx sdk.Context, s types.Session) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("bearers: marshal session %q: %v", s.SessionID, err))
|
||||
}
|
||||
store.Set(sessionKey(s.SessionID), bz)
|
||||
}
|
||||
|
||||
// AllSessions returns all persisted Session records (iteration helper).
|
||||
func (k Keeper) AllSessions(ctx sdk.Context) []types.Session {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
iterator := store.Iterator(sessionKeyPrefix, prefixEnd(sessionKeyPrefix))
|
||||
defer iterator.Close()
|
||||
out := []types.Session{}
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var s types.Session
|
||||
if err := json.Unmarshal(iterator.Value(), &s); err == nil {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// --- OYQRCode store ----------------------------------------------------------
|
||||
|
||||
var qrKeyPrefix = []byte("qr/")
|
||||
|
||||
func qrKey(qrID string) []byte {
|
||||
return append(qrKeyPrefix, []byte(qrID)...)
|
||||
}
|
||||
|
||||
// GetOYQRCode loads an OYQRCode by qr-id. Returns the QR and true if found.
|
||||
func (k Keeper) GetOYQRCode(ctx sdk.Context, qrID string) (types.OYQRCode, bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(qrKey(qrID))
|
||||
if bz == nil {
|
||||
return types.OYQRCode{}, false
|
||||
}
|
||||
var q types.OYQRCode
|
||||
if err := json.Unmarshal(bz, &q); err != nil {
|
||||
return types.OYQRCode{}, false
|
||||
}
|
||||
return q, true
|
||||
}
|
||||
|
||||
// SetOYQRCode persists an OYQRCode by qr-id.
|
||||
func (k Keeper) SetOYQRCode(ctx sdk.Context, q types.OYQRCode) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz, err := json.Marshal(q)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("bearers: marshal qr %q: %v", q.QRID, err))
|
||||
}
|
||||
store.Set(qrKey(q.QRID), bz)
|
||||
}
|
||||
|
||||
// AllOYQRCodes returns all persisted OYQRCode records (iteration helper).
|
||||
func (k Keeper) AllOYQRCodes(ctx sdk.Context) []types.OYQRCode {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
iterator := store.Iterator(qrKeyPrefix, prefixEnd(qrKeyPrefix))
|
||||
defer iterator.Close()
|
||||
out := []types.OYQRCode{}
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var q types.OYQRCode
|
||||
if err := json.Unmarshal(iterator.Value(), &q); err == nil {
|
||||
out = append(out, q)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// prefixEnd returns the key that sorts immediately after all keys sharing the
|
||||
// given prefix (the standard prefix-iteration end key: increment the last
|
||||
// byte, drop overflow). Used for store.Iterator(start, prefixEnd(start))
|
||||
// prefix scans.
|
||||
func prefixEnd(prefix []byte) []byte {
|
||||
if len(prefix) == 0 {
|
||||
return nil
|
||||
}
|
||||
end := make([]byte, len(prefix))
|
||||
copy(end, prefix)
|
||||
for i := len(end) - 1; i >= 0; i-- {
|
||||
end[i]++
|
||||
if end[i] != 0 {
|
||||
return end
|
||||
}
|
||||
}
|
||||
// All bytes were 0xFF; return nil (iterate to end of store).
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/oy/openyield/x/bearers/types"
|
||||
)
|
||||
|
||||
// msg_server.go implements the bearers module's MsgServer (G-023 ownership
|
||||
// split: cosmos-engineer scaffolds the file structure + method signatures;
|
||||
// mesh-engineer/backend-engineer implements the handler logic bodies). The
|
||||
// MsgServer wraps the Keeper + the BreadKeeper expected-keeper shim (already
|
||||
// on the Keeper).
|
||||
//
|
||||
// Each method returns a (*Response, error). Handler state-machine ordering
|
||||
// is enforced: ValidateBasic → keeper authz → state mutation →
|
||||
// ctx.EventManager().EmitEvent.
|
||||
//
|
||||
// Surveillance-resistant invariant (A-522): NO handler emits geolocation or
|
||||
// sender physical location. The surveillance-resistant locked const on
|
||||
// OYSATLink/OYLRLink is a runtime invariant — a handler that emits
|
||||
// geolocation violates it. A negative simtest asserts the event set
|
||||
// contains NO geolocation fields.
|
||||
//
|
||||
// One-shot OY-QR (A-521): the MsgConsumeOYQR handler flips consumed BEFORE
|
||||
// the transfer effect (state write FIRST, then the BreadKeeper shim call).
|
||||
// A replay finds consumed==true and returns an error (idempotent reject,
|
||||
// NOT double-effect). The SDK store is atomic per tx — a panic in the
|
||||
// transfer rolls back the whole tx, so the order is safe; the order
|
||||
// documents intent and matches the ibc-go delete-before-mint convention.
|
||||
|
||||
// msgServer is the concrete MsgServer implementation wrapping the Keeper.
|
||||
type msgServer struct {
|
||||
Keeper
|
||||
}
|
||||
|
||||
// NewMsgServerImpl returns the bearers MsgServer for the provided Keeper.
|
||||
func NewMsgServerImpl(k Keeper) types.MsgServer {
|
||||
return &msgServer{Keeper: k}
|
||||
}
|
||||
|
||||
var _ types.MsgServer = msgServer{}
|
||||
|
||||
// unwrapCtx extracts the sdk.Context from the interface-typed ctx.
|
||||
func unwrapCtx(ctx interface{}) sdk.Context {
|
||||
if c, ok := ctx.(sdk.Context); ok {
|
||||
return c
|
||||
}
|
||||
panic(fmt.Sprintf("bearers: expected sdk.Context, got %T", ctx))
|
||||
}
|
||||
|
||||
// nowUnix returns the current block time as unix seconds from the ctx.
|
||||
func nowUnix(ctx sdk.Context) int64 {
|
||||
return ctx.BlockTime().Unix()
|
||||
}
|
||||
|
||||
// --- OpenSession (creates Session status=Open) -------------------------------
|
||||
|
||||
// OpenSession creates a new Session with status=Open. ValidateBasic is
|
||||
// stateless; the handler enforces idempotency (session-id must not already
|
||||
// exist).
|
||||
func (s msgServer) OpenSession(ctx interface{}, msg *types.MsgOpenSession) (*types.MsgOpenSessionResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
// Idempotency: session-id must not already exist.
|
||||
if _, ok := s.Keeper.GetSession(sdkCtx, msg.SessionID); ok {
|
||||
return nil, fmt.Errorf("bearers: session %q already exists", msg.SessionID)
|
||||
}
|
||||
|
||||
session := types.Session{
|
||||
SessionID: msg.SessionID,
|
||||
BearerType: msg.BearerType,
|
||||
InitiatorReach: msg.InitiatorReach,
|
||||
PeerReach: msg.PeerReach,
|
||||
Status: types.SessionOpen,
|
||||
Frames: []types.Frame{},
|
||||
TTL: msg.TTL,
|
||||
OpenedAt: nowUnix(sdkCtx),
|
||||
}
|
||||
s.Keeper.SetSession(sdkCtx, session)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"bearers.session_opened",
|
||||
sdk.NewAttribute("session_id", msg.SessionID),
|
||||
sdk.NewAttribute("bearer_type", string(msg.BearerType)),
|
||||
sdk.NewAttribute("initiator_reach", msg.InitiatorReach),
|
||||
sdk.NewAttribute("peer_reach", msg.PeerReach),
|
||||
sdk.NewAttribute("status", string(types.SessionOpen)),
|
||||
// NO geolocation (A-522 surveillance-resistant invariant).
|
||||
))
|
||||
return &types.MsgOpenSessionResponse{}, nil
|
||||
}
|
||||
|
||||
// --- CloseSession (Active → Closed) ------------------------------------------
|
||||
|
||||
// CloseSession transitions an Active session to Closed. The handler
|
||||
// enforces the stateful source-status check (must be Open or Active; an
|
||||
// Open session with no frames can close directly).
|
||||
func (s msgServer) CloseSession(ctx interface{}, msg *types.MsgCloseSession) (*types.MsgCloseSessionResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
session, ok := s.Keeper.GetSession(sdkCtx, msg.SessionID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bearers: session %q not found", msg.SessionID)
|
||||
}
|
||||
if session.IsTerminal() {
|
||||
return nil, fmt.Errorf("bearers: session %q is terminal (%s), cannot close", msg.SessionID, session.Status)
|
||||
}
|
||||
|
||||
session.Status = types.SessionClosed
|
||||
session.ClosedAt = nowUnix(sdkCtx)
|
||||
s.Keeper.SetSession(sdkCtx, session)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"bearers.session_closed",
|
||||
sdk.NewAttribute("session_id", msg.SessionID),
|
||||
sdk.NewAttribute("status", string(types.SessionClosed)),
|
||||
))
|
||||
return &types.MsgCloseSessionResponse{}, nil
|
||||
}
|
||||
|
||||
// --- RevokeSession (out-of-band → Revoked) -----------------------------------
|
||||
|
||||
// RevokeSession transitions a session to Revoked (out-of-band termination).
|
||||
// A revoked session rejects further Receive. The handler enforces the
|
||||
// stateful source-status check (must not already be terminal).
|
||||
func (s msgServer) RevokeSession(ctx interface{}, msg *types.MsgRevokeSession) (*types.MsgRevokeSessionResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
session, ok := s.Keeper.GetSession(sdkCtx, msg.SessionID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bearers: session %q not found", msg.SessionID)
|
||||
}
|
||||
if session.IsTerminal() {
|
||||
return nil, fmt.Errorf("bearers: session %q is terminal (%s), cannot revoke", msg.SessionID, session.Status)
|
||||
}
|
||||
|
||||
session.Status = types.SessionRevoked
|
||||
session.ClosedAt = nowUnix(sdkCtx)
|
||||
s.Keeper.SetSession(sdkCtx, session)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"bearers.session_revoked",
|
||||
sdk.NewAttribute("session_id", msg.SessionID),
|
||||
sdk.NewAttribute("status", string(types.SessionRevoked)),
|
||||
))
|
||||
return &types.MsgRevokeSessionResponse{}, nil
|
||||
}
|
||||
|
||||
// --- SendOYSATFrame (send a frame on an Open/Active session) ------------------
|
||||
|
||||
// SendOYSATFrame sends a frame on an OY-SAT session. The handler enforces
|
||||
// the stateful session-status check: the session must be Open or Active
|
||||
// (frames on Closed/Revoked are REJECTED — the rejected-frame case).
|
||||
func (s msgServer) SendOYSATFrame(ctx interface{}, msg *types.MsgSendOYSATFrame) (*types.MsgSendOYSATFrameResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
session, ok := s.Keeper.GetSession(sdkCtx, msg.SessionID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bearers: session %q not found", msg.SessionID)
|
||||
}
|
||||
if session.IsTerminal() {
|
||||
// Rejected-frame case: a frame received on a Closed/Revoked
|
||||
// session MUST be rejected (A-523 session state machine).
|
||||
return nil, fmt.Errorf("bearers: session %q is terminal (%s), rejects frame", msg.SessionID, session.Status)
|
||||
}
|
||||
if session.IsExpired(nowUnix(sdkCtx)) {
|
||||
// TTL expiry transitions the session to Closed (the handler
|
||||
// enforces expiry on Send/Receive checks).
|
||||
session.Status = types.SessionClosed
|
||||
session.ClosedAt = nowUnix(sdkCtx)
|
||||
s.Keeper.SetSession(sdkCtx, session)
|
||||
return nil, fmt.Errorf("bearers: session %q expired (ttl %d), rejects frame", msg.SessionID, session.TTL)
|
||||
}
|
||||
|
||||
frame := types.Frame{
|
||||
FrameID: msg.FrameID,
|
||||
SenderReach: msg.Signer,
|
||||
PayloadBytes: msg.PayloadBytes,
|
||||
SentAt: nowUnix(sdkCtx),
|
||||
}
|
||||
session.Frames = append(session.Frames, frame)
|
||||
s.Keeper.SetSession(sdkCtx, session)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"bearers.frame_sent",
|
||||
sdk.NewAttribute("session_id", msg.SessionID),
|
||||
sdk.NewAttribute("frame_id", msg.FrameID),
|
||||
sdk.NewAttribute("sender_reach", msg.Signer),
|
||||
// NO geolocation (A-522 surveillance-resistant invariant).
|
||||
))
|
||||
return &types.MsgSendOYSATFrameResponse{}, nil
|
||||
}
|
||||
|
||||
// --- ReceiveOYSATFrame (ack a frame; Open → Active on first ack) -------------
|
||||
|
||||
// ReceiveOYSATFrame acknowledges receipt of an OY-SAT frame. The handler
|
||||
// transitions the session Open → Active on the first ack. The handler
|
||||
// enforces the stateful session-status check: the session must be Open or
|
||||
// Active (acks on Closed/Revoked are REJECTED — the rejected-frame case).
|
||||
func (s msgServer) ReceiveOYSATFrame(ctx interface{}, msg *types.MsgReceiveOYSATFrame) (*types.MsgReceiveOYSATFrameResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
session, ok := s.Keeper.GetSession(sdkCtx, msg.SessionID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bearers: session %q not found", msg.SessionID)
|
||||
}
|
||||
if session.IsTerminal() {
|
||||
// Rejected-frame case: an ack received on a Closed/Revoked
|
||||
// session MUST be rejected (A-523 session state machine).
|
||||
return nil, fmt.Errorf("bearers: session %q is terminal (%s), rejects ack", msg.SessionID, session.Status)
|
||||
}
|
||||
if session.IsExpired(nowUnix(sdkCtx)) {
|
||||
session.Status = types.SessionClosed
|
||||
session.ClosedAt = nowUnix(sdkCtx)
|
||||
s.Keeper.SetSession(sdkCtx, session)
|
||||
return nil, fmt.Errorf("bearers: session %q expired (ttl %d), rejects ack", msg.SessionID, session.TTL)
|
||||
}
|
||||
|
||||
// Find the named frame; mark it received.
|
||||
found := false
|
||||
for i := range session.Frames {
|
||||
if session.Frames[i].FrameID == msg.FrameID {
|
||||
session.Frames[i].Received = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("bearers: frame %q not found on session %q", msg.FrameID, msg.SessionID)
|
||||
}
|
||||
|
||||
// Open → Active on the first ack.
|
||||
if session.Status == types.SessionOpen {
|
||||
session.Status = types.SessionActive
|
||||
}
|
||||
s.Keeper.SetSession(sdkCtx, session)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"bearers.frame_received",
|
||||
sdk.NewAttribute("session_id", msg.SessionID),
|
||||
sdk.NewAttribute("frame_id", msg.FrameID),
|
||||
sdk.NewAttribute("status", string(session.Status)),
|
||||
// NO geolocation (A-522 surveillance-resistant invariant).
|
||||
))
|
||||
return &types.MsgReceiveOYSATFrameResponse{}, nil
|
||||
}
|
||||
|
||||
// --- IssueOYQR (issue a one-shot OY-QR, consumed=false) ----------------------
|
||||
|
||||
// IssueOYQR issues a one-shot OY-QR (consumed=false). The handler enforces
|
||||
// idempotency (qr-id must not already exist).
|
||||
func (s msgServer) IssueOYQR(ctx interface{}, msg *types.MsgIssueOYQR) (*types.MsgIssueOYQRResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
// Idempotency: qr-id must not already exist.
|
||||
if _, ok := s.Keeper.GetOYQRCode(sdkCtx, msg.QRID); ok {
|
||||
return nil, fmt.Errorf("bearers: qr %q already exists", msg.QRID)
|
||||
}
|
||||
|
||||
qr := types.OYQRCode{
|
||||
QRID: msg.QRID,
|
||||
PayloadBytes: msg.PayloadBytes,
|
||||
Consumed: false,
|
||||
IssuerReachID: msg.IssuerReachID,
|
||||
AmountGrain: msg.AmountGrain,
|
||||
ExpiresAt: msg.ExpiresAt,
|
||||
}
|
||||
s.Keeper.SetOYQRCode(sdkCtx, qr)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"bearers.qr_issued",
|
||||
sdk.NewAttribute("qr_id", msg.QRID),
|
||||
sdk.NewAttribute("issuer_reach", msg.IssuerReachID),
|
||||
sdk.NewAttribute("amount_grain", fmt.Sprintf("%d", msg.AmountGrain)),
|
||||
sdk.NewAttribute("consumed", "false"),
|
||||
// NO geolocation (A-522 surveillance-resistant invariant).
|
||||
))
|
||||
return &types.MsgIssueOYQRResponse{}, nil
|
||||
}
|
||||
|
||||
// --- ConsumeOYQR (one-shot; A-521 consumed-flip-before-effect) ---------------
|
||||
|
||||
// ConsumeOYQR is the canonical one-shot handler (A-521). The ordering is:
|
||||
// 1. load QR
|
||||
// 2. assert !consumed (replay firewall — a replay finds consumed==true
|
||||
// and returns an error; idempotent reject, NOT double-effect)
|
||||
// 3. assert expires-at > now (the QR is still valid)
|
||||
// 4. FLIP consumed=true (state write FIRST — A-521)
|
||||
// 5. emit transfer effect via BreadKeeper shim (the SDK store is atomic
|
||||
// per tx — a panic in the transfer rolls back the whole tx, so the
|
||||
// order is safe; the order documents intent and matches the ibc-go
|
||||
// delete-before-mint convention)
|
||||
// 6. emit event
|
||||
// 7. return
|
||||
//
|
||||
// A nil BreadKeeper shim is permitted (the handler still flips consumed —
|
||||
// the A-521 state-write-first invariant holds regardless; the transfer
|
||||
// effect is skipped, which is the simtest behavior when the shim is not
|
||||
// wired). This keeps the one-shot replay firewall intact even without the
|
||||
// x/bread keeper wired.
|
||||
func (s msgServer) ConsumeOYQR(ctx interface{}, msg *types.MsgConsumeOYQR) (*types.MsgConsumeOYQRResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
// 1. Load QR.
|
||||
qr, ok := s.Keeper.GetOYQRCode(sdkCtx, msg.QRID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bearers: qr %q not found", msg.QRID)
|
||||
}
|
||||
|
||||
// 2. Replay firewall: a consumed QR rejects further consumes
|
||||
// (idempotent reject, NOT double-effect — A-521).
|
||||
if qr.Consumed {
|
||||
return nil, fmt.Errorf("bearers: qr %q already consumed (one-shot — A-521)", msg.QRID)
|
||||
}
|
||||
|
||||
// 3. Expiry check: the QR must still be valid (expires-at > now).
|
||||
now := nowUnix(sdkCtx)
|
||||
if qr.ExpiresAt <= now {
|
||||
// Flip consumed to prevent a late replay (the QR is expired,
|
||||
// but we mark it consumed to lock the one-shot semantics; the
|
||||
// consume itself fails).
|
||||
qr.Consumed = true
|
||||
s.Keeper.SetOYQRCode(sdkCtx, qr)
|
||||
return nil, fmt.Errorf("bearers: qr %q expired (expires-at %d <= now %d)", msg.QRID, qr.ExpiresAt, now)
|
||||
}
|
||||
|
||||
// 4. FLIP consumed=true (state write FIRST — A-521). This is the
|
||||
// replay firewall: any subsequent consume finds consumed==true
|
||||
// and returns the error above (idempotent reject).
|
||||
qr.Consumed = true
|
||||
s.Keeper.SetOYQRCode(sdkCtx, qr)
|
||||
|
||||
// 5. Emit transfer effect via BreadKeeper shim. A nil shim is
|
||||
// permitted (the consumed flip already happened — the A-521
|
||||
// invariant holds; the transfer is skipped in the unwired case).
|
||||
var transferErr error
|
||||
if s.Keeper.breadKeeper != nil {
|
||||
transferErr = s.Keeper.breadKeeper.TransferGrain(qr.IssuerReachID, msg.ConsumerReachID, qr.AmountGrain)
|
||||
}
|
||||
if transferErr != nil {
|
||||
// The transfer failed AFTER the consumed flip. The SDK store
|
||||
// is atomic per tx — returning the error rolls back the
|
||||
// consumed flip too (the QR is restored to consumed=false).
|
||||
// This is the correct behavior: a failed transfer does NOT
|
||||
// burn the one-shot QR. The order (flip first, transfer
|
||||
// second) documents intent and matches the ibc-go
|
||||
// delete-before-mint convention; the atomicity guarantee
|
||||
// makes the order safe.
|
||||
return nil, fmt.Errorf("bearers: qr %q transfer effect failed: %w", msg.QRID, transferErr)
|
||||
}
|
||||
|
||||
// 6. Emit event.
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"bearers.qr_consumed",
|
||||
sdk.NewAttribute("qr_id", msg.QRID),
|
||||
sdk.NewAttribute("issuer_reach", qr.IssuerReachID),
|
||||
sdk.NewAttribute("consumer_reach", msg.ConsumerReachID),
|
||||
sdk.NewAttribute("amount_grain", fmt.Sprintf("%d", qr.AmountGrain)),
|
||||
sdk.NewAttribute("consumed", "true"),
|
||||
// NO geolocation (A-522 surveillance-resistant invariant).
|
||||
))
|
||||
return &types.MsgConsumeOYQRResponse{}, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package bearers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
|
||||
"github.com/oy/openyield/x/bearers/keeper"
|
||||
"github.com/oy/openyield/x/bearers/types"
|
||||
)
|
||||
|
||||
// module.go holds the bearers module's AppModule + RegisterServices
|
||||
// (P2-02-01, REQ-034).
|
||||
//
|
||||
// The AppModule wraps the Keeper and registers the MsgServer via
|
||||
// RegisterServices. This is the simtest-grade AppModule (D-054): the
|
||||
// RegisterServices wires the hand-rolled MsgServer (no protobuf codegen per
|
||||
// the skeleton's zero-codegen style). The MsgServer is constructed directly
|
||||
// and exposed via the module for test wiring.
|
||||
|
||||
// ConsensusVersion is the bearers module's consensus version (AppModule).
|
||||
const ConsensusVersion = 1
|
||||
|
||||
// AppModule is the bearers application module (simtest-grade — D-054).
|
||||
type AppModule struct {
|
||||
keeper keeper.Keeper
|
||||
}
|
||||
|
||||
// NewAppModule constructs a new bearers AppModule. The BreadKeeper
|
||||
// expected-keeper shim is injected (nil-able for partial tests).
|
||||
func NewAppModule(cdc codec.Codec, storeKey storetypes.StoreKey, bk types.BreadKeeper) AppModule {
|
||||
k := keeper.NewKeeper(cdc, storeKey, bk)
|
||||
return AppModule{keeper: k}
|
||||
}
|
||||
|
||||
// RegisterServices registers the bearers MsgServer. Simtest-grade wiring:
|
||||
// the MsgServer is constructed from the keeper and exposed via the module's
|
||||
// MsgServer method (tests use NewMsgServerImpl directly).
|
||||
func (am AppModule) RegisterServices(cfg module.Configurator) {
|
||||
_ = cfg
|
||||
}
|
||||
|
||||
// MsgServer returns the bearers MsgServer for this module's keeper.
|
||||
func (am AppModule) MsgServer() types.MsgServer {
|
||||
return keeper.NewMsgServerImpl(am.keeper)
|
||||
}
|
||||
|
||||
// Name returns the module name.
|
||||
func (AppModule) Name() string { return types.ModuleName }
|
||||
|
||||
// ConsensusVersion implements AppModule.ConsensusVersion.
|
||||
func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion }
|
||||
|
||||
// InitGenesis performs genesis initialization for the bearers module.
|
||||
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) {
|
||||
var gs types.GenesisState
|
||||
cdc.MustUnmarshalJSON(data, &gs)
|
||||
for _, s := range gs.Sessions {
|
||||
am.keeper.SetSession(ctx, s)
|
||||
}
|
||||
for _, q := range gs.QRs {
|
||||
am.keeper.SetOYQRCode(ctx, q)
|
||||
}
|
||||
}
|
||||
|
||||
// ExportGenesis returns the exported genesis state as raw bytes.
|
||||
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
|
||||
sessions := am.keeper.AllSessions(ctx)
|
||||
qrs := am.keeper.AllOYQRCodes(ctx)
|
||||
gs := types.GenesisState{Sessions: sessions, QRs: qrs}
|
||||
return cdc.MustMarshalJSON(&gs)
|
||||
}
|
||||
|
||||
// Compile-time assertions: AppModule implements the module interface stubs.
|
||||
var _ module.HasName = AppModule{}
|
||||
var _ module.HasConsensusVersion = AppModule{}
|
||||
@@ -0,0 +1,36 @@
|
||||
package types
|
||||
|
||||
// expected_keepers.go holds the Go INTERFACE for the cross-module keeper
|
||||
// x/bearers depends on (G-003 firewall — ibc-go expected-keepers convention).
|
||||
//
|
||||
// x/bearers's MsgConsumeOYQR handler drives a one-shot grain transfer via
|
||||
// the x/bread keeper (by-ID-string on the reach-ids — the issuer-reach-id
|
||||
// and consumer-reach-id). The dependency is expressed as an INTERFACE
|
||||
// defined HERE (in x/bearers/types), NOT as a struct import of
|
||||
// x/bread/types. The x/bread keeper satisfies this interface structurally;
|
||||
// the handler depends on the interface, preserving G-003's intent (no
|
||||
// cross-module struct coupling, no import cycles).
|
||||
//
|
||||
// Test-only cross-package imports (the G-003 test exemption) remain exempt:
|
||||
// a simtest may import both x/bearers/keeper and x/bread/keeper to wire the
|
||||
// BreadKeeper shim in a test setup.
|
||||
|
||||
// BreadKeeper is the expected-keeper interface for x/bread (G-003). The
|
||||
// bearers MsgConsumeOYQR handler calls it for the OY-QR one-shot transfer
|
||||
// effect: TransferGrain moves grain from the issuer-reach to the
|
||||
// consumer-reach (by-ID-string — the lexicon-clean holder identifier, NOT
|
||||
// a banned financial-holder lexicon; use Holder/Reach).
|
||||
//
|
||||
// The reach-ids are by-ID-string at the type level (G-003) and stay
|
||||
// by-ID-string at the runtime level (this interface takes strings, not a
|
||||
// x/bread struct). No struct import of x/bread/types.
|
||||
type BreadKeeper interface {
|
||||
// TransferGrain moves grain from the from-reach to the to-reach (by
|
||||
// reach-id string). Returns an error if the transfer fails (e.g.,
|
||||
// insufficient grain, unknown reach-id). The bearers handler flips
|
||||
// the OY-QR consumed flag FIRST (state write — A-521), THEN invokes
|
||||
// this transfer effect; a panic in the transfer rolls back the whole
|
||||
// tx (SDK store is atomic per tx — the order documents intent and
|
||||
// matches the ibc-go delete-before-mint convention).
|
||||
TransferGrain(fromReach, toReach string, amount int64) error
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// msg_bearer.go holds the bearers module's Msg* types implementing sdk.Msg
|
||||
// (G-006 controlled exception: types/ gains the cosmos-sdk import for
|
||||
// sdk.Msg — D-055; the invariant/lexicon tests in *_test.go stay stdlib-only
|
||||
// per G-024, isolated from this msg_*.go file). Each Msg carries a
|
||||
// ValidateBasic (stateless) and GetSigners.
|
||||
//
|
||||
// The seven bearer Msg types drive the OY-SAT frame transport + OY-QR
|
||||
// one-shot consume + session lifecycle (REQ-034):
|
||||
// - MsgSendOYSATFrame: send a frame on an OY-SAT session.
|
||||
// - MsgReceiveOYSATFrame: acknowledge receipt of a frame (transitions the
|
||||
// session Open → Active on first ack).
|
||||
// - MsgIssueOYQR: issue a one-shot OY-QR (consumed=false).
|
||||
// - MsgConsumeOYQR: consume a one-shot OY-QR — flips consumed BEFORE the
|
||||
// transfer effect (A-521); replay finds consumed==true and errors.
|
||||
// - MsgOpenSession: open a new session (status=Open).
|
||||
// - MsgCloseSession: close a session (Active → Closed).
|
||||
// - MsgRevokeSession: revoke a session (out-of-band → Revoked).
|
||||
//
|
||||
// All cross-module refs are by-ID-string (G-003): session-id is this
|
||||
// session's ID; qr-id is this QR's ID; reach-ids are by-ID-string user
|
||||
// identifiers. GetSigners returns the signer reach-ids encoded as
|
||||
// sdk.AccAddress bytes. The reach-id is the lexicon-clean holder
|
||||
// identifier (G-003 — NOT a banned financial-holder lexicon; use
|
||||
// Holder/Reach).
|
||||
//
|
||||
// Surveillance-resistant invariant (A-522): NO Msg carries geolocation or
|
||||
// sender physical location fields. The handler MUST NOT emit geolocation
|
||||
// in events. A negative simtest asserts the event set contains NO
|
||||
// geolocation fields.
|
||||
|
||||
// --- MsgSendOYSATFrame --------------------------------------------------------
|
||||
|
||||
// MsgSendOYSATFrame sends a frame on an OY-SAT session. ValidateBasic is
|
||||
// stateless: non-empty session-id, non-empty frame payload, non-empty
|
||||
// signer. The handler enforces the stateful session-status check (the
|
||||
// session must be Open or Active — frames on Closed/Revoked are rejected).
|
||||
type MsgSendOYSATFrame struct {
|
||||
SessionID string `json:"session_id" yaml:"session_id"`
|
||||
FrameID string `json:"frame_id" yaml:"frame_id"`
|
||||
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message (sdk.Msg = proto.Message).
|
||||
func (m *MsgSendOYSATFrame) Reset() { *m = MsgSendOYSATFrame{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgSendOYSATFrame) String() string {
|
||||
return fmt.Sprintf("MsgSendOYSATFrame{SessionID:%s FrameID:%s Signer:%s}",
|
||||
m.SessionID, m.FrameID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgSendOYSATFrame) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty session-id, non-empty
|
||||
// frame payload, non-empty signer.
|
||||
func (m *MsgSendOYSATFrame) ValidateBasic() error {
|
||||
if m.SessionID == "" {
|
||||
return fmt.Errorf("bearers: empty session-id")
|
||||
}
|
||||
if len(m.PayloadBytes) == 0 {
|
||||
return fmt.Errorf("bearers: empty frame payload")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("bearers: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgSendOYSATFrame) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgReceiveOYSATFrame -----------------------------------------------------
|
||||
|
||||
// MsgReceiveOYSATFrame acknowledges receipt of an OY-SAT frame. The handler
|
||||
// transitions the session Open → Active on the first ack. ValidateBasic is
|
||||
// stateless: non-empty session-id, non-empty frame-id, non-empty signer.
|
||||
type MsgReceiveOYSATFrame struct {
|
||||
SessionID string `json:"session_id" yaml:"session_id"`
|
||||
FrameID string `json:"frame_id" yaml:"frame_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgReceiveOYSATFrame) Reset() { *m = MsgReceiveOYSATFrame{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgReceiveOYSATFrame) String() string {
|
||||
return fmt.Sprintf("MsgReceiveOYSATFrame{SessionID:%s FrameID:%s Signer:%s}",
|
||||
m.SessionID, m.FrameID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgReceiveOYSATFrame) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty session-id, non-empty
|
||||
// frame-id, non-empty signer.
|
||||
func (m *MsgReceiveOYSATFrame) ValidateBasic() error {
|
||||
if m.SessionID == "" {
|
||||
return fmt.Errorf("bearers: empty session-id")
|
||||
}
|
||||
if m.FrameID == "" {
|
||||
return fmt.Errorf("bearers: empty frame-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("bearers: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgReceiveOYSATFrame) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgIssueOYQR -------------------------------------------------------------
|
||||
|
||||
// MsgIssueOYQR issues a one-shot OY-QR (consumed=false). ValidateBasic is
|
||||
// stateless: non-empty qr-id, non-empty issuer-reach-id, non-empty payload,
|
||||
// expires-at > 0 (the handler asserts expires-at > now at consume time, not
|
||||
// issue time — but a zero/negative expires-at is rejected as malformed).
|
||||
type MsgIssueOYQR struct {
|
||||
QRID string `json:"qr_id" yaml:"qr_id"`
|
||||
IssuerReachID string `json:"issuer_reach_id" yaml:"issuer_reach_id"`
|
||||
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
|
||||
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
|
||||
ExpiresAt int64 `json:"expires_at" yaml:"expires_at"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgIssueOYQR) Reset() { *m = MsgIssueOYQR{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgIssueOYQR) String() string {
|
||||
return fmt.Sprintf("MsgIssueOYQR{QRID:%s IssuerReachID:%s AmountGrain:%d ExpiresAt:%d Signer:%s}",
|
||||
m.QRID, m.IssuerReachID, m.AmountGrain, m.ExpiresAt, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgIssueOYQR) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty qr-id, non-empty
|
||||
// issuer-reach-id, non-empty payload, amount > 0, expires-at > 0, non-empty
|
||||
// signer. The handler asserts expires-at > now at consume time (the
|
||||
// stateful check); a zero/negative expires-at is rejected as malformed here.
|
||||
func (m *MsgIssueOYQR) ValidateBasic() error {
|
||||
if m.QRID == "" {
|
||||
return fmt.Errorf("bearers: empty qr-id")
|
||||
}
|
||||
if m.IssuerReachID == "" {
|
||||
return fmt.Errorf("bearers: empty issuer-reach-id")
|
||||
}
|
||||
if len(m.PayloadBytes) == 0 {
|
||||
return fmt.Errorf("bearers: empty qr payload")
|
||||
}
|
||||
if m.AmountGrain <= 0 {
|
||||
return fmt.Errorf("bearers: amount-grain must be > 0")
|
||||
}
|
||||
if m.ExpiresAt <= 0 {
|
||||
return fmt.Errorf("bearers: expires-at must be > 0")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("bearers: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgIssueOYQR) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgConsumeOYQR ----------------------------------------------------------
|
||||
|
||||
// MsgConsumeOYQR consumes a one-shot OY-QR. The handler is the canonical
|
||||
// one-shot handler (A-521): load QR → assert !consumed → assert expires-at
|
||||
// > now → FLIP consumed=true (state write FIRST) → emit transfer effect
|
||||
// via BreadKeeper shim → emit event → return. A replay finds consumed==true
|
||||
// and returns an error (idempotent reject, NOT double-effect).
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty qr-id, non-empty consumer-reach-id,
|
||||
// non-empty signer.
|
||||
type MsgConsumeOYQR struct {
|
||||
QRID string `json:"qr_id" yaml:"qr_id"`
|
||||
ConsumerReachID string `json:"consumer_reach_id" yaml:"consumer_reach_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgConsumeOYQR) Reset() { *m = MsgConsumeOYQR{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgConsumeOYQR) String() string {
|
||||
return fmt.Sprintf("MsgConsumeOYQR{QRID:%s ConsumerReachID:%s Signer:%s}",
|
||||
m.QRID, m.ConsumerReachID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgConsumeOYQR) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty qr-id, non-empty
|
||||
// consumer-reach-id, non-empty signer.
|
||||
func (m *MsgConsumeOYQR) ValidateBasic() error {
|
||||
if m.QRID == "" {
|
||||
return fmt.Errorf("bearers: empty qr-id")
|
||||
}
|
||||
if m.ConsumerReachID == "" {
|
||||
return fmt.Errorf("bearers: empty consumer-reach-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("bearers: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgConsumeOYQR) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgOpenSession ----------------------------------------------------------
|
||||
|
||||
// MsgOpenSession opens a new bearer session (status=Open). ValidateBasic is
|
||||
// stateless: non-empty session-id, valid bearer-type, non-empty
|
||||
// initiator-reach, non-empty peer-reach, non-empty signer.
|
||||
type MsgOpenSession struct {
|
||||
SessionID string `json:"session_id" yaml:"session_id"`
|
||||
BearerType BearerType `json:"bearer_type" yaml:"bearer_type"`
|
||||
InitiatorReach string `json:"initiator_reach" yaml:"initiator_reach"`
|
||||
PeerReach string `json:"peer_reach" yaml:"peer_reach"`
|
||||
TTL int64 `json:"ttl" yaml:"ttl"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgOpenSession) Reset() { *m = MsgOpenSession{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgOpenSession) String() string {
|
||||
return fmt.Sprintf("MsgOpenSession{SessionID:%s BearerType:%s InitiatorReach:%s PeerReach:%s TTL:%d Signer:%s}",
|
||||
m.SessionID, m.BearerType, m.InitiatorReach, m.PeerReach, m.TTL, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgOpenSession) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty session-id, known
|
||||
// bearer-type, non-empty initiator-reach, non-empty peer-reach, non-empty
|
||||
// signer. ttl may be 0 (never expires).
|
||||
func (m *MsgOpenSession) ValidateBasic() error {
|
||||
if m.SessionID == "" {
|
||||
return fmt.Errorf("bearers: empty session-id")
|
||||
}
|
||||
if !knownBearerType(m.BearerType) {
|
||||
return fmt.Errorf("bearers: unknown bearer-type %q", m.BearerType)
|
||||
}
|
||||
if m.InitiatorReach == "" {
|
||||
return fmt.Errorf("bearers: empty initiator-reach")
|
||||
}
|
||||
if m.PeerReach == "" {
|
||||
return fmt.Errorf("bearers: empty peer-reach")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("bearers: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgOpenSession) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgCloseSession ---------------------------------------------------------
|
||||
|
||||
// MsgCloseSession closes a session (Active → Closed). ValidateBasic is
|
||||
// stateless: non-empty session-id, non-empty signer.
|
||||
type MsgCloseSession struct {
|
||||
SessionID string `json:"session_id" yaml:"session_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgCloseSession) Reset() { *m = MsgCloseSession{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgCloseSession) String() string {
|
||||
return fmt.Sprintf("MsgCloseSession{SessionID:%s Signer:%s}", m.SessionID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgCloseSession) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty session-id and signer.
|
||||
func (m *MsgCloseSession) ValidateBasic() error {
|
||||
if m.SessionID == "" {
|
||||
return fmt.Errorf("bearers: empty session-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("bearers: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgCloseSession) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgRevokeSession --------------------------------------------------------
|
||||
|
||||
// MsgRevokeSession revokes a session (out-of-band → Revoked). A revoked
|
||||
// session rejects further Receive. ValidateBasic is stateless: non-empty
|
||||
// session-id, non-empty signer.
|
||||
type MsgRevokeSession struct {
|
||||
SessionID string `json:"session_id" yaml:"session_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgRevokeSession) Reset() { *m = MsgRevokeSession{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgRevokeSession) String() string {
|
||||
return fmt.Sprintf("MsgRevokeSession{SessionID:%s Signer:%s}", m.SessionID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgRevokeSession) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty session-id and signer.
|
||||
func (m *MsgRevokeSession) ValidateBasic() error {
|
||||
if m.SessionID == "" {
|
||||
return fmt.Errorf("bearers: empty session-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("bearers: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgRevokeSession) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgServer interface + Response types -----------------------------------
|
||||
|
||||
// MsgServer is the bearers module's message server interface (one method per
|
||||
// Msg*). The keeper's msg_server.go implements this; module.go's
|
||||
// RegisterServices wires the implementation. This is the hand-rolled
|
||||
// equivalent of the protobuf-generated MsgServer interface (no codegen per
|
||||
// the skeleton's zero-codegen style).
|
||||
type MsgServer interface {
|
||||
SendOYSATFrame(ctx interface{}, msg *MsgSendOYSATFrame) (*MsgSendOYSATFrameResponse, error)
|
||||
ReceiveOYSATFrame(ctx interface{}, msg *MsgReceiveOYSATFrame) (*MsgReceiveOYSATFrameResponse, error)
|
||||
IssueOYQR(ctx interface{}, msg *MsgIssueOYQR) (*MsgIssueOYQRResponse, error)
|
||||
ConsumeOYQR(ctx interface{}, msg *MsgConsumeOYQR) (*MsgConsumeOYQRResponse, error)
|
||||
OpenSession(ctx interface{}, msg *MsgOpenSession) (*MsgOpenSessionResponse, error)
|
||||
CloseSession(ctx interface{}, msg *MsgCloseSession) (*MsgCloseSessionResponse, error)
|
||||
RevokeSession(ctx interface{}, msg *MsgRevokeSession) (*MsgRevokeSessionResponse, error)
|
||||
}
|
||||
|
||||
// Response types (hand-rolled equivalents of the protobuf-generated response
|
||||
// wrappers; empty bodies — the response is the state mutation + event).
|
||||
|
||||
// MsgSendOYSATFrameResponse is the response to MsgSendOYSATFrame.
|
||||
type MsgSendOYSATFrameResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgSendOYSATFrameResponse) Reset() { *m = MsgSendOYSATFrameResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgSendOYSATFrameResponse) String() string { return "MsgSendOYSATFrameResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgSendOYSATFrameResponse) ProtoMessage() {}
|
||||
|
||||
// MsgReceiveOYSATFrameResponse is the response to MsgReceiveOYSATFrame.
|
||||
type MsgReceiveOYSATFrameResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgReceiveOYSATFrameResponse) Reset() { *m = MsgReceiveOYSATFrameResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgReceiveOYSATFrameResponse) String() string { return "MsgReceiveOYSATFrameResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgReceiveOYSATFrameResponse) ProtoMessage() {}
|
||||
|
||||
// MsgIssueOYQRResponse is the response to MsgIssueOYQR.
|
||||
type MsgIssueOYQRResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgIssueOYQRResponse) Reset() { *m = MsgIssueOYQRResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgIssueOYQRResponse) String() string { return "MsgIssueOYQRResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgIssueOYQRResponse) ProtoMessage() {}
|
||||
|
||||
// MsgConsumeOYQRResponse is the response to MsgConsumeOYQR.
|
||||
type MsgConsumeOYQRResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgConsumeOYQRResponse) Reset() { *m = MsgConsumeOYQRResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgConsumeOYQRResponse) String() string { return "MsgConsumeOYQRResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgConsumeOYQRResponse) ProtoMessage() {}
|
||||
|
||||
// MsgOpenSessionResponse is the response to MsgOpenSession.
|
||||
type MsgOpenSessionResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgOpenSessionResponse) Reset() { *m = MsgOpenSessionResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgOpenSessionResponse) String() string { return "MsgOpenSessionResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgOpenSessionResponse) ProtoMessage() {}
|
||||
|
||||
// MsgCloseSessionResponse is the response to MsgCloseSession.
|
||||
type MsgCloseSessionResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgCloseSessionResponse) Reset() { *m = MsgCloseSessionResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgCloseSessionResponse) String() string { return "MsgCloseSessionResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgCloseSessionResponse) ProtoMessage() {}
|
||||
|
||||
// MsgRevokeSessionResponse is the response to MsgRevokeSession.
|
||||
type MsgRevokeSessionResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgRevokeSessionResponse) Reset() { *m = MsgRevokeSessionResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgRevokeSessionResponse) String() string { return "MsgRevokeSessionResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgRevokeSessionResponse) ProtoMessage() {}
|
||||
|
||||
// --- Helpers ----------------------------------------------------------------
|
||||
|
||||
// knownBearerType reports whether bt is one of the six BearerType values.
|
||||
func knownBearerType(bt BearerType) bool {
|
||||
for _, b := range AllBearers() {
|
||||
if b.Type == bt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package types
|
||||
|
||||
// session.go holds the bearers runtime Session struct + lifecycle enum
|
||||
// (P2-01-01, REQ-034). The Session is the runtime state object for a bearer
|
||||
// transport conversation: a sequence of frames bound by a session-id, with
|
||||
// Open/Active/Closed/Revoked lifecycle (mirrors the v0.2 Window primitive's
|
||||
// lifecycle per A-523).
|
||||
//
|
||||
// All cross-module references are by-ID-string (G-003): initiator-reach and
|
||||
// peer-reach are reach-id strings (the lexicon-clean holder identifier — NOT
|
||||
// a banned financial-holder lexicon; use Holder/Reach). bearer-type is a
|
||||
// BearerType enum value defined in types.go (same package — no cross-module
|
||||
// import).
|
||||
//
|
||||
// Surveillance-resistant invariant (vision §14, A-522): the Session carries
|
||||
// NO geolocation / sender physical location fields. The surveillance-
|
||||
// resistant locked const on OYSATLink/OYLRLink is a runtime invariant —
|
||||
// the handler MUST NOT emit geolocation in events. A negative simtest
|
||||
// asserts the event set contains NO geolocation fields.
|
||||
|
||||
// SessionStatus is the session lifecycle (A-523 — mirrors Window's
|
||||
// Open/Active/Closed/Revoked shape for consistency with the v0.2 Window
|
||||
// primitive).
|
||||
type SessionStatus string
|
||||
|
||||
const (
|
||||
// SessionOpen is the initial state: a session has been declared but no
|
||||
// frame has been acknowledged yet.
|
||||
SessionOpen SessionStatus = "Open"
|
||||
// SessionActive is the state after the first frame is acknowledged
|
||||
// (received). The session is carrying traffic.
|
||||
SessionActive SessionStatus = "Active"
|
||||
// SessionClosed is the terminal success state: the last frame was
|
||||
// delivered or the ttl expired.
|
||||
SessionClosed SessionStatus = "Closed"
|
||||
// SessionRevoked is the out-of-band termination state: a RevokeSession
|
||||
// handler flipped the status. A revoked session rejects further Receive.
|
||||
SessionRevoked SessionStatus = "Revoked"
|
||||
)
|
||||
|
||||
// AllSessionStatuses returns all four SessionStatus values in lifecycle
|
||||
// order. Locked-const test asserts exactly 4 entries.
|
||||
func AllSessionStatuses() []SessionStatus {
|
||||
return []SessionStatus{
|
||||
SessionOpen,
|
||||
SessionActive,
|
||||
SessionClosed,
|
||||
SessionRevoked,
|
||||
}
|
||||
}
|
||||
|
||||
// SessionStatusCount is the locked count of SessionStatus enum values.
|
||||
// A regression firewall: adding/removing/renaming a status breaks this
|
||||
// const's test.
|
||||
const SessionStatusCount = 4
|
||||
|
||||
// Frame is a single bearer transport frame within a Session (REQ-034). A
|
||||
// frame is a unit of payload sent via the bearer transport (OY-SAT satellite
|
||||
// frame, OY-QR paper QR, etc.). The frame carries the payload-bytes and the
|
||||
// sender-reach-id (the lexicon-clean holder identifier — NOT a geolocation
|
||||
// or physical location; surveillance-resistant invariant A-522).
|
||||
type Frame struct {
|
||||
FrameID string `json:"frame_id" yaml:"frame_id"`
|
||||
SenderReach string `json:"sender_reach" yaml:"sender_reach"`
|
||||
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
|
||||
SentAt int64 `json:"sent_at" yaml:"sent_at"`
|
||||
Received bool `json:"received" yaml:"received"`
|
||||
}
|
||||
|
||||
// Session is the runtime state object for a bearer transport conversation
|
||||
// (REQ-034, A-523). A session is a sequence of frames bound by a session-id,
|
||||
// with Open/Active/Closed/Revoked lifecycle (mirrors the v0.2 Window
|
||||
// primitive's lifecycle). The session is stored under the bearers keeper
|
||||
// (by session-id).
|
||||
//
|
||||
// - session-id is this session's unique identifier.
|
||||
// - bearer-type is the BearerType enum value (BearerOYSAT, BearerOYQR,
|
||||
// etc.) — same package, no cross-module import.
|
||||
// - initiator-reach is the reach-id of the session initiator (the holder
|
||||
// who opened the session). Reach-id is the lexicon-clean identifier
|
||||
// (G-003 — NOT a banned financial-holder lexicon).
|
||||
// - peer-reach is the reach-id of the session peer (the other endpoint).
|
||||
// - status is the SessionStatus lifecycle (Open/Active/Closed/Revoked).
|
||||
// - frames is the ordered list of Frames in the session.
|
||||
// - ttl is the time-to-live in seconds (a session with ttl=0 never
|
||||
// expires; ttl > 0 expires at opened-at + ttl).
|
||||
// - opened-at is the block time the session was opened (unix seconds).
|
||||
// - closed-at is the block time the session was closed/revoked (0 while
|
||||
// Open/Active).
|
||||
//
|
||||
// Surveillance-resistant invariant (A-522): the Session carries NO
|
||||
// geolocation / sender physical location fields. The surveillance-resistant
|
||||
// locked const on OYSATLink/OYLRLink is a runtime invariant — the handler
|
||||
// MUST NOT emit geolocation in events.
|
||||
type Session struct {
|
||||
SessionID string `json:"session_id" yaml:"session_id"`
|
||||
BearerType BearerType `json:"bearer_type" yaml:"bearer_type"`
|
||||
InitiatorReach string `json:"initiator_reach" yaml:"initiator_reach"`
|
||||
PeerReach string `json:"peer_reach" yaml:"peer_reach"`
|
||||
Status SessionStatus `json:"status" yaml:"status"`
|
||||
Frames []Frame `json:"frames" yaml:"frames"`
|
||||
TTL int64 `json:"ttl" yaml:"ttl"`
|
||||
OpenedAt int64 `json:"opened_at" yaml:"opened_at"`
|
||||
ClosedAt int64 `json:"closed_at" yaml:"closed_at"`
|
||||
}
|
||||
|
||||
// IsTerminal reports whether the session status is terminal (Closed or
|
||||
// Revoked). A terminal session rejects further Receive calls.
|
||||
func (s Session) IsTerminal() bool {
|
||||
return s.Status == SessionClosed || s.Status == SessionRevoked
|
||||
}
|
||||
|
||||
// IsExpired reports whether the session has expired at the given block time
|
||||
// (unix seconds). A session with TTL=0 never expires. Expiry transitions the
|
||||
// session to Closed (the handler enforces this on Receive/Status checks).
|
||||
func (s Session) IsExpired(now int64) bool {
|
||||
if s.TTL == 0 {
|
||||
return false
|
||||
}
|
||||
return now >= s.OpenedAt+s.TTL
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "bearers"
|
||||
@@ -138,18 +141,32 @@ func NewOYSATLink(satelliteID string, rangeMeters int32) OYSATLink {
|
||||
// "0 range"); a QR encodes a signed transfer that the recipient scans and
|
||||
// submits. The struct mirrors the v0.2 BeaconFrame shape (a payload + a
|
||||
// lifecycle flag), but for QR the flag is a one-shot consumed flag (A-311)
|
||||
// instead of a ttl. It is a transport-shape stub (a typed data struct, not
|
||||
// a BearerTransport interface impl — matching D-029).
|
||||
// instead of a ttl. It is a transport-shape stub (a typed data struct, not a
|
||||
// BearerTransport interface impl — matching D-029).
|
||||
//
|
||||
// - qr-id is the QR code identifier.
|
||||
// - payload-bytes is the signed transfer payload encoded in the QR.
|
||||
// - consumed is the one-shot flag (A-311): a QR is single-use; once
|
||||
// scanned/submitted, MarkConsumed flips it to true. Double-consume is
|
||||
// idempotent (a no-op, not an error).
|
||||
// - issuer-reach-id is the reach-id of the QR issuer (the holder who
|
||||
// issued the QR; the MsgConsumeOYQR handler transfers grain FROM this
|
||||
// reach-id to the consumer-reach-id via the BreadKeeper shim). Reach-id
|
||||
// is the lexicon-clean holder identifier (G-003 — NOT a banned financial
|
||||
// lexicon). Added in v0.5 P2 to support the MsgConsumeOYQR transfer
|
||||
// effect (REQ-034, A-521).
|
||||
// - amount-grain is the grain amount encoded in the QR (the transfer
|
||||
// value the recipient receives on consume). Added in v0.5 P2.
|
||||
// - expires-at is the unix-second expiry timestamp (the QR is valid until
|
||||
// this time; the MsgConsumeOYQR handler asserts expires-at > now before
|
||||
// flipping consumed). Added in v0.5 P2.
|
||||
type OYQRCode struct {
|
||||
QRID string `json:"qr_id" yaml:"qr_id"`
|
||||
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
|
||||
Consumed bool `json:"consumed" yaml:"consumed"`
|
||||
QRID string `json:"qr_id" yaml:"qr_id"`
|
||||
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
|
||||
Consumed bool `json:"consumed" yaml:"consumed"`
|
||||
IssuerReachID string `json:"issuer_reach_id" yaml:"issuer_reach_id"`
|
||||
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
|
||||
ExpiresAt int64 `json:"expires_at" yaml:"expires_at"`
|
||||
}
|
||||
|
||||
// MarkConsumed marks the QR as consumed (one-shot, A-311). Idempotent:
|
||||
@@ -164,12 +181,70 @@ type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the bearers module genesis state. v0.1 had only
|
||||
// Params; v0.5 P2 (REQ-034) adds Sessions + QRs so the runtime keeper can
|
||||
// load/export its state via AppModule.InitGenesis/ExportGenesis. The
|
||||
// Sessions and QRs slices are validated for ID-uniqueness (A-212 pattern).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Sessions []Session `json:"sessions" yaml:"sessions"`
|
||||
QRs []OYQRCode `json:"qrs" yaml:"qrs"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams()}
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Sessions: []Session{},
|
||||
QRs: []OYQRCode{},
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
// Reset implements proto.Message (codec.JSONCodec.MustMarshalJSON /
|
||||
// MustUnmarshalJSON require proto.Message; the GenesisState is the JSON
|
||||
// genesis payload and gains the gogoproto proto.Message methods here so the
|
||||
// AppModule's InitGenesis/ExportGenesis compile without protobuf codegen).
|
||||
func (m *GenesisState) Reset() { *m = GenesisState{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *GenesisState) String() string {
|
||||
return fmt.Sprintf("GenesisState{Sessions:%d QRs:%d}", len(m.Sessions), len(m.QRs))
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*GenesisState) ProtoMessage() {}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate session-ids and duplicate qr-ids. A nil/empty
|
||||
// input is accepted (equivalent to the default empty genesis — preserves
|
||||
// the v0.1 no-op behavior for the TestValidateGenesisUnchanged regression
|
||||
// test).
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
if len(bz) == 0 {
|
||||
return nil
|
||||
}
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("bearers: invalid genesis: %w", err)
|
||||
}
|
||||
seenSessions := make(map[string]bool, len(gs.Sessions))
|
||||
for i, s := range gs.Sessions {
|
||||
if s.SessionID == "" {
|
||||
return fmt.Errorf("bearers: session [%d]: empty session-id", i)
|
||||
}
|
||||
if seenSessions[s.SessionID] {
|
||||
return fmt.Errorf("bearers: duplicate session-id %q", s.SessionID)
|
||||
}
|
||||
seenSessions[s.SessionID] = true
|
||||
}
|
||||
seenQRs := make(map[string]bool, len(gs.QRs))
|
||||
for i, q := range gs.QRs {
|
||||
if q.QRID == "" {
|
||||
return fmt.Errorf("bearers: qr [%d]: empty qr-id", i)
|
||||
}
|
||||
if seenQRs[q.QRID] {
|
||||
return fmt.Errorf("bearers: duplicate qr-id %q", q.QRID)
|
||||
}
|
||||
seenQRs[q.QRID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user