c97e18fc1f
---ci--- project: oy phase: 1 milestone: v0.5 status: complete requirements: covered: [REQ-033] partial: [] ---/ci---
226 lines
8.2 KiB
Go
226 lines
8.2 KiB
Go
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)
|
|
}
|