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---
274 lines
11 KiB
Go
274 lines
11 KiB
Go
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
|
|
}
|