6c34650a0d
v0.5 Bearers Runtime — 7 runtime REQs (REQ-033..039) shipped as feature. 8 modules promoted to runtime (MsgServer + simtest). cosmos-sdk v0.50.8 + ibc-go v8.2.1 added (G-006 controlled exception). G-003 + locked-const firewalls intact. 8 keeper packages ≥80% coverage. 5 GRILL decisions ratified; 8 binding fixes landed; 5 P1+ flagged for v0.6+. ---ci--- project: oy phase: 8 milestone: v0.5 status: complete requirements: covered: [REQ-033, REQ-034, REQ-035, REQ-036, REQ-037, REQ-038, REQ-039] partial: [] ---/ci---
677 lines
24 KiB
Go
677 lines
24 KiB
Go
package keeper_test
|
|
|
|
// msg_server_simtest_test.go is the x/bridge keeper simtest (P1-06-01).
|
|
//
|
|
// D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no
|
|
// real IBC light clients. The simtest wires the expected-keeper shims
|
|
// (WatcherKeeper + BreadKeeper) to in-test stubs (G-003 test exemption:
|
|
// the test imports x/bridge/keeper + defines stub keepers that satisfy the
|
|
// interfaces; no production struct imports across x/<module>/types).
|
|
//
|
|
// Coverage (A-513, G-021):
|
|
// - OnRecvPacket: mints wrapped Bread (assert BreadKeeper.MintWrappedBread
|
|
// called); ICS-20 v1 denom trace parse; Solana guardian sig set (2-of-N
|
|
// stub).
|
|
// - OnAcknowledgementPacket: deletes the in-flight record (first ack) and
|
|
// rejects the second (REPLAY PROTECTION — G-021, A-513 CVE-class pitfall).
|
|
// - OnTimeoutPacket: refunds the escrow exactly once (second timeout is a
|
|
// no-op — the Refunded flag guards).
|
|
// - BridgeStatus lifecycle: Pending → Attested (MsgAttestBridgeRoute) →
|
|
// Active (MsgActivateBridge) → Closed (MsgCloseBridge).
|
|
// - Solana stub guardian sig set (2-of-N).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"cosmossdk.io/log"
|
|
"cosmossdk.io/store"
|
|
storetypes "cosmossdk.io/store/types"
|
|
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
|
|
dbm "github.com/cosmos/cosmos-db"
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
|
|
|
|
"github.com/oy/openyield/x/bridge/keeper"
|
|
bridgetypes "github.com/oy/openyield/x/bridge/types"
|
|
)
|
|
|
|
// --- Stub expected-keepers (G-003 test exemption) ----------------------------
|
|
|
|
// stubWatcherKeeper satisfies bridgetypes.WatcherKeeper for the simtest. The
|
|
// IsQuorumSigned returns true for the configured quorum-id (the simtest
|
|
// stubs the Watcher 6-of-9 quorum + the Solana guardian 2-of-N quorum).
|
|
type stubWatcherKeeper struct {
|
|
// signedQuorums maps quorum-id → true if the quorum reached threshold.
|
|
signedQuorums map[string]bool
|
|
// solanaCalls tracks IsQuorumSigned invocations for the Solana branch.
|
|
solanaCalls int
|
|
}
|
|
|
|
func (s *stubWatcherKeeper) IsQuorumSigned(quorumID string, payload []byte) bool {
|
|
if quorumID == "solana-guardians" {
|
|
s.solanaCalls++
|
|
}
|
|
return s.signedQuorums[quorumID]
|
|
}
|
|
|
|
// stubBreadKeeper satisfies bridgetypes.BreadKeeper for the simtest. It
|
|
// records mint/release calls for assertion.
|
|
type stubBreadKeeper struct {
|
|
mints []mintCall
|
|
releases []releaseCall
|
|
}
|
|
|
|
type mintCall struct {
|
|
denom string
|
|
amount int64
|
|
reachID string
|
|
}
|
|
|
|
type releaseCall struct {
|
|
denom string
|
|
amount int64
|
|
reachID string
|
|
}
|
|
|
|
func (s *stubBreadKeeper) MintWrappedBread(ctx interface{}, denom string, amount int64, holderReach string) error {
|
|
s.mints = append(s.mints, mintCall{denom, amount, holderReach})
|
|
return nil
|
|
}
|
|
|
|
func (s *stubBreadKeeper) ReleaseWrappedBread(ctx interface{}, denom string, amount int64, holderReach string) error {
|
|
s.releases = append(s.releases, releaseCall{denom, amount, holderReach})
|
|
return nil
|
|
}
|
|
|
|
// --- Simtest context helper --------------------------------------------------
|
|
|
|
// newSimtestContext constructs an in-memory sdk.Context with a KVStore mounted
|
|
// at the bridge store key. D-054: in-memory, no real IBC light clients.
|
|
func newSimtestContext(t *testing.T) (sdk.Context, *stubWatcherKeeper, *stubBreadKeeper, keeper.Keeper) {
|
|
t.Helper()
|
|
db := dbm.NewMemDB()
|
|
cdc := newTestCodec()
|
|
storeKey := storetypes.NewKVStoreKey(bridgetypes.StoreKey)
|
|
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
|
|
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
|
|
if err := cms.LoadLatestVersion(); err != nil {
|
|
t.Fatalf("load latest version: %v", err)
|
|
}
|
|
ctx := sdk.NewContext(cms, cmtproto.Header{}, false, log.NewNopLogger())
|
|
|
|
wk := &stubWatcherKeeper{signedQuorums: map[string]bool{}}
|
|
bk := &stubBreadKeeper{}
|
|
k := keeper.NewKeeper(cdc, storeKey, wk, bk)
|
|
return ctx, wk, bk, k
|
|
}
|
|
|
|
// newTestCodec constructs a minimal codec for the simtest (the keeper uses
|
|
// JSON marshaling, so a bare proto codec suffices).
|
|
func newTestCodec() codec.Codec {
|
|
registry := codectypes.NewInterfaceRegistry()
|
|
return codec.NewProtoCodec(registry)
|
|
}
|
|
|
|
// --- ICS-20 v1 packet helpers ------------------------------------------------
|
|
|
|
// ics20PacketData returns the ICS-20 v1 packet payload (matches
|
|
// keeper.ICS20PacketData).
|
|
func ics20PacketData(denom, amount, sender, receiver string) []byte {
|
|
bz, _ := json.Marshal(map[string]string{
|
|
"denom": denom,
|
|
"amount": amount,
|
|
"sender": sender,
|
|
"receiver": receiver,
|
|
})
|
|
return bz
|
|
}
|
|
|
|
// newPacket constructs a real channeltypes.Packet for the simtest.
|
|
func newPacket(sourcePort, sourceChannel string, sequence uint64, data []byte) channeltypes.Packet {
|
|
return channeltypes.Packet{
|
|
SourcePort: sourcePort,
|
|
SourceChannel: sourceChannel,
|
|
Sequence: sequence,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
// --- OnRecvPacket: mint wrapped Bread + denom trace + Solana ----------------
|
|
|
|
// TestOnRecvPacketMintsWrappedBread asserts OnRecvPacket mints wrapped Bread
|
|
// for a valid ICS-20 v1 packet (EVM chain).
|
|
func TestOnRecvPacketMintsWrappedBread(t *testing.T) {
|
|
ctx, _, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
packet := newPacket("transfer.Polygon", "channel-0", 1, ics20PacketData(
|
|
"transfer/channel-0/uatom", "1000", "sender-reach", "receiver-reach"))
|
|
|
|
ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress([]byte("relayer")))
|
|
if !ack.Success() {
|
|
t.Fatalf("OnRecvPacket should succeed; got error ack")
|
|
}
|
|
if len(bk.mints) != 1 {
|
|
t.Fatalf("expected 1 mint call, got %d", len(bk.mints))
|
|
}
|
|
if bk.mints[0].denom != "transfer/channel-0/uatom" {
|
|
t.Errorf("mint denom = %q, want transfer/channel-0/uatom", bk.mints[0].denom)
|
|
}
|
|
if bk.mints[0].amount != 1000 {
|
|
t.Errorf("mint amount = %d, want 1000", bk.mints[0].amount)
|
|
}
|
|
if bk.mints[0].reachID != "receiver-reach" {
|
|
t.Errorf("mint reach = %q, want receiver-reach", bk.mints[0].reachID)
|
|
}
|
|
|
|
// In-flight record written.
|
|
if _, ok := k.GetInflight(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence); !ok {
|
|
t.Error("in-flight record not written after OnRecvPacket")
|
|
}
|
|
}
|
|
|
|
// TestOnRecvPacketRejectsBadDenomTrace asserts OnRecvPacket rejects a packet
|
|
// whose denom trace lacks the `transfer/channel-N/` hop prefix.
|
|
func TestOnRecvPacketRejectsBadDenomTrace(t *testing.T) {
|
|
ctx, _, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
packet := newPacket("transfer.Polygon", "channel-0", 1, ics20PacketData(
|
|
"uatom", "1000", "sender", "receiver")) // no hop prefix
|
|
|
|
ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{})
|
|
if ack.Success() {
|
|
t.Error("OnRecvPacket should fail on bad denom trace")
|
|
}
|
|
if len(bk.mints) != 0 {
|
|
t.Errorf("no mint should happen on bad denom trace; got %d", len(bk.mints))
|
|
}
|
|
}
|
|
|
|
// TestOnRecvPacketRejectsBadICS20 asserts a malformed ICS-20 payload is rejected.
|
|
func TestOnRecvPacketRejectsBadICS20(t *testing.T) {
|
|
ctx, _, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
packet := newPacket("transfer.Polygon", "channel-0", 1, []byte("not-json"))
|
|
ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{})
|
|
if ack.Success() {
|
|
t.Error("OnRecvPacket should fail on malformed ICS-20")
|
|
}
|
|
if len(bk.mints) != 0 {
|
|
t.Errorf("no mint on bad ICS-20; got %d", len(bk.mints))
|
|
}
|
|
}
|
|
|
|
// TestOnRecvPacketSolanaGuardianSigSet asserts the Solana branch verifies the
|
|
// wormhole guardian sig set (2-of-N stub) from state before minting.
|
|
func TestOnRecvPacketSolanaGuardianSigSet(t *testing.T) {
|
|
ctx, wk, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
// Configure the frozen stub guardian set (D-054 — frozen in simtest).
|
|
k.SetGuardianSet(ctx, keeper.GuardianSet{
|
|
Guardians: []string{"guardian-1", "guardian-2", "guardian-3"},
|
|
Threshold: 2,
|
|
})
|
|
wk.signedQuorums["solana-guardians"] = true
|
|
|
|
packet := newPacket("transfer.Solana", "channel-1", 1, ics20PacketData(
|
|
"transfer/channel-1/wsol", "500", "sol-sender", "sol-receiver"))
|
|
|
|
ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{})
|
|
if !ack.Success() {
|
|
t.Fatalf("OnRecvPacket Solana should succeed with guardian quorum; got error")
|
|
}
|
|
if len(bk.mints) != 1 {
|
|
t.Fatalf("expected 1 mint for Solana, got %d", len(bk.mints))
|
|
}
|
|
if bk.mints[0].denom != "transfer/channel-1/wsol" {
|
|
t.Errorf("mint denom = %q", bk.mints[0].denom)
|
|
}
|
|
if wk.solanaCalls != 1 {
|
|
t.Errorf("expected 1 Solana guardian sig check, got %d", wk.solanaCalls)
|
|
}
|
|
}
|
|
|
|
// TestOnRecvPacketSolanaRejectsNoGuardianSet asserts the Solana branch rejects
|
|
// when the guardian set is not configured.
|
|
func TestOnRecvPacketSolanaRejectsNoGuardianSet(t *testing.T) {
|
|
ctx, _, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
// No guardian set configured.
|
|
|
|
packet := newPacket("transfer.Solana", "channel-1", 1, ics20PacketData(
|
|
"transfer/channel-1/wsol", "500", "sender", "receiver"))
|
|
|
|
ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{})
|
|
if ack.Success() {
|
|
t.Error("OnRecvPacket Solana should fail without guardian set")
|
|
}
|
|
if len(bk.mints) != 0 {
|
|
t.Errorf("no mint should happen; got %d", len(bk.mints))
|
|
}
|
|
}
|
|
|
|
// TestOnRecvPacketSolanaRejectsNoQuorum asserts the Solana branch rejects when
|
|
// the guardian sig set did not reach the 2-of-N quorum.
|
|
func TestOnRecvPacketSolanaRejectsNoQuorum(t *testing.T) {
|
|
ctx, wk, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
k.SetGuardianSet(ctx, keeper.GuardianSet{
|
|
Guardians: []string{"guardian-1", "guardian-2", "guardian-3"},
|
|
Threshold: 2,
|
|
})
|
|
wk.signedQuorums["solana-guardians"] = false // quorum NOT reached
|
|
|
|
packet := newPacket("transfer.Solana", "channel-1", 1, ics20PacketData(
|
|
"transfer/channel-1/wsol", "500", "sender", "receiver"))
|
|
ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{})
|
|
if ack.Success() {
|
|
t.Error("OnRecvPacket Solana should fail without quorum")
|
|
}
|
|
if len(bk.mints) != 0 {
|
|
t.Errorf("no mint on Solana quorum failure; got %d", len(bk.mints))
|
|
}
|
|
}
|
|
|
|
// TestOnRecvPacketRejectsZeroAmount asserts a zero/negative amount is rejected.
|
|
func TestOnRecvPacketRejectsZeroAmount(t *testing.T) {
|
|
ctx, _, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
packet := newPacket("transfer.Polygon", "channel-0", 1, ics20PacketData(
|
|
"transfer/channel-0/uatom", "0", "sender", "receiver"))
|
|
ack := im.OnRecvPacket(ctx, packet, sdk.AccAddress{})
|
|
if ack.Success() {
|
|
t.Error("OnRecvPacket should reject zero amount")
|
|
}
|
|
if len(bk.mints) != 0 {
|
|
t.Errorf("no mint on zero amount; got %d", len(bk.mints))
|
|
}
|
|
}
|
|
|
|
// --- OnAcknowledgementPacket: delete-on-first-ack + ERROR-on-second (G-021) --
|
|
|
|
// TestOnAckPacketDeletesInflightRecord asserts OnAcknowledgementPacket deletes
|
|
// the in-flight record on the first ack (replay protection mirroring ibc-go).
|
|
func TestOnAckPacketDeletesInflightRecord(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
k.SetInflight(ctx, keeper.InflightPacket{
|
|
SourcePort: "transfer.Polygon", SourceChannel: "channel-0",
|
|
Sequence: 7, Denom: "transfer/channel-0/uatom", Amount: 1000,
|
|
Sender: "s", Receiver: "r",
|
|
})
|
|
packet := newPacket("transfer.Polygon", "channel-0", 7, ics20PacketData(
|
|
"transfer/channel-0/uatom", "1000", "s", "r"))
|
|
|
|
if err := im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{}); err != nil {
|
|
t.Fatalf("first ack should succeed, got: %v", err)
|
|
}
|
|
if _, ok := k.GetInflight(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence); ok {
|
|
t.Error("in-flight record should be deleted after first ack")
|
|
}
|
|
}
|
|
|
|
// TestOnAckPacketRejectsSecondAck asserts the SECOND OnAcknowledgementPacket
|
|
// returns ERROR (G-021 — NOT a silent no-op; the A-513 CVE-class replay pitfall
|
|
// is closed by failing loudly).
|
|
func TestOnAckPacketRejectsSecondAck(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
k.SetInflight(ctx, keeper.InflightPacket{
|
|
SourcePort: "transfer.Polygon", SourceChannel: "channel-0", Sequence: 9,
|
|
})
|
|
packet := newPacket("transfer.Polygon", "channel-0", 9, ics20PacketData(
|
|
"transfer/channel-0/uatom", "1000", "s", "r"))
|
|
_ = im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{})
|
|
|
|
// Second ack: record is gone → ERROR (G-021).
|
|
err := im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{})
|
|
if err == nil {
|
|
t.Fatal("G-021: second OnAcknowledgementPacket must return ERROR, not nil (A-513 replay pitfall)")
|
|
}
|
|
}
|
|
|
|
// TestOnAckPacketNoInflightRecordReturnsError asserts an ack with no prior
|
|
// in-flight record returns ERROR (the replay signal — G-021).
|
|
func TestOnAckPacketNoInflightRecordReturnsError(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
packet := newPacket("transfer.Polygon", "channel-0", 42, ics20PacketData(
|
|
"transfer/channel-0/uatom", "1000", "s", "r"))
|
|
err := im.OnAcknowledgementPacket(ctx, packet, []byte(`{}`), sdk.AccAddress{})
|
|
if err == nil {
|
|
t.Error("ack with no in-flight record should return ERROR (G-021 replay signal)")
|
|
}
|
|
}
|
|
|
|
// --- OnTimeoutPacket: refund exactly once ------------------------------------
|
|
|
|
// TestOnTimeoutPacketRefundsOnce asserts OnTimeoutPacket refunds the
|
|
// source-chain escrow via the BreadKeeper shim exactly once.
|
|
func TestOnTimeoutPacketRefundsOnce(t *testing.T) {
|
|
ctx, _, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
k.SetInflight(ctx, keeper.InflightPacket{
|
|
SourcePort: "transfer.Polygon", SourceChannel: "channel-0",
|
|
Sequence: 3, Denom: "transfer/channel-0/uatom", Amount: 750,
|
|
Sender: "timeout-sender", Receiver: "r", Refunded: false,
|
|
})
|
|
packet := newPacket("transfer.Polygon", "channel-0", 3, ics20PacketData(
|
|
"transfer/channel-0/uatom", "750", "timeout-sender", "r"))
|
|
|
|
if err := im.OnTimeoutPacket(ctx, packet, sdk.AccAddress{}); err != nil {
|
|
t.Fatalf("first timeout should succeed: %v", err)
|
|
}
|
|
if len(bk.releases) != 1 {
|
|
t.Fatalf("expected 1 release on first timeout, got %d", len(bk.releases))
|
|
}
|
|
if bk.releases[0].amount != 750 {
|
|
t.Errorf("release amount = %d, want 750", bk.releases[0].amount)
|
|
}
|
|
if bk.releases[0].reachID != "timeout-sender" {
|
|
t.Errorf("release reach = %q, want timeout-sender", bk.releases[0].reachID)
|
|
}
|
|
|
|
// Second timeout: no-op (Refunded flag guards exactly-once).
|
|
if err := im.OnTimeoutPacket(ctx, packet, sdk.AccAddress{}); err != nil {
|
|
t.Fatalf("second timeout should be a no-op (nil), got: %v", err)
|
|
}
|
|
if len(bk.releases) != 1 {
|
|
t.Errorf("second timeout should NOT refund again; got %d releases total", len(bk.releases))
|
|
}
|
|
}
|
|
|
|
// TestOnTimeoutPacketNoInflightRecordIsNoop asserts a timeout with no
|
|
// in-flight record is a benign no-op (not an error).
|
|
func TestOnTimeoutPacketNoInflightRecordIsNoop(t *testing.T) {
|
|
ctx, _, bk, k := newSimtestContext(t)
|
|
im := keeper.NewIBCModule(k)
|
|
|
|
packet := newPacket("transfer.Polygon", "channel-0", 99, ics20PacketData(
|
|
"transfer/channel-0/uatom", "1000", "s", "r"))
|
|
err := im.OnTimeoutPacket(ctx, packet, sdk.AccAddress{})
|
|
if err != nil {
|
|
t.Errorf("timeout with no in-flight record should be a no-op (nil); got %v", err)
|
|
}
|
|
if len(bk.releases) != 0 {
|
|
t.Errorf("no release should happen; got %d", len(bk.releases))
|
|
}
|
|
}
|
|
|
|
// --- BridgeStatus lifecycle (MsgServer) --------------------------------------
|
|
|
|
// TestBridgeStatusLifecycle asserts the full BridgeStatus lifecycle:
|
|
// Pending → Attested → Active → Closed.
|
|
func TestBridgeStatusLifecycle(t *testing.T) {
|
|
ctx, wk, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{
|
|
BridgeID: "bridge-1", L2Chain: "Polygon", Status: bridgetypes.BridgePending,
|
|
})
|
|
wk.signedQuorums["quorum-1"] = true
|
|
|
|
// Pending → Attested.
|
|
if _, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{
|
|
BridgeID: "bridge-1", WatcherQuorumID: "quorum-1", Signer: "watcher-reach",
|
|
}); err != nil {
|
|
t.Fatalf("AttestBridgeRoute: %v", err)
|
|
}
|
|
r, _ := k.GetBridgeRoute(ctx, "bridge-1")
|
|
if r.Status != bridgetypes.BridgeAttested {
|
|
t.Errorf("after attest, status = %q, want Attested", r.Status)
|
|
}
|
|
if r.WatcherQuorumID != "quorum-1" {
|
|
t.Errorf("watcher quorum id = %q, want quorum-1", r.WatcherQuorumID)
|
|
}
|
|
|
|
// Attested → Active.
|
|
if _, err := srv.ActivateBridge(ctx, &bridgetypes.MsgActivateBridge{
|
|
BridgeID: "bridge-1", Signer: "watcher-reach",
|
|
}); err != nil {
|
|
t.Fatalf("ActivateBridge: %v", err)
|
|
}
|
|
r, _ = k.GetBridgeRoute(ctx, "bridge-1")
|
|
if r.Status != bridgetypes.BridgeActive {
|
|
t.Errorf("after activate, status = %q, want Active", r.Status)
|
|
}
|
|
|
|
// Active → Closed.
|
|
if _, err := srv.CloseBridge(ctx, &bridgetypes.MsgCloseBridge{
|
|
BridgeID: "bridge-1", Signer: "watcher-reach",
|
|
}); err != nil {
|
|
t.Fatalf("CloseBridge: %v", err)
|
|
}
|
|
r, _ = k.GetBridgeRoute(ctx, "bridge-1")
|
|
if r.Status != bridgetypes.BridgeClosed {
|
|
t.Errorf("after close, status = %q, want Closed", r.Status)
|
|
}
|
|
}
|
|
|
|
// TestAttestBridgeRouteRejectsBadStatus asserts AttestBridgeRoute rejects a
|
|
// route that is not Pending.
|
|
func TestAttestBridgeRouteRejectsBadStatus(t *testing.T) {
|
|
ctx, wk, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
wk.signedQuorums["quorum-1"] = true
|
|
|
|
k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{
|
|
BridgeID: "bridge-2", L2Chain: "Base", Status: bridgetypes.BridgeActive,
|
|
})
|
|
_, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{
|
|
BridgeID: "bridge-2", WatcherQuorumID: "quorum-1", Signer: "watcher-reach",
|
|
})
|
|
if err == nil {
|
|
t.Error("AttestBridgeRoute should reject an Active route (must be Pending)")
|
|
}
|
|
}
|
|
|
|
// TestAttestBridgeRouteRejectsNoQuorum asserts AttestBridgeRoute rejects when
|
|
// the Watcher quorum did not reach threshold.
|
|
func TestAttestBridgeRouteRejectsNoQuorum(t *testing.T) {
|
|
ctx, wk, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
wk.signedQuorums["quorum-1"] = false
|
|
|
|
k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{
|
|
BridgeID: "bridge-3", L2Chain: "Polygon", Status: bridgetypes.BridgePending,
|
|
})
|
|
_, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{
|
|
BridgeID: "bridge-3", WatcherQuorumID: "quorum-1", Signer: "watcher-reach",
|
|
})
|
|
if err == nil {
|
|
t.Error("AttestBridgeRoute should reject when Watcher quorum not signed")
|
|
}
|
|
}
|
|
|
|
// TestAttestBridgeRouteRejectsNotFound asserts AttestBridgeRoute rejects a
|
|
// missing route.
|
|
func TestAttestBridgeRouteRejectsNotFound(t *testing.T) {
|
|
ctx, wk, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
wk.signedQuorums["quorum-1"] = true
|
|
|
|
_, err := srv.AttestBridgeRoute(ctx, &bridgetypes.MsgAttestBridgeRoute{
|
|
BridgeID: "missing", WatcherQuorumID: "quorum-1", Signer: "watcher-reach",
|
|
})
|
|
if err == nil {
|
|
t.Error("AttestBridgeRoute should reject a missing route")
|
|
}
|
|
}
|
|
|
|
// TestActivateBridgeRejectsBadStatus asserts ActivateBridge rejects a route
|
|
// that is not Attested.
|
|
func TestActivateBridgeRejectsBadStatus(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{
|
|
BridgeID: "bridge-4", L2Chain: "Polygon", Status: bridgetypes.BridgePending,
|
|
})
|
|
_, err := srv.ActivateBridge(ctx, &bridgetypes.MsgActivateBridge{
|
|
BridgeID: "bridge-4", Signer: "watcher-reach",
|
|
})
|
|
if err == nil {
|
|
t.Error("ActivateBridge should reject a Pending route (must be Attested)")
|
|
}
|
|
}
|
|
|
|
// TestCloseBridgeRejectsBadStatus asserts CloseBridge rejects a route that is
|
|
// not Active.
|
|
func TestCloseBridgeRejectsBadStatus(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{
|
|
BridgeID: "bridge-5", L2Chain: "Polygon", Status: bridgetypes.BridgeAttested,
|
|
})
|
|
_, err := srv.CloseBridge(ctx, &bridgetypes.MsgCloseBridge{
|
|
BridgeID: "bridge-5", Signer: "watcher-reach",
|
|
})
|
|
if err == nil {
|
|
t.Error("CloseBridge should reject an Attested route (must be Active)")
|
|
}
|
|
}
|
|
|
|
// --- ValidateBasic (Msg types) -----------------------------------------------
|
|
|
|
func TestMsgAttestBridgeRouteValidateBasic(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
msg bridgetypes.MsgAttestBridgeRoute
|
|
ok bool
|
|
}{
|
|
{"valid", bridgetypes.MsgAttestBridgeRoute{"b1", "q1", "s"}, true},
|
|
{"empty bridge-id", bridgetypes.MsgAttestBridgeRoute{"", "q1", "s"}, false},
|
|
{"empty quorum-id", bridgetypes.MsgAttestBridgeRoute{"b1", "", "s"}, false},
|
|
{"empty signer", bridgetypes.MsgAttestBridgeRoute{"b1", "q1", ""}, false},
|
|
}
|
|
for _, c := range cases {
|
|
err := c.msg.ValidateBasic()
|
|
if c.ok && err != nil {
|
|
t.Errorf("%s: expected ok, got %v", c.name, err)
|
|
}
|
|
if !c.ok && err == nil {
|
|
t.Errorf("%s: expected error, got nil", c.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMsgActivateBridgeValidateBasic(t *testing.T) {
|
|
if err := (&bridgetypes.MsgActivateBridge{BridgeID: "b1", Signer: "s"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&bridgetypes.MsgActivateBridge{BridgeID: "", Signer: "s"}).ValidateBasic(); err == nil {
|
|
t.Error("empty bridge-id should fail")
|
|
}
|
|
}
|
|
|
|
func TestMsgCloseBridgeValidateBasic(t *testing.T) {
|
|
if err := (&bridgetypes.MsgCloseBridge{BridgeID: "b1", Signer: "s"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&bridgetypes.MsgCloseBridge{BridgeID: "b1", Signer: ""}).ValidateBasic(); err == nil {
|
|
t.Error("empty signer should fail")
|
|
}
|
|
}
|
|
|
|
// TestMsgGetSigners asserts GetSigners returns the signer reach-id as bytes.
|
|
func TestMsgGetSigners(t *testing.T) {
|
|
m := &bridgetypes.MsgAttestBridgeRoute{Signer: "watcher-reach"}
|
|
addrs := m.GetSigners()
|
|
if len(addrs) != 1 {
|
|
t.Fatalf("expected 1 signer, got %d", len(addrs))
|
|
}
|
|
if string(addrs[0]) != "watcher-reach" {
|
|
t.Errorf("signer = %q, want watcher-reach", string(addrs[0]))
|
|
}
|
|
}
|
|
|
|
// --- Denom trace parser ------------------------------------------------------
|
|
|
|
func TestParseDenomTrace(t *testing.T) {
|
|
cases := []struct {
|
|
denom string
|
|
wantPrefix string
|
|
wantBase string
|
|
}{
|
|
{"transfer/channel-0/uatom", "transfer/channel-0", "uatom"},
|
|
{"transfer/channel-1/wsol", "transfer/channel-1", "wsol"},
|
|
{"uatom", "", "uatom"},
|
|
{"", "", ""},
|
|
}
|
|
for _, c := range cases {
|
|
p, b := keeper.ParseDenomTrace(c.denom)
|
|
if p != c.wantPrefix || b != c.wantBase {
|
|
t.Errorf("ParseDenomTrace(%q) = (%q,%q), want (%q,%q)", c.denom, p, b, c.wantPrefix, c.wantBase)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateDenomTrace(t *testing.T) {
|
|
if err := keeper.ValidateDenomTrace("transfer/channel-0/uatom"); err != nil {
|
|
t.Errorf("valid denom trace: %v", err)
|
|
}
|
|
if err := keeper.ValidateDenomTrace("uatom"); err == nil {
|
|
t.Error("bare denom (no hop prefix) should fail")
|
|
}
|
|
if err := keeper.ValidateDenomTrace(""); err == nil {
|
|
t.Error("empty denom should fail")
|
|
}
|
|
}
|
|
|
|
// --- Keeper store helpers ----------------------------------------------------
|
|
|
|
func TestSetGetBridgeRoute(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
r := bridgetypes.BridgeRoute{BridgeID: "b9", L2Chain: "Polygon", Status: bridgetypes.BridgePending}
|
|
k.SetBridgeRoute(ctx, r)
|
|
got, ok := k.GetBridgeRoute(ctx, "b9")
|
|
if !ok {
|
|
t.Fatal("GetBridgeRoute: not found")
|
|
}
|
|
if got.L2Chain != "Polygon" {
|
|
t.Errorf("L2Chain = %q", got.L2Chain)
|
|
}
|
|
if _, ok := k.GetBridgeRoute(ctx, "missing"); ok {
|
|
t.Error("GetBridgeRoute should return false for missing route")
|
|
}
|
|
}
|
|
|
|
func TestAllBridgeRoutes(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{BridgeID: "b1", Status: bridgetypes.BridgePending})
|
|
k.SetBridgeRoute(ctx, bridgetypes.BridgeRoute{BridgeID: "b2", Status: bridgetypes.BridgeActive})
|
|
all := k.AllBridgeRoutes(ctx)
|
|
if len(all) != 2 {
|
|
t.Errorf("expected 2 routes, got %d", len(all))
|
|
}
|
|
}
|
|
|
|
func TestGuardianSetStore(t *testing.T) {
|
|
ctx, _, _, k := newSimtestContext(t)
|
|
gs := keeper.GuardianSet{
|
|
Guardians: []string{"g1", "g2", "g3"}, Threshold: 2,
|
|
}
|
|
k.SetGuardianSet(ctx, gs)
|
|
got, ok := k.GetGuardianSet(ctx)
|
|
if !ok {
|
|
t.Fatal("GetGuardianSet: not found")
|
|
}
|
|
if got.Threshold != 2 {
|
|
t.Errorf("threshold = %d, want 2", got.Threshold)
|
|
}
|
|
if len(got.Guardians) != 3 {
|
|
t.Errorf("guardians = %d, want 3", len(got.Guardians))
|
|
}
|
|
}
|