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/bridge/types" ) // keeper.go holds the store-backed Keeper for the bridge module (P1-03-01). // // The Keeper wraps an sdk.KVStore via a storeKey. It replaces the v0.3 // in-memory stub (the stub may stay as a test helper). The Keeper holds the // BridgeRoute records (by bridge-id) and the IBC in-flight packet records // (by source-port/source-channel/sequence) used for replay protection (A-513). // // The Keeper also holds the expected-keeper shims (WatcherKeeper for the // Attested transition + Solana guardian sig set; BreadKeeper for mint/release // wrapped Bread on recv/timeout). The shims are interfaces (G-003 — no // struct imports of x/watcher/types or x/bread/types); the concrete keepers // satisfy them structurally. // // State-machine ordering (vision §7, enforced in every handler): // ValidateBasic → keeper authz → state mutation → ctx.EventManager().EmitEvent // Keeper is the store-backed bridge keeper. type Keeper struct { cdc codec.Codec storeKey storetypes.StoreKey watcherKeeper types.WatcherKeeper breadKeeper types.BreadKeeper } // NewKeeper constructs a new store-backed bridge Keeper. The expected-keeper // shims are injected (nil-able for partial tests; the handler guards nil // shims where appropriate). func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, wk types.WatcherKeeper, bk types.BreadKeeper) Keeper { return Keeper{ cdc: cdc, storeKey: storeKey, watcherKeeper: wk, breadKeeper: bk, } } // SetWatcherKeeper sets the WatcherKeeper expected-keeper shim (for // post-construction wiring, e.g., app wiring or test setup). func (k *Keeper) SetWatcherKeeper(wk types.WatcherKeeper) { k.watcherKeeper = wk } // SetBreadKeeper sets the BreadKeeper expected-keeper shim. func (k *Keeper) SetBreadKeeper(bk types.BreadKeeper) { k.breadKeeper = bk } // --- BridgeRoute store -------------------------------------------------------- // routeKey is the store key prefix for a BridgeRoute record (by bridge-id). var routeKeyPrefix = []byte("route/") func routeKey(bridgeID string) []byte { return append(routeKeyPrefix, []byte(bridgeID)...) } // GetBridgeRoute loads a BridgeRoute by bridge-id. Returns the route and // true if found, or zero value + false if not. This is the store-backed // implementation that satisfies x/exit/types.BridgeKeeper (GetBridgeRoute // returns status + bridgeType; the status is the BridgeStatus string). func (k Keeper) GetBridgeRoute(ctx sdk.Context, bridgeID string) (types.BridgeRoute, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(routeKey(bridgeID)) if bz == nil { return types.BridgeRoute{}, false } var r types.BridgeRoute if err := json.Unmarshal(bz, &r); err != nil { return types.BridgeRoute{}, false } return r, true } // SetBridgeRoute persists a BridgeRoute by bridge-id. func (k Keeper) SetBridgeRoute(ctx sdk.Context, r types.BridgeRoute) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(r) if err != nil { panic(fmt.Sprintf("bridge: marshal route %q: %v", r.BridgeID, err)) } store.Set(routeKey(r.BridgeID), bz) } // AllBridgeRoutes returns all persisted BridgeRoute records (iteration // helper for tests/queries). func (k Keeper) AllBridgeRoutes(ctx sdk.Context) []types.BridgeRoute { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(routeKeyPrefix, prefixEnd(routeKeyPrefix)) defer iterator.Close() out := []types.BridgeRoute{} for ; iterator.Valid(); iterator.Next() { var r types.BridgeRoute if err := json.Unmarshal(iterator.Value(), &r); err == nil { out = append(out, r) } } return out } // prefixEnd returns the key that sorts immediately after all keys sharing the // given prefix (the standard prefix-iteration end key). 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 } } return nil } // --- IBC in-flight packet store (replay protection — A-513) ------------------- // // The in-flight record tracks a packet that has been received but not yet // acknowledged. OnRecvPacket writes the record; OnAcknowledgementPacket // deletes it (first ack). A second OnAcknowledgementPacket finds no record // and returns ERROR (G-021 — replay protection, not a silent no-op). This // mirrors ibc-go's delete-on-ack pattern. var inflightPrefix = []byte("inflight/") func inflightKey(sourcePort, sourceChannel string, sequence uint64) []byte { return append(inflightPrefix, []byte(fmt.Sprintf("%s/%s/%d", sourcePort, sourceChannel, sequence))...) } // InflightPacket is the in-flight packet record (replay protection — A-513). type InflightPacket struct { SourcePort string `json:"source_port" yaml:"source_port"` SourceChannel string `json:"source_channel" yaml:"source_channel"` Sequence uint64 `json:"sequence" yaml:"sequence"` Denom string `json:"denom" yaml:"denom"` Amount int64 `json:"amount" yaml:"amount"` Sender string `json:"sender" yaml:"sender"` // source-chain sender reach-id Receiver string `json:"receiver" yaml:"receiver"` // dest-chain receiver reach-id L2Chain string `json:"l2_chain" yaml:"l2_chain"` // the L2 chain (EVM or Solana) Refunded bool `json:"refunded" yaml:"refunded"` // timeout-refund exactly-once guard } // SetInflight writes the in-flight packet record (OnRecvPacket). func (k Keeper) SetInflight(ctx sdk.Context, p InflightPacket) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(p) if err != nil { panic(fmt.Sprintf("bridge: marshal inflight %s/%s/%d: %v", p.SourcePort, p.SourceChannel, p.Sequence, err)) } store.Set(inflightKey(p.SourcePort, p.SourceChannel, p.Sequence), bz) } // GetInflight loads the in-flight packet record. Returns the record and // true if found, or zero value + false if not. The absence of a record on // OnAcknowledgementPacket is the replay signal (G-021). func (k Keeper) GetInflight(ctx sdk.Context, sourcePort, sourceChannel string, sequence uint64) (InflightPacket, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(inflightKey(sourcePort, sourceChannel, sequence)) if bz == nil { return InflightPacket{}, false } var p InflightPacket if err := json.Unmarshal(bz, &p); err != nil { return InflightPacket{}, false } return p, true } // DeleteInflight deletes the in-flight packet record (OnAcknowledgementPacket // — first ack; the deletion is the replay-protection signal). func (k Keeper) DeleteInflight(ctx sdk.Context, sourcePort, sourceChannel string, sequence uint64) { store := ctx.KVStore(k.storeKey) store.Delete(inflightKey(sourcePort, sourceChannel, sequence)) } // --- Solana guardian sig set (wormhole-adapter — D-059) ----------------------- // // The Solana branch verifies a wormhole guardian sig set (a 2-of-N quorum, // N = the wormhole guardian set). The set is read from state (not // hardcoded — D-054 uses a frozen stub set in simtest; live rotation is // deferred). The set is stored as a JSON array of guardian reach-ids. var guardianSetKey = []byte("solana/guardian-set") // GuardianSet is the wormhole guardian sig set for the Solana branch. type GuardianSet struct { Guardians []string `json:"guardians" yaml:"guardians"` // guardian reach-ids Threshold int `json:"threshold" yaml:"threshold"` // 2-of-N quorum } // GetGuardianSet loads the current Solana guardian sig set from state. func (k Keeper) GetGuardianSet(ctx sdk.Context) (GuardianSet, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(guardianSetKey) if bz == nil { return GuardianSet{}, false } var gs GuardianSet if err := json.Unmarshal(bz, &gs); err != nil { return GuardianSet{}, false } return gs, true } // SetGuardianSet persists the Solana guardian sig set (simtest uses a frozen // stub set; live rotation deferred per D-054). func (k Keeper) SetGuardianSet(ctx sdk.Context, gs GuardianSet) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(gs) if err != nil { panic(fmt.Sprintf("bridge: marshal guardian set: %v", err)) } store.Set(guardianSetKey, bz) }