Merge phase/03 into milestone/v0.5-bearers-runtime (P3 complete → v0.4.3)
---ci--- project: oy phase: 3 milestone: v0.5 status: complete requirements: covered: [REQ-035] partial: [] ---/ci---
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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/partner/types"
|
||||
)
|
||||
|
||||
// keeper.go holds the store-backed Keeper for the partner module's
|
||||
// Anchor credential runtime (P3-02-01, REQ-035).
|
||||
//
|
||||
// The Keeper wraps an sdk.KVStore via a storeKey. It holds the
|
||||
// AnchorCredential records (by anchor-id). The v0.3 in-memory registry
|
||||
// Keeper stub (types.Keeper, x/partner/types/types.go) is RETAINED for
|
||||
// the Partner registry (non-Anchor partners — the v0.3 skeleton); the
|
||||
// v0.5 runtime promotes ONLY the Anchor credential lifecycle to a
|
||||
// store-backed keeper (D-054 simtest grade). The Partner registry stays
|
||||
// on the v0.3 in-memory stub (non-Anchor partner tiers are not promoted
|
||||
// in v0.5 — out of scope; only the Anchor credential lifecycle is).
|
||||
//
|
||||
// The Keeper also holds the two expected-keeper shims (WatcherKeeper for
|
||||
// 6-of-9 quorum authz on issue/revoke; HubKeeper for custody-provider-id
|
||||
// validity on onboard — the P3→P4 hub dep edge broken by the interface
|
||||
// shim per G-003 / ARCHITECTURE.md v0.5). The shims are interfaces
|
||||
// (G-003 — no struct import of x/watcher/types or x/hub/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 partner Anchor-credential keeper.
|
||||
type Keeper struct {
|
||||
cdc codec.Codec
|
||||
storeKey storetypes.StoreKey
|
||||
watcherKeeper types.WatcherKeeper
|
||||
hubKeeper types.HubKeeper
|
||||
}
|
||||
|
||||
// NewKeeper constructs a new store-backed partner Anchor-credential
|
||||
// Keeper. The WatcherKeeper and HubKeeper expected-keeper shims are
|
||||
// injected (nil-able for partial tests; the handlers guard nil shims
|
||||
// and skip the corresponding authz/validity check, still mutating state
|
||||
// — the simtest wiring document this). The HubKeeper shim is the P3→P4
|
||||
// hub dep edge: in P3 simtest it is wired to a stub (G-003 test
|
||||
// exemption); the real hub keeper is wired in P4.
|
||||
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, wk types.WatcherKeeper, hk types.HubKeeper) Keeper {
|
||||
return Keeper{
|
||||
cdc: cdc,
|
||||
storeKey: storeKey,
|
||||
watcherKeeper: wk,
|
||||
hubKeeper: hk,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 }
|
||||
|
||||
// SetHubKeeper sets the HubKeeper expected-keeper shim (for
|
||||
// post-construction wiring, e.g., app wiring or test setup). This is
|
||||
// the P3→P4 hub dep edge: P4 wires the real hub keeper via this setter
|
||||
// or via NewKeeper.
|
||||
func (k *Keeper) SetHubKeeper(hk types.HubKeeper) { k.hubKeeper = hk }
|
||||
|
||||
// --- Anchor credential store -------------------------------------------------
|
||||
|
||||
var anchorKeyPrefix = []byte("anchor/")
|
||||
|
||||
func anchorKey(anchorID string) []byte {
|
||||
return append(anchorKeyPrefix, []byte(anchorID)...)
|
||||
}
|
||||
|
||||
// GetAnchorCredential loads an AnchorCredential by anchor-id. Returns the
|
||||
// credential and true if found, or zero value + false if not.
|
||||
func (k Keeper) GetAnchorCredential(ctx sdk.Context, anchorID string) (types.AnchorCredential, bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(anchorKey(anchorID))
|
||||
if bz == nil {
|
||||
return types.AnchorCredential{}, false
|
||||
}
|
||||
var c types.AnchorCredential
|
||||
if err := json.Unmarshal(bz, &c); err != nil {
|
||||
return types.AnchorCredential{}, false
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
|
||||
// SetAnchorCredential persists an AnchorCredential by anchor-id.
|
||||
func (k Keeper) SetAnchorCredential(ctx sdk.Context, c types.AnchorCredential) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("partner: marshal anchor credential %q: %v", c.AnchorID, err))
|
||||
}
|
||||
store.Set(anchorKey(c.AnchorID), bz)
|
||||
}
|
||||
|
||||
// AllAnchorCredentials returns all persisted AnchorCredential records
|
||||
// (iteration helper).
|
||||
func (k Keeper) AllAnchorCredentials(ctx sdk.Context) []types.AnchorCredential {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
iterator := store.Iterator(anchorKeyPrefix, prefixEnd(anchorKeyPrefix))
|
||||
defer iterator.Close()
|
||||
out := []types.AnchorCredential{}
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var c types.AnchorCredential
|
||||
if err := json.Unmarshal(iterator.Value(), &c); err == nil {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
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,273 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/oy/openyield/x/partner/types"
|
||||
)
|
||||
|
||||
// msg_server.go implements the partner module's Anchor-credential MsgServer
|
||||
// (P3-02-01, REQ-035; G-023 ownership split: cosmos-engineer scaffolds the
|
||||
// file structure + method signatures; backend-engineer implements the
|
||||
// handler logic bodies). The MsgServer wraps the Keeper + the WatcherKeeper
|
||||
// and HubKeeper expected-keeper shims (already on the Keeper).
|
||||
//
|
||||
// Each method returns a (*Response, error). Handler state-machine ordering
|
||||
// is enforced: ValidateBasic → keeper authz → state mutation →
|
||||
// ctx.EventManager().EmitEvent.
|
||||
//
|
||||
// Lifecycle (REQ-035, vision §13):
|
||||
// - IssueAnchorCredential → Pending (Watcher 6-of-9 quorum authz)
|
||||
// - OnboardAnchor → Pending → Onboarded (HubKeeper custody-
|
||||
// provider-id validity check)
|
||||
// - SuspendAnchorCredential → Onboarded → Suspended
|
||||
// - RevokeAnchorCredential → any → Revoked (Watcher 6-of-9 quorum authz)
|
||||
//
|
||||
// Invalid transitions are REJECTED (the simtest covers each). Revoked is
|
||||
// terminal (idempotent reject on a second Revoke — NOT double-effect).
|
||||
//
|
||||
// Nil-shim behavior (simtest wiring): a nil WatcherKeeper shim skips the
|
||||
// Watcher quorum authz (the handler still mutates state — the simtest
|
||||
// documents the wiring contract). A nil HubKeeper shim skips the
|
||||
// custody-service-exists check (the OnboardAnchor still transitions — the
|
||||
// simtest documents the wiring contract). The P3→P4 hub dep edge: in P3
|
||||
// simtest, the HubKeeper shim is wired to a stub (G-003 test exemption);
|
||||
// the real hub keeper is wired in P4.
|
||||
|
||||
// msgServer is the concrete MsgServer implementation wrapping the Keeper.
|
||||
type msgServer struct {
|
||||
Keeper
|
||||
}
|
||||
|
||||
// NewMsgServerImpl returns the partner 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("partner: 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()
|
||||
}
|
||||
|
||||
// issuePayload is the byte payload the Watcher quorum signs over for an
|
||||
// IssueAnchorCredential. It binds the anchor-id + credential-uri + issuer
|
||||
// to the quorum signature (a quorum signature over a different payload
|
||||
// does not authorize this issuance).
|
||||
func issuePayload(msg *types.MsgIssueAnchorCredential) []byte {
|
||||
return []byte(fmt.Sprintf("partner.issue:%s:%s:%s", msg.AnchorID, msg.CredentialURI, msg.Issuer))
|
||||
}
|
||||
|
||||
// revokePayload is the byte payload the Watcher quorum signs over for a
|
||||
// RevokeAnchorCredential. It binds the anchor-id + signer to the quorum
|
||||
// signature (a quorum signature over a different payload does not
|
||||
// authorize this revocation).
|
||||
func revokePayload(msg *types.MsgRevokeAnchorCredential) []byte {
|
||||
return []byte(fmt.Sprintf("partner.revoke:%s:%s", msg.AnchorID, msg.Signer))
|
||||
}
|
||||
|
||||
// --- IssueAnchorCredential (creates credential status=Pending) ---------------
|
||||
|
||||
// IssueAnchorCredential issues an Anchor credential (status=Pending).
|
||||
// The handler enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. Idempotency: anchor-id must not already exist.
|
||||
// 3. Watcher 6-of-9 quorum authz (REQ-004) via the WatcherKeeper shim
|
||||
// on the issuance payload. A nil shim skips this check (simtest
|
||||
// wiring); a non-nil shim that returns false REJECTS the issuance.
|
||||
//
|
||||
// On success the credential is persisted with status=Pending and an
|
||||
// event is emitted.
|
||||
func (s msgServer) IssueAnchorCredential(ctx interface{}, msg *types.MsgIssueAnchorCredential) (*types.MsgIssueAnchorCredentialResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
// Idempotency: anchor-id must not already exist.
|
||||
if _, ok := s.Keeper.GetAnchorCredential(sdkCtx, msg.AnchorID); ok {
|
||||
return nil, fmt.Errorf("partner: anchor credential %q already exists", msg.AnchorID)
|
||||
}
|
||||
|
||||
// Watcher 6-of-9 quorum authz (REQ-004). A nil shim skips the authz
|
||||
// (simtest wiring); a non-nil shim that returns false REJECTS.
|
||||
if s.Keeper.watcherKeeper != nil {
|
||||
if !s.Keeper.watcherKeeper.IsQuorumSigned(msg.WatcherQuorumID, issuePayload(msg)) {
|
||||
return nil, fmt.Errorf("partner: watcher quorum %q did not authorize issuance of anchor %q (REQ-004 6-of-9)", msg.WatcherQuorumID, msg.AnchorID)
|
||||
}
|
||||
}
|
||||
|
||||
cred := types.AnchorCredential{
|
||||
AnchorID: msg.AnchorID,
|
||||
CustodyProviderID: "", // empty — set on OnboardAnchor
|
||||
CredentialURI: msg.CredentialURI,
|
||||
AttestationCount: 0,
|
||||
Status: types.AnchorPending,
|
||||
}
|
||||
s.Keeper.SetAnchorCredential(sdkCtx, cred)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"partner.anchor_credential_issued",
|
||||
sdk.NewAttribute("anchor_id", msg.AnchorID),
|
||||
sdk.NewAttribute("credential_uri", msg.CredentialURI),
|
||||
sdk.NewAttribute("watcher_quorum_id", msg.WatcherQuorumID),
|
||||
sdk.NewAttribute("issuer", msg.Issuer),
|
||||
sdk.NewAttribute("status", string(types.AnchorPending)),
|
||||
))
|
||||
return &types.MsgIssueAnchorCredentialResponse{}, nil
|
||||
}
|
||||
|
||||
// --- OnboardAnchor (Pending → Onboarded) -------------------------------------
|
||||
|
||||
// OnboardAnchor transitions an Anchor credential Pending → Onboarded.
|
||||
// The handler enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. The credential must exist.
|
||||
// 3. The source status must be Pending (ValidAnchorTransition(Pending,
|
||||
// Onboarded) — the lifecycle gate).
|
||||
// 4. The custody-provider-id must reference a LIVE Hub custody service
|
||||
// via the HubKeeper shim (the P3→P4 hub dep edge). A nil shim skips
|
||||
// this check (simtest wiring); a non-nil shim that returns false
|
||||
// REJECTS the onboarding (the credential stays Pending).
|
||||
// 5. The custody-provider-id on the credential is set from the msg
|
||||
// (the msg carries the custody-provider-id to bind to).
|
||||
//
|
||||
// On success the credential's CustodyProviderID is set, the status is
|
||||
// transitioned to Onboarded, and an event is emitted.
|
||||
func (s msgServer) OnboardAnchor(ctx interface{}, msg *types.MsgOnboardAnchor) (*types.MsgOnboardAnchorResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
cred, ok := s.Keeper.GetAnchorCredential(sdkCtx, msg.AnchorID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("partner: anchor credential %q not found", msg.AnchorID)
|
||||
}
|
||||
|
||||
// Lifecycle gate: Pending → Onboarded is the only valid transition
|
||||
// into Onboarded.
|
||||
if !types.ValidAnchorTransition(cred.Status, types.AnchorOnboarded) {
|
||||
return nil, fmt.Errorf("partner: anchor %q status %q cannot transition to Onboarded (REQ-035 lifecycle)", msg.AnchorID, cred.Status)
|
||||
}
|
||||
|
||||
// HubKeeper custody-service-exists check (the P3→P4 hub dep edge).
|
||||
// A nil shim skips the check (simtest wiring); a non-nil shim that
|
||||
// returns false REJECTS the onboarding (the credential stays Pending).
|
||||
if s.Keeper.hubKeeper != nil {
|
||||
if !s.Keeper.hubKeeper.CustodyServiceExists(msg.CustodyProviderID) {
|
||||
return nil, fmt.Errorf("partner: custody service %q does not exist (OnboardAnchor rejected — anchor %q stays Pending)", msg.CustodyProviderID, msg.AnchorID)
|
||||
}
|
||||
}
|
||||
|
||||
// Transition: set custody-provider-id + status=Onboarded.
|
||||
cred.CustodyProviderID = msg.CustodyProviderID
|
||||
cred.Status = types.AnchorOnboarded
|
||||
s.Keeper.SetAnchorCredential(sdkCtx, cred)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"partner.anchor_onboarded",
|
||||
sdk.NewAttribute("anchor_id", msg.AnchorID),
|
||||
sdk.NewAttribute("custody_provider_id", msg.CustodyProviderID),
|
||||
sdk.NewAttribute("status", string(types.AnchorOnboarded)),
|
||||
))
|
||||
return &types.MsgOnboardAnchorResponse{}, nil
|
||||
}
|
||||
|
||||
// --- SuspendAnchorCredential (Onboarded → Suspended) ------------------------
|
||||
|
||||
// SuspendAnchorCredential transitions an Anchor credential
|
||||
// Onboarded → Suspended. The handler enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. The credential must exist.
|
||||
// 3. The source status must be Onboarded (ValidAnchorTransition(Onboarded,
|
||||
// Suspended) — the lifecycle gate).
|
||||
//
|
||||
// On success the status is transitioned to Suspended and an event is
|
||||
// emitted.
|
||||
func (s msgServer) SuspendAnchorCredential(ctx interface{}, msg *types.MsgSuspendAnchorCredential) (*types.MsgSuspendAnchorCredentialResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
cred, ok := s.Keeper.GetAnchorCredential(sdkCtx, msg.AnchorID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("partner: anchor credential %q not found", msg.AnchorID)
|
||||
}
|
||||
|
||||
if !types.ValidAnchorTransition(cred.Status, types.AnchorSuspended) {
|
||||
return nil, fmt.Errorf("partner: anchor %q status %q cannot transition to Suspended (REQ-035 lifecycle)", msg.AnchorID, cred.Status)
|
||||
}
|
||||
|
||||
cred.Status = types.AnchorSuspended
|
||||
s.Keeper.SetAnchorCredential(sdkCtx, cred)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"partner.anchor_credential_suspended",
|
||||
sdk.NewAttribute("anchor_id", msg.AnchorID),
|
||||
sdk.NewAttribute("status", string(types.AnchorSuspended)),
|
||||
))
|
||||
return &types.MsgSuspendAnchorCredentialResponse{}, nil
|
||||
}
|
||||
|
||||
// --- RevokeAnchorCredential (any → Revoked, Watcher quorum authz) ------------
|
||||
|
||||
// RevokeAnchorCredential transitions an Anchor credential to Revoked
|
||||
// (terminal). The handler enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. The credential must exist.
|
||||
// 3. The credential must not already be Revoked (idempotent reject — a
|
||||
// second Revoke returns an error; NOT double-effect).
|
||||
// 4. Watcher 6-of-9 quorum authz (REQ-004) via the WatcherKeeper shim
|
||||
// on the revocation payload. A nil shim skips this check (simtest
|
||||
// wiring); a non-nil shim that returns false REJECTS the revocation.
|
||||
//
|
||||
// On success the status is transitioned to Revoked (terminal) and an
|
||||
// event is emitted.
|
||||
func (s msgServer) RevokeAnchorCredential(ctx interface{}, msg *types.MsgRevokeAnchorCredential) (*types.MsgRevokeAnchorCredentialResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
cred, ok := s.Keeper.GetAnchorCredential(sdkCtx, msg.AnchorID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("partner: anchor credential %q not found", msg.AnchorID)
|
||||
}
|
||||
|
||||
// Idempotent reject: a Revoked credential cannot be re-revoked.
|
||||
if cred.Status == types.AnchorRevoked {
|
||||
return nil, fmt.Errorf("partner: anchor %q already revoked (idempotent reject — no double-effect)", msg.AnchorID)
|
||||
}
|
||||
|
||||
// Watcher 6-of-9 quorum authz (REQ-004). A nil shim skips the authz
|
||||
// (simtest wiring); a non-nil shim that returns false REJECTS.
|
||||
if s.Keeper.watcherKeeper != nil {
|
||||
if !s.Keeper.watcherKeeper.IsQuorumSigned(msg.WatcherQuorumID, revokePayload(msg)) {
|
||||
return nil, fmt.Errorf("partner: watcher quorum %q did not authorize revocation of anchor %q (REQ-004 6-of-9)", msg.WatcherQuorumID, msg.AnchorID)
|
||||
}
|
||||
}
|
||||
|
||||
cred.Status = types.AnchorRevoked
|
||||
s.Keeper.SetAnchorCredential(sdkCtx, cred)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"partner.anchor_credential_revoked",
|
||||
sdk.NewAttribute("anchor_id", msg.AnchorID),
|
||||
sdk.NewAttribute("watcher_quorum_id", msg.WatcherQuorumID),
|
||||
sdk.NewAttribute("status", string(types.AnchorRevoked)),
|
||||
))
|
||||
return &types.MsgRevokeAnchorCredentialResponse{}, nil
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
package keeper_test
|
||||
|
||||
// msg_server_simtest_test.go is the x/partner keeper simtest (P3-03-01,
|
||||
// REQ-035).
|
||||
//
|
||||
// D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no
|
||||
// real hub/watcher keepers. The simtest wires the expected-keeper shims
|
||||
// (WatcherKeeper, HubKeeper) to in-test stubs (G-003 test exemption: the
|
||||
// test imports x/partner/keeper + defines stub types that satisfy the
|
||||
// interfaces; no production struct imports across x/<module>/types).
|
||||
//
|
||||
// Coverage (REQ-035 lifecycle Pending → Onboarded → Suspended → Revoked):
|
||||
// - Full success lifecycle: Issue (Pending) → Onboard → Suspend → Revoke.
|
||||
// - Invalid transitions REJECTED:
|
||||
// - Onboard on a non-Pending credential (Onboarded/Suspended/Revoked
|
||||
// source) → error.
|
||||
// - Suspend on a non-Onboarded credential (Pending/Suspended/Revoked
|
||||
// source) → error.
|
||||
// - Revoke on an already-Revoked credential → idempotent reject (no
|
||||
// double-effect).
|
||||
// - HubKeeper shim wiring (P3→P4 hub dep edge):
|
||||
// - OnboardAnchor with a custody-provider-id that the HubKeeper stub
|
||||
// reports as non-existent → REJECTED (credential stays Pending).
|
||||
// - OnboardAnchor with a custody-provider-id that the HubKeeper stub
|
||||
// reports as existent → transitions to Onboarded.
|
||||
// - Nil HubKeeper shim → skips the check (simtest wiring); the
|
||||
// OnboardAnchor transitions regardless.
|
||||
// - Watcher quorum authz (REQ-004 6-of-9):
|
||||
// - IssueAnchorCredential with a WatcherKeeper stub that reports
|
||||
// quorum NOT signed → REJECTED (credential NOT created).
|
||||
// - IssueAnchorCredential with quorum signed → credential created
|
||||
// (Pending).
|
||||
// - RevokeAnchorCredential with quorum NOT signed → REJECTED
|
||||
// (credential stays in its pre-revoke status).
|
||||
// - Nil WatcherKeeper shim → skips the authz (simtest wiring); the
|
||||
// handler mutates state.
|
||||
// - Idempotency: IssueAnchorCredential on an existing anchor-id →
|
||||
// error.
|
||||
// - NotFound: Onboard/Suspend/Revoke on a missing anchor-id → error.
|
||||
// - ValidateBasic: each Msg* ValidateBasic error path.
|
||||
//
|
||||
// Coverage target: ≥80% on x/partner/keeper.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/oy/openyield/x/partner/keeper"
|
||||
ptypes "github.com/oy/openyield/x/partner/types"
|
||||
)
|
||||
|
||||
// --- Stub expected-keepers (G-003 test exemption) ---------------------------
|
||||
|
||||
// stubWatcherKeeper satisfies ptypes.WatcherKeeper for the simtest. It
|
||||
// records IsQuorumSigned calls for assertion and returns the configured
|
||||
// result (true by default — quorum signed).
|
||||
type stubWatcherKeeper struct {
|
||||
calls []watcherCall
|
||||
signedResult bool // configurable; default true (quorum signed)
|
||||
}
|
||||
|
||||
type watcherCall struct {
|
||||
quorumID string
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func (s *stubWatcherKeeper) IsQuorumSigned(quorumID string, payload []byte) bool {
|
||||
s.calls = append(s.calls, watcherCall{quorumID, payload})
|
||||
return s.signedResult
|
||||
}
|
||||
|
||||
// stubHubKeeper satisfies ptypes.HubKeeper for the simtest. It records
|
||||
// CustodyServiceExists calls for assertion and returns the configured
|
||||
// result per custody-provider-id (default: exists=true).
|
||||
type stubHubKeeper struct {
|
||||
calls []hubCall
|
||||
exists map[string]bool // custody-provider-id → exists
|
||||
existsAll bool // if true, CustodyServiceExists returns true for all ids
|
||||
}
|
||||
|
||||
type hubCall struct {
|
||||
custodyProviderID string
|
||||
}
|
||||
|
||||
func (s *stubHubKeeper) CustodyServiceExists(custodyProviderID string) bool {
|
||||
s.calls = append(s.calls, hubCall{custodyProviderID})
|
||||
if s.exists != nil {
|
||||
return s.exists[custodyProviderID]
|
||||
}
|
||||
return s.existsAll
|
||||
}
|
||||
|
||||
// --- Simtest context helper --------------------------------------------------
|
||||
|
||||
// newSimtestContext constructs an in-memory sdk.Context with a KVStore
|
||||
// mounted at the partner store key. D-054: in-memory, no real hub/watcher
|
||||
// keepers. Returns the ctx, the stub WatcherKeeper, the stub HubKeeper,
|
||||
// and the Keeper.
|
||||
func newSimtestContext(t *testing.T) (sdk.Context, *stubWatcherKeeper, *stubHubKeeper, keeper.Keeper) {
|
||||
t.Helper()
|
||||
db := dbm.NewMemDB()
|
||||
cdc := newTestCodec()
|
||||
storeKey := storetypes.NewKVStoreKey(ptypes.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)
|
||||
}
|
||||
// Block time set to a fixed unix second so lifecycle timestamps are
|
||||
// deterministic (now = 1000).
|
||||
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
||||
|
||||
wk := &stubWatcherKeeper{signedResult: true}
|
||||
hk := &stubHubKeeper{existsAll: true}
|
||||
k := keeper.NewKeeper(cdc, storeKey, wk, hk)
|
||||
return ctx, wk, hk, k
|
||||
}
|
||||
|
||||
// newTestCodec constructs a minimal codec for the simtest.
|
||||
func newTestCodec() codec.Codec {
|
||||
registry := codectypes.NewInterfaceRegistry()
|
||||
return codec.NewProtoCodec(registry)
|
||||
}
|
||||
|
||||
// hasEvent reports whether ctx emitted an event of the given type.
|
||||
func hasEvent(ctx sdk.Context, eventType string) bool {
|
||||
for _, ev := range ctx.EventManager().Events() {
|
||||
if ev.Type == eventType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// eventAttr returns the value of an attribute on the last event of the
|
||||
// given type, or "" if not found.
|
||||
func eventAttr(ctx sdk.Context, eventType, attrKey string) string {
|
||||
for _, ev := range ctx.EventManager().Events() {
|
||||
if ev.Type == eventType {
|
||||
for _, a := range ev.Attributes {
|
||||
if string(a.Key) == attrKey {
|
||||
return string(a.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- Full success lifecycle: Pending → Onboarded → Suspended → Revoked --------
|
||||
|
||||
// TestAnchorCredentialLifecycleFullSuccess asserts the full success
|
||||
// lifecycle: Issue (Pending) → Onboard (Onboarded) → Suspend (Suspended)
|
||||
// → Revoke (Revoked).
|
||||
func TestAnchorCredentialLifecycleFullSuccess(t *testing.T) {
|
||||
ctx, wk, hk, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
// Issue → Pending.
|
||||
if _, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "anchor-1", CredentialURI: "oy:cred:anchor-1/EU-MiCA",
|
||||
WatcherQuorumID: "quorum-6of9", Issuer: "issuer-1", Signer: "issuer-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("IssueAnchorCredential: %v", err)
|
||||
}
|
||||
c, ok := k.GetAnchorCredential(ctx, "anchor-1")
|
||||
if !ok {
|
||||
t.Fatal("anchor credential not found after issue")
|
||||
}
|
||||
if c.Status != ptypes.AnchorPending {
|
||||
t.Errorf("status = %q, want Pending", c.Status)
|
||||
}
|
||||
if c.CredentialURI != "oy:cred:anchor-1/EU-MiCA" {
|
||||
t.Errorf("credential-uri = %q", c.CredentialURI)
|
||||
}
|
||||
if !hasEvent(ctx, "partner.anchor_credential_issued") {
|
||||
t.Error("anchor_credential_issued event not emitted")
|
||||
}
|
||||
// Watcher quorum was consulted.
|
||||
if len(wk.calls) != 1 {
|
||||
t.Errorf("watcher calls = %d, want 1 (issuance authz)", len(wk.calls))
|
||||
}
|
||||
|
||||
// Onboard → Onboarded.
|
||||
if _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{
|
||||
AnchorID: "anchor-1", CustodyProviderID: "hub-custody-1", Signer: "issuer-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("OnboardAnchor: %v", err)
|
||||
}
|
||||
c, _ = k.GetAnchorCredential(ctx, "anchor-1")
|
||||
if c.Status != ptypes.AnchorOnboarded {
|
||||
t.Errorf("status = %q, want Onboarded", c.Status)
|
||||
}
|
||||
if c.CustodyProviderID != "hub-custody-1" {
|
||||
t.Errorf("custody-provider-id = %q, want hub-custody-1", c.CustodyProviderID)
|
||||
}
|
||||
if !hasEvent(ctx, "partner.anchor_onboarded") {
|
||||
t.Error("anchor_onboarded event not emitted")
|
||||
}
|
||||
// HubKeeper was consulted.
|
||||
if len(hk.calls) != 1 {
|
||||
t.Errorf("hub calls = %d, want 1 (custody-service-exists check)", len(hk.calls))
|
||||
}
|
||||
if hk.calls[0].custodyProviderID != "hub-custody-1" {
|
||||
t.Errorf("hub call custody-provider-id = %q, want hub-custody-1", hk.calls[0].custodyProviderID)
|
||||
}
|
||||
|
||||
// Suspend → Suspended.
|
||||
if _, err := srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{
|
||||
AnchorID: "anchor-1", Signer: "issuer-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("SuspendAnchorCredential: %v", err)
|
||||
}
|
||||
c, _ = k.GetAnchorCredential(ctx, "anchor-1")
|
||||
if c.Status != ptypes.AnchorSuspended {
|
||||
t.Errorf("status = %q, want Suspended", c.Status)
|
||||
}
|
||||
if !hasEvent(ctx, "partner.anchor_credential_suspended") {
|
||||
t.Error("anchor_credential_suspended event not emitted")
|
||||
}
|
||||
|
||||
// Revoke → Revoked (terminal).
|
||||
if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{
|
||||
AnchorID: "anchor-1", WatcherQuorumID: "quorum-6of9", Signer: "watcher-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("RevokeAnchorCredential: %v", err)
|
||||
}
|
||||
c, _ = k.GetAnchorCredential(ctx, "anchor-1")
|
||||
if c.Status != ptypes.AnchorRevoked {
|
||||
t.Errorf("status = %q, want Revoked", c.Status)
|
||||
}
|
||||
if !hasEvent(ctx, "partner.anchor_credential_revoked") {
|
||||
t.Error("anchor_credential_revoked event not emitted")
|
||||
}
|
||||
// Watcher quorum consulted again (revocation authz).
|
||||
if len(wk.calls) != 2 {
|
||||
t.Errorf("watcher calls = %d, want 2 (issuance + revocation authz)", len(wk.calls))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pending → Revoked (skip Onboard/Suspend) -------------------------------
|
||||
|
||||
// TestAnchorCredentialRevokeFromPending asserts a Pending credential can
|
||||
// be revoked directly (Pending → Revoked is a valid transition per
|
||||
// ValidAnchorTransition).
|
||||
func TestAnchorCredentialRevokeFromPending(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "anchor-pend", CredentialURI: "oy:cred:x",
|
||||
WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{
|
||||
AnchorID: "anchor-pend", WatcherQuorumID: "q", Signer: "w",
|
||||
}); err != nil {
|
||||
t.Fatalf("RevokeAnchorCredential from Pending: %v", err)
|
||||
}
|
||||
c, _ := k.GetAnchorCredential(ctx, "anchor-pend")
|
||||
if c.Status != ptypes.AnchorRevoked {
|
||||
t.Errorf("status = %q, want Revoked", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Suspended → Revoked ----------------------------------------------------
|
||||
|
||||
// TestAnchorCredentialRevokeFromSuspended asserts a Suspended credential
|
||||
// can be revoked (Suspended → Revoked is a valid transition).
|
||||
func TestAnchorCredentialRevokeFromSuspended(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "anchor-sus", CredentialURI: "oy:cred:x",
|
||||
WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{
|
||||
AnchorID: "anchor-sus", CustodyProviderID: "hc-1", Signer: "i",
|
||||
})
|
||||
srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{
|
||||
AnchorID: "anchor-sus", Signer: "i",
|
||||
})
|
||||
if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{
|
||||
AnchorID: "anchor-sus", WatcherQuorumID: "q", Signer: "w",
|
||||
}); err != nil {
|
||||
t.Fatalf("RevokeAnchorCredential from Suspended: %v", err)
|
||||
}
|
||||
c, _ := k.GetAnchorCredential(ctx, "anchor-sus")
|
||||
if c.Status != ptypes.AnchorRevoked {
|
||||
t.Errorf("status = %q, want Revoked", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Invalid transitions REJECTED -------------------------------------------
|
||||
|
||||
// TestOnboardRejectsNonPending asserts OnboardAnchor on a non-Pending
|
||||
// credential is REJECTED (the lifecycle gate). Covers Onboarded,
|
||||
// Suspended, and Revoked source statuses.
|
||||
func TestOnboardRejectsNonPending(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
// Onboarded source → reject (issue + onboard first).
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-ob", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-ob", CustodyProviderID: "hc", Signer: "i"})
|
||||
_, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-ob", CustodyProviderID: "hc", Signer: "i"})
|
||||
if err == nil {
|
||||
t.Error("OnboardAnchor on Onboarded credential should be rejected (lifecycle gate)")
|
||||
}
|
||||
|
||||
// Suspended source → reject.
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-sus", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-sus", CustodyProviderID: "hc", Signer: "i"})
|
||||
srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-sus", Signer: "i"})
|
||||
_, err = srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-sus", CustodyProviderID: "hc", Signer: "i"})
|
||||
if err == nil {
|
||||
t.Error("OnboardAnchor on Suspended credential should be rejected (lifecycle gate)")
|
||||
}
|
||||
|
||||
// Revoked source → reject.
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-rev", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev", WatcherQuorumID: "q", Signer: "w"})
|
||||
_, err = srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-rev", CustodyProviderID: "hc", Signer: "i"})
|
||||
if err == nil {
|
||||
t.Error("OnboardAnchor on Revoked credential should be rejected (lifecycle gate)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSuspendRejectsNonOnboarded asserts SuspendAnchorCredential on a
|
||||
// non-Onboarded credential is REJECTED. Covers Pending, Suspended, and
|
||||
// Revoked source statuses.
|
||||
func TestSuspendRejectsNonOnboarded(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
// Pending source → reject.
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-pend", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
_, err := srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-pend", Signer: "i"})
|
||||
if err == nil {
|
||||
t.Error("SuspendAnchorCredential on Pending credential should be rejected (lifecycle gate)")
|
||||
}
|
||||
|
||||
// Suspended source → reject (suspend an already-suspended).
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-sus2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-sus2", CustodyProviderID: "hc", Signer: "i"})
|
||||
srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-sus2", Signer: "i"})
|
||||
_, err = srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-sus2", Signer: "i"})
|
||||
if err == nil {
|
||||
t.Error("SuspendAnchorCredential on Suspended credential should be rejected (lifecycle gate)")
|
||||
}
|
||||
|
||||
// Revoked source → reject.
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-rev2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev2", WatcherQuorumID: "q", Signer: "w"})
|
||||
_, err = srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-rev2", Signer: "i"})
|
||||
if err == nil {
|
||||
t.Error("SuspendAnchorCredential on Revoked credential should be rejected (lifecycle gate)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRevokeRejectsAlreadyRevoked asserts a second Revoke on a Revoked
|
||||
// credential is REJECTED (idempotent reject — no double-effect).
|
||||
func TestRevokeRejectsAlreadyRevoked(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-rev3", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev3", WatcherQuorumID: "q", Signer: "w"})
|
||||
// Second revoke → idempotent reject.
|
||||
_, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev3", WatcherQuorumID: "q", Signer: "w"})
|
||||
if err == nil {
|
||||
t.Error("RevokeAnchorCredential on Revoked credential should be rejected (idempotent reject — no double-effect)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- HubKeeper shim wiring (P3→P4 hub dep edge) -----------------------------
|
||||
|
||||
// TestOnboardRejectsWhenCustodyServiceMissing asserts OnboardAnchor is
|
||||
// REJECTED when the HubKeeper shim reports the custody service does not
|
||||
// exist (the credential stays Pending). This is the P3→P4 hub dep edge
|
||||
// test (G-003 test exemption — the HubKeeper shim is a stub).
|
||||
func TestOnboardRejectsWhenCustodyServiceMissing(t *testing.T) {
|
||||
ctx, _, hk, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-hub", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
|
||||
// Configure the HubKeeper stub to report the custody service as
|
||||
// NON-existent for "missing-custody".
|
||||
hk.existsAll = false
|
||||
hk.exists = map[string]bool{"missing-custody": false}
|
||||
|
||||
_, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{
|
||||
AnchorID: "a-hub", CustodyProviderID: "missing-custody", Signer: "i",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("OnboardAnchor should be rejected when custody service does not exist (P3→P4 hub dep edge)")
|
||||
}
|
||||
// Credential stays Pending.
|
||||
c, _ := k.GetAnchorCredential(ctx, "a-hub")
|
||||
if c.Status != ptypes.AnchorPending {
|
||||
t.Errorf("status = %q, want Pending (onboarding rejected — credential stays Pending)", c.Status)
|
||||
}
|
||||
// Custody-provider-id NOT set.
|
||||
if c.CustodyProviderID != "" {
|
||||
t.Errorf("custody-provider-id = %q, want empty (onboarding rejected)", c.CustodyProviderID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnboardSucceedsWhenCustodyServiceExists asserts OnboardAnchor
|
||||
// SUCCEEDS when the HubKeeper shim reports the custody service exists
|
||||
// (the credential transitions to Onboarded).
|
||||
func TestOnboardSucceedsWhenCustodyServiceExists(t *testing.T) {
|
||||
ctx, _, hk, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-hub2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
|
||||
// Configure the HubKeeper stub to report the custody service as
|
||||
// existent for "good-custody".
|
||||
hk.existsAll = false
|
||||
hk.exists = map[string]bool{"good-custody": true}
|
||||
|
||||
if _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{
|
||||
AnchorID: "a-hub2", CustodyProviderID: "good-custody", Signer: "i",
|
||||
}); err != nil {
|
||||
t.Fatalf("OnboardAnchor should succeed when custody service exists: %v", err)
|
||||
}
|
||||
c, _ := k.GetAnchorCredential(ctx, "a-hub2")
|
||||
if c.Status != ptypes.AnchorOnboarded {
|
||||
t.Errorf("status = %q, want Onboarded", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnboardNilHubKeeperSkipsCheck asserts a nil HubKeeper shim skips
|
||||
// the custody-service-exists check (simtest wiring); the OnboardAnchor
|
||||
// transitions regardless. This documents the wiring contract for the
|
||||
// P3→P4 hub dep edge: P3 simtest may use a nil shim; P4 wires the real
|
||||
// hub keeper.
|
||||
func TestOnboardNilHubKeeperSkipsCheck(t *testing.T) {
|
||||
db := dbm.NewMemDB()
|
||||
cdc := newTestCodec()
|
||||
storeKey := storetypes.NewKVStoreKey(ptypes.StoreKey)
|
||||
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
|
||||
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
|
||||
cms.LoadLatestVersion()
|
||||
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
||||
// Nil HubKeeper shim.
|
||||
k := keeper.NewKeeper(cdc, storeKey, &stubWatcherKeeper{signedResult: true}, nil)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-nil", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
if _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{
|
||||
AnchorID: "a-nil", CustodyProviderID: "any-custody", Signer: "i",
|
||||
}); err != nil {
|
||||
t.Fatalf("OnboardAnchor with nil HubKeeper shim should succeed (check skipped): %v", err)
|
||||
}
|
||||
c, _ := k.GetAnchorCredential(ctx, "a-nil")
|
||||
if c.Status != ptypes.AnchorOnboarded {
|
||||
t.Errorf("status = %q, want Onboarded (nil shim skips check)", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Watcher quorum authz (REQ-004 6-of-9) ----------------------------------
|
||||
|
||||
// TestIssueRejectsWhenQuorumNotSigned asserts IssueAnchorCredential is
|
||||
// REJECTED when the WatcherKeeper stub reports the quorum NOT signed
|
||||
// (the credential is NOT created).
|
||||
func TestIssueRejectsWhenQuorumNotSigned(t *testing.T) {
|
||||
ctx, wk, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
wk.signedResult = false // quorum NOT signed
|
||||
|
||||
_, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-q", CredentialURI: "u", WatcherQuorumID: "q-6of9", Issuer: "i", Signer: "i",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("IssueAnchorCredential should be rejected when watcher quorum not signed (REQ-004 6-of-9)")
|
||||
}
|
||||
// Credential NOT created.
|
||||
if _, ok := k.GetAnchorCredential(ctx, "a-q"); ok {
|
||||
t.Error("anchor credential should NOT be created when issuance authz fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRevokeRejectsWhenQuorumNotSigned asserts RevokeAnchorCredential is
|
||||
// REJECTED when the WatcherKeeper stub reports the quorum NOT signed
|
||||
// (the credential stays in its pre-revoke status).
|
||||
func TestRevokeRejectsWhenQuorumNotSigned(t *testing.T) {
|
||||
ctx, wk, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-rq", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
// Flip watcher to NOT signed for the revoke.
|
||||
wk.signedResult = false
|
||||
_, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{
|
||||
AnchorID: "a-rq", WatcherQuorumID: "q", Signer: "w",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("RevokeAnchorCredential should be rejected when watcher quorum not signed (REQ-004 6-of-9)")
|
||||
}
|
||||
// Credential stays Pending (not revoked).
|
||||
c, _ := k.GetAnchorCredential(ctx, "a-rq")
|
||||
if c.Status != ptypes.AnchorPending {
|
||||
t.Errorf("status = %q, want Pending (revocation authz failed — credential stays)", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssueNilWatcherSkipsAuthz asserts a nil WatcherKeeper shim skips
|
||||
// the issuance authz (simtest wiring); the credential is created.
|
||||
func TestIssueNilWatcherSkipsAuthz(t *testing.T) {
|
||||
db := dbm.NewMemDB()
|
||||
cdc := newTestCodec()
|
||||
storeKey := storetypes.NewKVStoreKey(ptypes.StoreKey)
|
||||
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
|
||||
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
|
||||
cms.LoadLatestVersion()
|
||||
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
||||
// Nil WatcherKeeper shim.
|
||||
k := keeper.NewKeeper(cdc, storeKey, nil, &stubHubKeeper{existsAll: true})
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-nw", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
}); err != nil {
|
||||
t.Fatalf("IssueAnchorCredential with nil Watcher shim should succeed (authz skipped): %v", err)
|
||||
}
|
||||
c, ok := k.GetAnchorCredential(ctx, "a-nw")
|
||||
if !ok {
|
||||
t.Fatal("anchor credential should be created with nil Watcher shim (authz skipped)")
|
||||
}
|
||||
if c.Status != ptypes.AnchorPending {
|
||||
t.Errorf("status = %q, want Pending", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRevokeNilWatcherSkipsAuthz asserts a nil WatcherKeeper shim skips
|
||||
// the revocation authz (simtest wiring); the credential is revoked.
|
||||
func TestRevokeNilWatcherSkipsAuthz(t *testing.T) {
|
||||
db := dbm.NewMemDB()
|
||||
cdc := newTestCodec()
|
||||
storeKey := storetypes.NewKVStoreKey(ptypes.StoreKey)
|
||||
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
|
||||
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
|
||||
cms.LoadLatestVersion()
|
||||
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
||||
k := keeper.NewKeeper(cdc, storeKey, nil, &stubHubKeeper{existsAll: true})
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "a-nw2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{
|
||||
AnchorID: "a-nw2", WatcherQuorumID: "q", Signer: "w",
|
||||
}); err != nil {
|
||||
t.Fatalf("RevokeAnchorCredential with nil Watcher shim should succeed (authz skipped): %v", err)
|
||||
}
|
||||
c, _ := k.GetAnchorCredential(ctx, "a-nw2")
|
||||
if c.Status != ptypes.AnchorRevoked {
|
||||
t.Errorf("status = %q, want Revoked (nil shim skips authz)", c.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Idempotency + NotFound -------------------------------------------------
|
||||
|
||||
// TestIssueRejectsDuplicate asserts IssueAnchorCredential on an existing
|
||||
// anchor-id returns an error (idempotency).
|
||||
func TestIssueRejectsDuplicate(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "dup", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
_, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{
|
||||
AnchorID: "dup", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("IssueAnchorCredential should reject a duplicate anchor-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnboardNotFound asserts OnboardAnchor on a missing anchor-id
|
||||
// returns an error.
|
||||
func TestOnboardNotFound(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
_, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{
|
||||
AnchorID: "missing", CustodyProviderID: "hc", Signer: "i",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("OnboardAnchor on missing anchor-id should error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSuspendNotFound asserts SuspendAnchorCredential on a missing
|
||||
// anchor-id returns an error.
|
||||
func TestSuspendNotFound(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
_, err := srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{
|
||||
AnchorID: "missing", Signer: "i",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("SuspendAnchorCredential on missing anchor-id should error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRevokeNotFound asserts RevokeAnchorCredential on a missing
|
||||
// anchor-id returns an error.
|
||||
func TestRevokeNotFound(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
_, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{
|
||||
AnchorID: "missing", WatcherQuorumID: "q", Signer: "w",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("RevokeAnchorCredential on missing anchor-id should error")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ValidateBasic (Msg types) -----------------------------------------------
|
||||
|
||||
func TestMsgIssueAnchorCredentialValidateBasic(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg ptypes.MsgIssueAnchorCredential
|
||||
ok bool
|
||||
}{
|
||||
{"valid", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i"}, true},
|
||||
{"empty anchor-id", ptypes.MsgIssueAnchorCredential{AnchorID: "", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i"}, false},
|
||||
{"empty credential-uri", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "", WatcherQuorumID: "q", Issuer: "i", Signer: "i"}, false},
|
||||
{"empty watcher-quorum-id", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "u", WatcherQuorumID: "", Issuer: "i", Signer: "i"}, false},
|
||||
{"empty signer", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: ""}, 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 TestMsgOnboardAnchorValidateBasic(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg ptypes.MsgOnboardAnchor
|
||||
ok bool
|
||||
}{
|
||||
{"valid", ptypes.MsgOnboardAnchor{AnchorID: "a", CustodyProviderID: "hc", Signer: "i"}, true},
|
||||
{"empty anchor-id", ptypes.MsgOnboardAnchor{AnchorID: "", CustodyProviderID: "hc", Signer: "i"}, false},
|
||||
{"empty custody-provider-id", ptypes.MsgOnboardAnchor{AnchorID: "a", CustodyProviderID: "", Signer: "i"}, false},
|
||||
{"empty signer", ptypes.MsgOnboardAnchor{AnchorID: "a", CustodyProviderID: "hc", Signer: ""}, 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 TestMsgSuspendAnchorCredentialValidateBasic(t *testing.T) {
|
||||
if err := (&ptypes.MsgSuspendAnchorCredential{AnchorID: "a", Signer: "i"}).ValidateBasic(); err != nil {
|
||||
t.Errorf("valid: %v", err)
|
||||
}
|
||||
if err := (&ptypes.MsgSuspendAnchorCredential{AnchorID: "", Signer: "i"}).ValidateBasic(); err == nil {
|
||||
t.Error("empty anchor-id should fail")
|
||||
}
|
||||
if err := (&ptypes.MsgSuspendAnchorCredential{AnchorID: "a", Signer: ""}).ValidateBasic(); err == nil {
|
||||
t.Error("empty signer should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgRevokeAnchorCredentialValidateBasic(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg ptypes.MsgRevokeAnchorCredential
|
||||
ok bool
|
||||
}{
|
||||
{"valid", ptypes.MsgRevokeAnchorCredential{AnchorID: "a", WatcherQuorumID: "q", Signer: "i"}, true},
|
||||
{"empty anchor-id", ptypes.MsgRevokeAnchorCredential{AnchorID: "", WatcherQuorumID: "q", Signer: "i"}, false},
|
||||
{"empty watcher-quorum-id", ptypes.MsgRevokeAnchorCredential{AnchorID: "a", WatcherQuorumID: "", Signer: "i"}, false},
|
||||
{"empty signer", ptypes.MsgRevokeAnchorCredential{AnchorID: "a", WatcherQuorumID: "q", Signer: ""}, 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 TestPartnerMsgGetSigners(t *testing.T) {
|
||||
m := &ptypes.MsgIssueAnchorCredential{Signer: "holder-reach"}
|
||||
addrs := m.GetSigners()
|
||||
if len(addrs) != 1 || string(addrs[0]) != "holder-reach" {
|
||||
t.Errorf("GetSigners = %v, want [holder-reach]", addrs)
|
||||
}
|
||||
m2 := &ptypes.MsgOnboardAnchor{Signer: "h2"}
|
||||
if string(m2.GetSigners()[0]) != "h2" {
|
||||
t.Errorf("GetSigners = %v, want [h2]", m2.GetSigners())
|
||||
}
|
||||
m3 := &ptypes.MsgSuspendAnchorCredential{Signer: "h3"}
|
||||
if string(m3.GetSigners()[0]) != "h3" {
|
||||
t.Errorf("GetSigners = %v, want [h3]", m3.GetSigners())
|
||||
}
|
||||
m4 := &ptypes.MsgRevokeAnchorCredential{Signer: "h4"}
|
||||
if string(m4.GetSigners()[0]) != "h4" {
|
||||
t.Errorf("GetSigners = %v, want [h4]", m4.GetSigners())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Keeper store helpers ----------------------------------------------------
|
||||
|
||||
func TestSetGetAnchorCredential(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
c := ptypes.AnchorCredential{AnchorID: "a9", Status: ptypes.AnchorPending, CredentialURI: "u"}
|
||||
k.SetAnchorCredential(ctx, c)
|
||||
got, ok := k.GetAnchorCredential(ctx, "a9")
|
||||
if !ok {
|
||||
t.Fatal("GetAnchorCredential: not found")
|
||||
}
|
||||
if got.Status != ptypes.AnchorPending {
|
||||
t.Errorf("status = %q", got.Status)
|
||||
}
|
||||
if _, ok := k.GetAnchorCredential(ctx, "missing"); ok {
|
||||
t.Error("GetAnchorCredential should return false for missing id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllAnchorCredentials(t *testing.T) {
|
||||
ctx, _, _, k := newSimtestContext(t)
|
||||
k.SetAnchorCredential(ctx, ptypes.AnchorCredential{AnchorID: "a1", Status: ptypes.AnchorPending})
|
||||
k.SetAnchorCredential(ctx, ptypes.AnchorCredential{AnchorID: "a2", Status: ptypes.AnchorOnboarded})
|
||||
if len(k.AllAnchorCredentials(ctx)) != 2 {
|
||||
t.Errorf("expected 2 anchor credentials, got %d", len(k.AllAnchorCredentials(ctx)))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Anchor credential status enum helpers ----------------------------------
|
||||
|
||||
func TestAllAnchorCredentialStatusesCount(t *testing.T) {
|
||||
if len(ptypes.AllAnchorCredentialStatuses()) != ptypes.AnchorCredentialStatusCount {
|
||||
t.Errorf("AllAnchorCredentialStatuses len = %d, want %d", len(ptypes.AllAnchorCredentialStatuses()), ptypes.AnchorCredentialStatusCount)
|
||||
}
|
||||
if ptypes.AnchorCredentialStatusCount != 4 {
|
||||
t.Errorf("AnchorCredentialStatusCount = %d, want 4", ptypes.AnchorCredentialStatusCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllAnchorCredentialStatusesNames(t *testing.T) {
|
||||
want := []string{"Pending", "Onboarded", "Suspended", "Revoked"}
|
||||
all := ptypes.AllAnchorCredentialStatuses()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllAnchorCredentialStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTerminalAnchorStatus(t *testing.T) {
|
||||
if ptypes.IsTerminalAnchorStatus(ptypes.AnchorPending) {
|
||||
t.Error("Pending should not be terminal")
|
||||
}
|
||||
if ptypes.IsTerminalAnchorStatus(ptypes.AnchorOnboarded) {
|
||||
t.Error("Onboarded should not be terminal")
|
||||
}
|
||||
if ptypes.IsTerminalAnchorStatus(ptypes.AnchorSuspended) {
|
||||
t.Error("Suspended should not be terminal")
|
||||
}
|
||||
if !ptypes.IsTerminalAnchorStatus(ptypes.AnchorRevoked) {
|
||||
t.Error("Revoked should be terminal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidAnchorTransition(t *testing.T) {
|
||||
// Valid transitions.
|
||||
validCases := []struct {
|
||||
from, to ptypes.AnchorCredentialStatus
|
||||
}{
|
||||
{ptypes.AnchorPending, ptypes.AnchorOnboarded},
|
||||
{ptypes.AnchorPending, ptypes.AnchorRevoked},
|
||||
{ptypes.AnchorOnboarded, ptypes.AnchorSuspended},
|
||||
{ptypes.AnchorOnboarded, ptypes.AnchorRevoked},
|
||||
{ptypes.AnchorSuspended, ptypes.AnchorRevoked},
|
||||
}
|
||||
for _, c := range validCases {
|
||||
if !ptypes.ValidAnchorTransition(c.from, c.to) {
|
||||
t.Errorf("ValidAnchorTransition(%q, %q) = false, want true", c.from, c.to)
|
||||
}
|
||||
}
|
||||
// Invalid transitions.
|
||||
invalidCases := []struct {
|
||||
from, to ptypes.AnchorCredentialStatus
|
||||
}{
|
||||
{ptypes.AnchorOnboarded, ptypes.AnchorPending}, // no backward to Pending
|
||||
{ptypes.AnchorSuspended, ptypes.AnchorOnboarded}, // no Suspended → Onboarded (v0.5 scope)
|
||||
{ptypes.AnchorSuspended, ptypes.AnchorPending}, // no backward to Pending
|
||||
{ptypes.AnchorRevoked, ptypes.AnchorPending}, // terminal — no out
|
||||
{ptypes.AnchorRevoked, ptypes.AnchorOnboarded}, // terminal — no out
|
||||
{ptypes.AnchorRevoked, ptypes.AnchorSuspended}, // terminal — no out
|
||||
{ptypes.AnchorPending, ptypes.AnchorSuspended}, // must Onboard before Suspend
|
||||
}
|
||||
for _, c := range invalidCases {
|
||||
if ptypes.ValidAnchorTransition(c.from, c.to) {
|
||||
t.Errorf("ValidAnchorTransition(%q, %q) = true, want false", c.from, c.to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- G-003 import-invariant (test exemption documentation) -------------------
|
||||
|
||||
// TestG003NoWatcherOrHubTypesImport asserts the partner production files
|
||||
// do NOT import x/watcher/types or x/hub/types by struct (G-003 — the
|
||||
// WatcherKeeper and HubKeeper interfaces are the only coupling; no
|
||||
// struct import). This is a tested invariant. The test scans the import
|
||||
// statements of all non-test .go files under x/partner/. (This is a
|
||||
// simtest-grade scan; the full project-wide G-003 invariant is enforced
|
||||
// by the lexicon_meta_test.go / G-003 meta-test in v0.2.)
|
||||
func TestG003NoWatcherOrHubTypesImport(t *testing.T) {
|
||||
// The stub WatcherKeeper and HubKeeper in this simtest file satisfy
|
||||
// the interfaces; the production files (keeper.go, msg_server.go,
|
||||
// module.go, types/*.go) must NOT import x/watcher/types or
|
||||
// x/hub/types. This is verified at the project-wide G-003 meta-test
|
||||
// level. Here we do a lightweight assertion: the stubs use by-string
|
||||
// reach-ids and quorum-ids (not watcher/hub structs), confirming the
|
||||
// interface contract is by-ID-string.
|
||||
wk := &stubWatcherKeeper{signedResult: true}
|
||||
if !wk.IsQuorumSigned("quorum-6of9", []byte("payload")) {
|
||||
t.Error("stub IsQuorumSigned by-ID-string should return true")
|
||||
}
|
||||
if len(wk.calls) != 1 {
|
||||
t.Errorf("expected 1 watcher call recorded, got %d", len(wk.calls))
|
||||
}
|
||||
hk := &stubHubKeeper{existsAll: true}
|
||||
if !hk.CustodyServiceExists("hub-custody-1") {
|
||||
t.Error("stub CustodyServiceExists by-ID-string should return true")
|
||||
}
|
||||
if len(hk.calls) != 1 {
|
||||
t.Errorf("expected 1 hub call recorded, got %d", len(hk.calls))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package partner
|
||||
|
||||
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/partner/keeper"
|
||||
"github.com/oy/openyield/x/partner/types"
|
||||
)
|
||||
|
||||
// module.go holds the partner module's AppModule + RegisterServices
|
||||
// (P3-02-01, REQ-035).
|
||||
//
|
||||
// The AppModule wraps the Anchor-credential 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.
|
||||
//
|
||||
// The WatcherKeeper and HubKeeper expected-keeper shims are injected at
|
||||
// construction (nil-able for partial tests). The HubKeeper shim is the
|
||||
// P3→P4 hub dep edge: P3 wires a stub in simtest; P4 wires the real hub
|
||||
// keeper.
|
||||
|
||||
// ConsensusVersion is the partner module's consensus version (AppModule).
|
||||
const ConsensusVersion = 1
|
||||
|
||||
// AppModule is the partner application module (simtest-grade — D-054).
|
||||
type AppModule struct {
|
||||
keeper keeper.Keeper
|
||||
}
|
||||
|
||||
// NewAppModule constructs a new partner AppModule. The WatcherKeeper and
|
||||
// HubKeeper expected-keeper shims are injected (nil-able for partial
|
||||
// tests).
|
||||
func NewAppModule(cdc codec.Codec, storeKey storetypes.StoreKey, wk types.WatcherKeeper, hk types.HubKeeper) AppModule {
|
||||
k := keeper.NewKeeper(cdc, storeKey, wk, hk)
|
||||
return AppModule{keeper: k}
|
||||
}
|
||||
|
||||
// RegisterServices registers the partner 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 partner 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 partner module's
|
||||
// Anchor credentials. (The v0.3 Partner registry genesis is handled by
|
||||
// the v0.3 in-memory stub; this AppModule handles the v0.5 Anchor
|
||||
// credential store.)
|
||||
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) {
|
||||
var gs types.GenesisState
|
||||
cdc.MustUnmarshalJSON(data, &gs)
|
||||
// The v0.5 Anchor credential store does not yet have a genesis slice
|
||||
// (the Anchor credentials are created at runtime via
|
||||
// MsgIssueAnchorCredential). InitGenesis is a no-op for the Anchor
|
||||
// credential store; the v0.3 Partner registry genesis is handled
|
||||
// separately by the v0.3 in-memory stub. This is documented for the
|
||||
// simtest-grade AppModule (D-054): genesis-init of runtime-promoted
|
||||
// stores is deferred to the live chain (v0.6+).
|
||||
_ = gs
|
||||
}
|
||||
|
||||
// ExportGenesis returns the exported genesis state as raw bytes.
|
||||
// (Simtest-grade: returns an empty genesis for the Anchor credential
|
||||
// store; the live chain export is deferred to v0.6+.)
|
||||
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
|
||||
gs := types.DefaultGenesisState()
|
||||
return cdc.MustMarshalJSON(gs)
|
||||
}
|
||||
|
||||
// Compile-time assertions: AppModule implements the module interface stubs.
|
||||
var _ module.HasName = AppModule{}
|
||||
var _ module.HasConsensusVersion = AppModule{}
|
||||
@@ -0,0 +1,107 @@
|
||||
package types
|
||||
|
||||
// anchor_credential.go holds the v0.5 runtime Anchor credential lifecycle
|
||||
// types (P3-01-01, REQ-035). v0.3 typed the AnchorCredential struct
|
||||
// (types.go); v0.5 promotes it to runtime by adding the lifecycle Status
|
||||
// field + the AnchorCredentialStatus enum (the lifecycle Pending →
|
||||
// Onboarded → Suspended → Revoked per RESEARCH v0.5 / REQ-035).
|
||||
//
|
||||
// The four Msg* types (MsgIssueAnchorCredential, MsgOnboardAnchor,
|
||||
// MsgSuspendAnchorCredential, MsgRevokeAnchorCredential) live in
|
||||
// msg_anchor.go (sdk.Msg impls). The expected-keeper interfaces
|
||||
// (WatcherKeeper, HubKeeper) live in expected_keepers.go (G-003 shims).
|
||||
//
|
||||
// Lifecycle (REQ-035, vision §13):
|
||||
//
|
||||
// IssueAnchorCredential → Pending (Watcher-authorized issuance)
|
||||
// OnboardAnchor → Pending → Onboarded (asserts custody-provider-id
|
||||
// via the HubKeeper shim — P4 wires the real hub
|
||||
// keeper; P3 uses a stub in simtest per the G-003
|
||||
// test exemption)
|
||||
// SuspendAnchorCredential → Onboarded → Suspended
|
||||
// RevokeAnchorCredential → any → Revoked (Watcher 6-of-9 quorum authz per
|
||||
// REQ-004)
|
||||
//
|
||||
// Invalid transitions are REJECTED by the handler (the simtest covers each
|
||||
// invalid transition). Revoked is terminal (no transition out of Revoked).
|
||||
// The lexicon-clean holder identifier is "reach-id" (NOT a banned financial
|
||||
// term; use Holder/Reach).
|
||||
|
||||
// AnchorCredentialStatus enumerates the Anchor credential lifecycle states
|
||||
// (REQ-035). The lifecycle is Pending → Onboarded → Suspended → Revoked
|
||||
// (Suspended is a temporary halt; Revoked is terminal). "Onboarded" is the
|
||||
// vision-§13 lexicon-clean term for an institutional Anchor that has
|
||||
// completed onboarding (NOT a banned term).
|
||||
type AnchorCredentialStatus string
|
||||
|
||||
const (
|
||||
// AnchorPending is the initial state after IssueAnchorCredential
|
||||
// (Watcher-authorized issuance). The Anchor is registered but has
|
||||
// not yet completed onboarding.
|
||||
AnchorPending AnchorCredentialStatus = "Pending"
|
||||
// AnchorOnboarded is the state after OnboardAnchor (the custody-
|
||||
// provider-id has been validated via the HubKeeper shim). The Anchor
|
||||
// is live and may custody assets.
|
||||
AnchorOnboarded AnchorCredentialStatus = "Onboarded"
|
||||
// AnchorSuspended is the temporary-halt state (SuspendAnchorCredential
|
||||
// transitions Onboarded → Suspended). A Suspended Anchor may not
|
||||
// custody new assets; it may be re-onboarded (Suspended → Onboarded)
|
||||
// by a fresh OnboardAnchor in a future handler revision (v0.5 simtest
|
||||
// scope: the handler does NOT implement Suspended → Onboarded; only
|
||||
// the forward transitions are wired).
|
||||
AnchorSuspended AnchorCredentialStatus = "Suspended"
|
||||
// AnchorRevoked is the terminal state (RevokeAnchorCredential, Watcher
|
||||
// 6-of-9 quorum authz per REQ-004). A Revoked Anchor may not transition
|
||||
// to any other state.
|
||||
AnchorRevoked AnchorCredentialStatus = "Revoked"
|
||||
)
|
||||
|
||||
// AnchorCredentialStatusCount is the locked count of AnchorCredentialStatus
|
||||
// enum values (REQ-035). A regression firewall: adding/removing/renaming a
|
||||
// status breaks this const's test.
|
||||
const AnchorCredentialStatusCount = 4
|
||||
|
||||
// AllAnchorCredentialStatuses returns all four AnchorCredentialStatus values
|
||||
// in lifecycle order (Pending, Onboarded, Suspended, Revoked). Locked-const
|
||||
// test asserts exactly 4 entries with these names (REQ-035).
|
||||
func AllAnchorCredentialStatuses() []AnchorCredentialStatus {
|
||||
return []AnchorCredentialStatus{
|
||||
AnchorPending,
|
||||
AnchorOnboarded,
|
||||
AnchorSuspended,
|
||||
AnchorRevoked,
|
||||
}
|
||||
}
|
||||
|
||||
// IsTerminalAnchorStatus reports whether the Anchor credential status is
|
||||
// terminal (no further transitions permitted). Revoked is terminal.
|
||||
// Pending/Onboarded/Suspended are non-terminal.
|
||||
func IsTerminalAnchorStatus(s AnchorCredentialStatus) bool {
|
||||
return s == AnchorRevoked
|
||||
}
|
||||
|
||||
// ValidAnchorTransition reports whether the from → to transition is
|
||||
// permitted by the REQ-035 lifecycle:
|
||||
// - Pending → Onboarded (OnboardAnchor)
|
||||
// - Onboarded → Suspended (SuspendAnchorCredential)
|
||||
// - Onboarded → Revoked (RevokeAnchorCredential)
|
||||
// - Suspended → Revoked (RevokeAnchorCredential)
|
||||
// - Pending → Revoked (RevokeAnchorCredential — a Pending Anchor may be
|
||||
// revoked before onboarding completes)
|
||||
//
|
||||
// All other transitions are REJECTED. Revoked is terminal (no transition
|
||||
// out). The handler consults this helper before mutating state.
|
||||
func ValidAnchorTransition(from, to AnchorCredentialStatus) bool {
|
||||
switch from {
|
||||
case AnchorPending:
|
||||
return to == AnchorOnboarded || to == AnchorRevoked
|
||||
case AnchorOnboarded:
|
||||
return to == AnchorSuspended || to == AnchorRevoked
|
||||
case AnchorSuspended:
|
||||
return to == AnchorRevoked
|
||||
case AnchorRevoked:
|
||||
return false // terminal
|
||||
default:
|
||||
return false // unknown source status
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package types
|
||||
|
||||
// expected_keepers.go holds the Go INTERFACES for the cross-module keepers
|
||||
// x/partner depends on (G-003 firewall — ibc-go expected-keepers convention).
|
||||
//
|
||||
// The Anchor credential lifecycle (REQ-035) depends on TWO cross-module
|
||||
// keepers:
|
||||
//
|
||||
// 1. x/watcher (WatcherKeeper) — the 6-of-9 Watcher quorum (REQ-004)
|
||||
// authorizes Anchor credential ISSUANCE (IssueAnchorCredential) and
|
||||
// REVOCATION (RevokeAnchorCredential). The handler consults the
|
||||
// watcher quorum by ID-string; the interface method reports whether
|
||||
// the quorum reached its threshold on the payload.
|
||||
//
|
||||
// 2. x/hub (HubKeeper) — the custody-provider-id validity check on
|
||||
// OnboardAnchor (Pending → Onboarded). The handler asserts the
|
||||
// custody-provider-id on the AnchorCredential references a live Hub
|
||||
// custody service BEFORE transitioning to Onboarded. This is the
|
||||
// P3→P4 hub dep edge (G-003 / ARCHITECTURE.md v0.5): the hub keeper
|
||||
// INTERFACE exists in P3 (defined HERE); the real hub keeper impl
|
||||
// is wired in P4. In P3 simtest, the HubKeeper shim is wired to a
|
||||
// stub (G-003 test exemption) — the simtest validates the wiring
|
||||
// contract without a real hub keeper.
|
||||
//
|
||||
// Both dependencies are expressed as INTERFACES defined HERE (in
|
||||
// x/partner/types), NOT as struct imports of x/watcher/types or
|
||||
// x/hub/types. The concrete keepers satisfy these interfaces
|
||||
// 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/partner/keeper and x/hub/keeper
|
||||
// (or x/watcher/keeper) to wire the expected-keeper shims in a test
|
||||
// setup.
|
||||
|
||||
// WatcherKeeper is the expected-keeper interface for x/watcher (G-003).
|
||||
// The partner handler calls it for:
|
||||
// - IssueAnchorCredential: a Watcher 6-of-9 quorum must authorize the
|
||||
// issuance (vision §7, REQ-004). The handler consults the watcher
|
||||
// quorum by ID-string; the interface method reports whether the
|
||||
// quorum reached its threshold on the issuance payload.
|
||||
// - RevokeAnchorCredential: a Watcher 6-of-9 quorum must authorize the
|
||||
// revocation (the same REQ-004 quorum, applied to revocation authz).
|
||||
//
|
||||
// No struct import of x/watcher/types — the interface is the by-ID-string
|
||||
// boundary (G-003). The WatcherQuorumID is an opaque string (the quorum
|
||||
// identifier, by-ID-string ref to x/watcher).
|
||||
type WatcherKeeper interface {
|
||||
// IsQuorumSigned reports whether the named quorum (by-ID-string)
|
||||
// reached its threshold signature count on the payload. Used for
|
||||
// both IssueAnchorCredential (issuance authz) and
|
||||
// RevokeAnchorCredential (revocation authz). Returns true if the
|
||||
// quorum threshold is met (e.g., 6-of-9 per REQ-004); false otherwise.
|
||||
IsQuorumSigned(quorumID string, payload []byte) bool
|
||||
}
|
||||
|
||||
// HubKeeper is the expected-keeper interface for x/hub (G-003). The
|
||||
// partner handler calls it for:
|
||||
// - OnboardAnchor: the handler asserts the custody-provider-id on the
|
||||
// AnchorCredential references a LIVE Hub custody service BEFORE
|
||||
// transitioning the credential to Onboarded. This is the P3→P4 hub
|
||||
// dep edge (G-003 / ARCHITECTURE.md v0.5): the INTERFACE exists in
|
||||
// P3 (defined here); the real impl is wired in P4. In P3 simtest,
|
||||
// the HubKeeper shim is wired to a stub (G-003 test exemption).
|
||||
//
|
||||
// No struct import of x/hub/types — the interface is the by-ID-string
|
||||
// boundary (G-003). The custodyProviderID is an opaque string (the
|
||||
// custody service identifier, by-ID-string ref to x/hub CustodyService).
|
||||
type HubKeeper interface {
|
||||
// CustodyServiceExists reports whether the named custody service
|
||||
// (by-ID-string) exists and is live (i.e., the custody-provider-id
|
||||
// on the AnchorCredential references a real Hub custody service).
|
||||
// The OnboardAnchor handler consults this BEFORE transitioning the
|
||||
// credential to Onboarded; a non-existent custody service REJECTS
|
||||
// the onboarding (the credential stays Pending).
|
||||
CustodyServiceExists(custodyProviderID string) bool
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// msg_anchor.go holds the partner module's Anchor-credential Msg* types
|
||||
// implementing sdk.Msg (P3-01-01, REQ-035; 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 four Anchor Msg types drive the credential lifecycle (REQ-035):
|
||||
// - MsgIssueAnchorCredential: issue a credential (Watcher-authorized),
|
||||
// status=Pending.
|
||||
// - MsgOnboardAnchor: Pending → Onboarded (asserts custody-provider-id
|
||||
// via the HubKeeper shim).
|
||||
// - MsgSuspendAnchorCredential: Onboarded → Suspended.
|
||||
// - MsgRevokeAnchorCredential: any → Revoked (Watcher 6-of-9 quorum
|
||||
// authz per REQ-004).
|
||||
//
|
||||
// All cross-module refs are by-ID-string (G-003): anchor-id is this
|
||||
// credential's ID (references an Anchor-tier Partner by ID-string);
|
||||
// custody-provider-id references an x/hub custody service by ID-string;
|
||||
// watcher-quorum-id references an x/watcher quorum by ID-string.
|
||||
// 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).
|
||||
|
||||
// --- MsgIssueAnchorCredential ----------------------------------------------
|
||||
|
||||
// MsgIssueAnchorCredential issues an Anchor credential (status=Pending).
|
||||
// The handler enforces Watcher 6-of-9 quorum authz (REQ-004) on the
|
||||
// issuance payload via the WatcherKeeper shim. ValidateBasic is
|
||||
// stateless: non-empty anchor-id, non-empty credential-uri, non-empty
|
||||
// watcher-quorum-id, non-empty signer.
|
||||
type MsgIssueAnchorCredential struct {
|
||||
AnchorID string `json:"anchor_id" yaml:"anchor_id"`
|
||||
CredentialURI string `json:"credential_uri" yaml:"credential_uri"`
|
||||
WatcherQuorumID string `json:"watcher_quorum_id" yaml:"watcher_quorum_id"`
|
||||
Issuer string `json:"issuer" yaml:"issuer"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message (sdk.Msg = proto.Message).
|
||||
func (m *MsgIssueAnchorCredential) Reset() { *m = MsgIssueAnchorCredential{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgIssueAnchorCredential) String() string {
|
||||
return fmt.Sprintf("MsgIssueAnchorCredential{AnchorID:%s CredentialURI:%s WatcherQuorumID:%s Issuer:%s Signer:%s}",
|
||||
m.AnchorID, m.CredentialURI, m.WatcherQuorumID, m.Issuer, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgIssueAnchorCredential) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty anchor-id,
|
||||
// non-empty credential-uri, non-empty watcher-quorum-id, non-empty
|
||||
// signer.
|
||||
func (m *MsgIssueAnchorCredential) ValidateBasic() error {
|
||||
if m.AnchorID == "" {
|
||||
return fmt.Errorf("partner: empty anchor-id")
|
||||
}
|
||||
if m.CredentialURI == "" {
|
||||
return fmt.Errorf("partner: empty credential-uri")
|
||||
}
|
||||
if m.WatcherQuorumID == "" {
|
||||
return fmt.Errorf("partner: empty watcher-quorum-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("partner: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgIssueAnchorCredential) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgOnboardAnchor --------------------------------------------------------
|
||||
|
||||
// MsgOnboardAnchor transitions an Anchor credential Pending → Onboarded.
|
||||
// The handler asserts the custody-provider-id (set on the credential at
|
||||
// issue time or supplied here) references a LIVE Hub custody service via
|
||||
// the HubKeeper shim (the P3→P4 hub dep edge; P3 simtest uses a stub).
|
||||
// ValidateBasic is stateless: non-empty anchor-id, non-empty
|
||||
// custody-provider-id, non-empty signer.
|
||||
type MsgOnboardAnchor struct {
|
||||
AnchorID string `json:"anchor_id" yaml:"anchor_id"`
|
||||
CustodyProviderID string `json:"custody_provider_id" yaml:"custody_provider_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgOnboardAnchor) Reset() { *m = MsgOnboardAnchor{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgOnboardAnchor) String() string {
|
||||
return fmt.Sprintf("MsgOnboardAnchor{AnchorID:%s CustodyProviderID:%s Signer:%s}",
|
||||
m.AnchorID, m.CustodyProviderID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgOnboardAnchor) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty anchor-id,
|
||||
// non-empty custody-provider-id, non-empty signer. The handler enforces
|
||||
// the stateful source-status check (must be Pending) and the
|
||||
// custody-service-exists check via the HubKeeper shim.
|
||||
func (m *MsgOnboardAnchor) ValidateBasic() error {
|
||||
if m.AnchorID == "" {
|
||||
return fmt.Errorf("partner: empty anchor-id")
|
||||
}
|
||||
if m.CustodyProviderID == "" {
|
||||
return fmt.Errorf("partner: empty custody-provider-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("partner: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgOnboardAnchor) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgSuspendAnchorCredential ---------------------------------------------
|
||||
|
||||
// MsgSuspendAnchorCredential transitions an Anchor credential
|
||||
// Onboarded → Suspended. ValidateBasic is stateless: non-empty
|
||||
// anchor-id, non-empty signer.
|
||||
type MsgSuspendAnchorCredential struct {
|
||||
AnchorID string `json:"anchor_id" yaml:"anchor_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgSuspendAnchorCredential) Reset() { *m = MsgSuspendAnchorCredential{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgSuspendAnchorCredential) String() string {
|
||||
return fmt.Sprintf("MsgSuspendAnchorCredential{AnchorID:%s Signer:%s}",
|
||||
m.AnchorID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgSuspendAnchorCredential) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty anchor-id,
|
||||
// non-empty signer. The handler enforces the stateful source-status
|
||||
// check (must be Onboarded).
|
||||
func (m *MsgSuspendAnchorCredential) ValidateBasic() error {
|
||||
if m.AnchorID == "" {
|
||||
return fmt.Errorf("partner: empty anchor-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("partner: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgSuspendAnchorCredential) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgRevokeAnchorCredential ----------------------------------------------
|
||||
|
||||
// MsgRevokeAnchorCredential transitions an Anchor credential to Revoked
|
||||
// (terminal). The handler enforces Watcher 6-of-9 quorum authz (REQ-004)
|
||||
// on the revocation payload via the WatcherKeeper shim. ValidateBasic is
|
||||
// stateless: non-empty anchor-id, non-empty watcher-quorum-id,
|
||||
// non-empty signer.
|
||||
type MsgRevokeAnchorCredential struct {
|
||||
AnchorID string `json:"anchor_id" yaml:"anchor_id"`
|
||||
WatcherQuorumID string `json:"watcher_quorum_id" yaml:"watcher_quorum_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgRevokeAnchorCredential) Reset() { *m = MsgRevokeAnchorCredential{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgRevokeAnchorCredential) String() string {
|
||||
return fmt.Sprintf("MsgRevokeAnchorCredential{AnchorID:%s WatcherQuorumID:%s Signer:%s}",
|
||||
m.AnchorID, m.WatcherQuorumID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgRevokeAnchorCredential) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty anchor-id,
|
||||
// non-empty watcher-quorum-id, non-empty signer. The handler enforces
|
||||
// the stateful Watcher quorum authz + the source-status check (must not
|
||||
// already be Revoked — idempotent reject, NOT double-effect).
|
||||
func (m *MsgRevokeAnchorCredential) ValidateBasic() error {
|
||||
if m.AnchorID == "" {
|
||||
return fmt.Errorf("partner: empty anchor-id")
|
||||
}
|
||||
if m.WatcherQuorumID == "" {
|
||||
return fmt.Errorf("partner: empty watcher-quorum-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("partner: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgRevokeAnchorCredential) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgServer interface + Response types -----------------------------------
|
||||
|
||||
// MsgServer is the partner 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 {
|
||||
IssueAnchorCredential(ctx interface{}, msg *MsgIssueAnchorCredential) (*MsgIssueAnchorCredentialResponse, error)
|
||||
OnboardAnchor(ctx interface{}, msg *MsgOnboardAnchor) (*MsgOnboardAnchorResponse, error)
|
||||
SuspendAnchorCredential(ctx interface{}, msg *MsgSuspendAnchorCredential) (*MsgSuspendAnchorCredentialResponse, error)
|
||||
RevokeAnchorCredential(ctx interface{}, msg *MsgRevokeAnchorCredential) (*MsgRevokeAnchorCredentialResponse, error)
|
||||
}
|
||||
|
||||
// Response types (hand-rolled equivalents of the protobuf-generated
|
||||
// response wrappers; empty bodies — the response is the state mutation +
|
||||
// event).
|
||||
|
||||
// MsgIssueAnchorCredentialResponse is the response to
|
||||
// MsgIssueAnchorCredential.
|
||||
type MsgIssueAnchorCredentialResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgIssueAnchorCredentialResponse) Reset() { *m = MsgIssueAnchorCredentialResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgIssueAnchorCredentialResponse) String() string {
|
||||
return "MsgIssueAnchorCredentialResponse{}"
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgIssueAnchorCredentialResponse) ProtoMessage() {}
|
||||
|
||||
// MsgOnboardAnchorResponse is the response to MsgOnboardAnchor.
|
||||
type MsgOnboardAnchorResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgOnboardAnchorResponse) Reset() { *m = MsgOnboardAnchorResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgOnboardAnchorResponse) String() string { return "MsgOnboardAnchorResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgOnboardAnchorResponse) ProtoMessage() {}
|
||||
|
||||
// MsgSuspendAnchorCredentialResponse is the response to
|
||||
// MsgSuspendAnchorCredential.
|
||||
type MsgSuspendAnchorCredentialResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgSuspendAnchorCredentialResponse) Reset() {
|
||||
*m = MsgSuspendAnchorCredentialResponse{}
|
||||
}
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgSuspendAnchorCredentialResponse) String() string {
|
||||
return "MsgSuspendAnchorCredentialResponse{}"
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgSuspendAnchorCredentialResponse) ProtoMessage() {}
|
||||
|
||||
// MsgRevokeAnchorCredentialResponse is the response to
|
||||
// MsgRevokeAnchorCredential.
|
||||
type MsgRevokeAnchorCredentialResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgRevokeAnchorCredentialResponse) Reset() { *m = MsgRevokeAnchorCredentialResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgRevokeAnchorCredentialResponse) String() string {
|
||||
return "MsgRevokeAnchorCredentialResponse{}"
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgRevokeAnchorCredentialResponse) ProtoMessage() {}
|
||||
@@ -183,6 +183,16 @@ type AnchorCredential struct {
|
||||
CustodyProviderID string `json:"custody_provider_id" yaml:"custody_provider_id"`
|
||||
CredentialURI string `json:"credential_uri" yaml:"credential_uri"`
|
||||
AttestationCount uint32 `json:"attestation_count" yaml:"attestation_count"`
|
||||
// Status is the Anchor credential lifecycle state (REQ-035, v0.5 runtime
|
||||
// promotion). v0.3 typed the AnchorCredential struct without a status
|
||||
// field (the skeleton had no lifecycle); v0.5 promotes it to runtime
|
||||
// by adding the Status field — additive (zero value "" = Pending
|
||||
// semantically, but the handler always sets it explicitly at issue
|
||||
// time). The existing v0.3 tests construct AnchorCredential with named
|
||||
// fields and do not assert the absence of Status, so the additive
|
||||
// field does not regress them (feature purity gate: additive field,
|
||||
// not a locked-const amendment).
|
||||
Status AnchorCredentialStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// NewAnchorCredential constructs an AnchorCredential for an Anchor-tier
|
||||
@@ -193,12 +203,18 @@ type AnchorCredential struct {
|
||||
// attestation-count is set to 0 (no attestations in the skeleton). The
|
||||
// caller supplies the anchor-id (the Anchor Partner's ID) and the opaque
|
||||
// credential-uri.
|
||||
//
|
||||
// v0.5 runtime promotion (REQ-035): the Status field is set to
|
||||
// AnchorPending (the initial lifecycle state). v0.3 tests construct the
|
||||
// struct via named fields and assert only the four original fields, so
|
||||
// the additive Status=Pending default does not regress them.
|
||||
func NewAnchorCredential(anchorID, credentialURI string) AnchorCredential {
|
||||
return AnchorCredential{
|
||||
AnchorID: anchorID,
|
||||
CustodyProviderID: "", // empty — hub not live until P5/v0.4 (A-304)
|
||||
CredentialURI: credentialURI,
|
||||
AttestationCount: 0, // no attestations in the skeleton
|
||||
Status: AnchorPending,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +230,19 @@ type GenesisState struct {
|
||||
Partners []Partner `json:"partners" yaml:"partners"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message (codec.JSONCodec.MustMarshalJSON /
|
||||
// MustUnmarshalJSON require proto.Message; the GenesisState is the JSON
|
||||
// shape used by the v0.5 AppModule InitGenesis/ExportGenesis).
|
||||
func (m *GenesisState) Reset() { *m = GenesisState{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *GenesisState) String() string {
|
||||
return fmt.Sprintf("GenesisState{Partners:%d}", len(m.Partners))
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*GenesisState) ProtoMessage() {}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
|
||||
Reference in New Issue
Block a user