Files
openyield/x/hub/keeper/custody_state.go
T
cloudinit-bot 3c52aa1bb2
docs-build / go test ./... (lexicon firewall + all x/* tests) (push) Has been cancelled
docs-build / mkdocs build (docs site artifact) (push) Has been cancelled
Merge phase/04 into milestone/v0.5-bearers-runtime (P4 complete → v0.4.4)
---ci---
project: oy
phase: 4
milestone: v0.5
status: complete
requirements:
  covered: [REQ-036]
  partial: []
---/ci---
2026-08-18 00:49:16 +00:00

169 lines
6.9 KiB
Go

package keeper
// custody_state.go holds the custody asset records (assetID → custody entry
// + sig ref + key version) for the x/hub custody runtime (P4-02-01,
// REQ-036). data-engineer territory (P4 phase-specific — removed after P4
// per PERSONAS.md).
//
// D-054: in-memory test store ONLY — the SDK in-memory store (dbm NewMemDB)
// is the substrate; NO real database, NO migration (simtest grade). The
// custody state is the closest thing to a data store in v0.5; there is NO
// real database (the SDK store is the substrate). data-engineer's role is
// narrow: ensure the custody state shape (assetID → custody entry + sig ref
// + key version) is consistent with the CustodyKeyring interface and
// supports rotation (D-058).
//
// State shape (consistent with CustodyKeyring interface, D-058):
// - assetID → CustodyEntry (assetID, holder-reach-id, partner-id, sig-ref,
// key-version, custody-status)
// - sig-ref is the opaque reference to the signature produced by
// CustodyKeyring.Sign on the custody-receive payload (stored so a
// later CustodyReleaseAsset can verify the release is authorized by
// the same key version that received the asset — rotation safety).
// - key-version is the CustodyKeyring active key version at the time of
// custody-receive (recorded so a post-rotation release can detect the
// key has rotated — the handler may require re-attestation).
//
// The custody state is store-backed (wraps an sdk.KVStore via a storeKey on
// the Keeper). The custody entry is JSON-marshaled (same pattern as
// x/partner/keeper/keeper.go AnchorCredential store — simtest-grade, no
// protobuf codegen).
//
// Lexicon note (REQ-012, A-542): "custody", "asset", "holder", "reach-id",
// "sig-ref", "key-version", "receive", "release" are all lexicon-clean.
// The inbound/outbound custody names follow A-542 (the banned storage
// terms are NOT used; CustodyReceiveAsset / CustodyReleaseAsset are the
// safe vision vocabulary). "holder"/"reach-id" (NOT the banned holder
// lexicon term).
import (
"encoding/json"
"fmt"
storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// CustodyEntry is the per-assetID custody record. Stored in the hub
// custody store keyed by assetID. The sig-ref + key-version support
// rotation safety (D-058): a post-rotation release can detect the key
// has rotated and require re-attestation.
type CustodyEntry struct {
// AssetID is the opaque asset identifier (the custody key is assetID).
// Opaque so the hub does not import any asset-denom module (G-003).
AssetID string `json:"asset_id" yaml:"asset_id"`
// HolderReachID is the lexicon-clean holder identifier (NOT the banned
// holder-lexicon term; use Holder/Reach per REQ-012). The reach-id that
// asset; the CustodyReleaseAsset handler asserts the signer is this
// holder or an authorized Window grantee.
HolderReachID string `json:"holder_reach_id" yaml:"holder_reach_id"`
// PartnerID is the operator-partner-id (by-ID-string ref to an
// x/partner Anchor Partner — G-003). The Anchor operator that
// custody-received the asset.
PartnerID string `json:"partner_id" yaml:"partner_id"`
// SigRef is the opaque reference to the signature produced by
// CustodyKeyring.Sign on the custody-receive payload. Stored so a
// later CustodyReleaseAsset can verify the release is authorized by
// the same key version that received the asset (rotation safety —
// D-058).
SigRef []byte `json:"sig_ref" yaml:"sig_ref"`
// KeyVersion is the CustodyKeyring active key version at the time of
// custody-receive (recorded so a post-rotation release can detect the
// key has rotated — the handler may require re-attestation).
KeyVersion uint64 `json:"key_version" yaml:"key_version"`
// Status is the custody lifecycle state (Held or Released).
CustodyStatus CustodyStatus `json:"custody_status" yaml:"custody_status"`
}
// CustodyStatus enumerates the custody entry lifecycle states (REQ-036).
// Held is the active state (asset is in custody); Released is the terminal
// state (asset has been released to the holder or an authorized grantee).
// The custody lifecycle is receive → hold → release (A-544
// compliance-before-custody: the handler checks compliance BEFORE the
// custody debit on release).
type CustodyStatus string
const (
// CustodyHeld is the active state: the asset is in custody.
CustodyHeld CustodyStatus = "Held"
// CustodyReleased is the terminal state: the asset has been released.
CustodyReleased CustodyStatus = "Released"
)
// custodyStore is the store-backed custody state (wraps an sdk.KVStore via
// a storeKey on the Keeper). The Keeper owns the storeKey; this struct is
// the helper that reads/writes the custody entries.
type custodyStore struct {
storeKey storetypes.StoreKey
}
// --- Custody store key helpers ------------------------------------------------
var custodyKeyPrefix = []byte("custody/")
func custodyKey(assetID string) []byte {
return append(custodyKeyPrefix, []byte(assetID)...)
}
// custodyPrefixEnd returns the key that sorts immediately after all keys
// sharing the custody key prefix (the standard prefix-iteration end key).
func custodyPrefixEnd() []byte {
return prefixEnd(custodyKeyPrefix)
}
// getCustodyEntry loads a CustodyEntry by assetID. Returns the entry and
// true if found, or zero value + false if not.
func (cs custodyStore) getCustodyEntry(ctx sdk.Context, assetID string) (CustodyEntry, bool) {
store := ctx.KVStore(cs.storeKey)
bz := store.Get(custodyKey(assetID))
if bz == nil {
return CustodyEntry{}, false
}
var e CustodyEntry
if err := json.Unmarshal(bz, &e); err != nil {
return CustodyEntry{}, false
}
return e, true
}
// setCustodyEntry persists a CustodyEntry by assetID.
func (cs custodyStore) setCustodyEntry(ctx sdk.Context, e CustodyEntry) {
store := ctx.KVStore(cs.storeKey)
bz, err := json.Marshal(e)
if err != nil {
panic(fmt.Sprintf("hub: marshal custody entry %q: %v", e.AssetID, err))
}
store.Set(custodyKey(e.AssetID), bz)
}
// deleteCustodyEntry removes a CustodyEntry by assetID (used on full release
// if the entry is not retained; the simtest retains Released entries for
// audit — delete is provided for completeness but the handler uses
// setCustodyEntry with CustodyReleased to retain the audit trail).
func (cs custodyStore) deleteCustodyEntry(ctx sdk.Context, assetID string) {
store := ctx.KVStore(cs.storeKey)
store.Delete(custodyKey(assetID))
}
// allCustodyEntries returns all persisted CustodyEntry records (iteration
// helper, unordered).
func (cs custodyStore) allCustodyEntries(ctx sdk.Context) []CustodyEntry {
store := ctx.KVStore(cs.storeKey)
iterator := store.Iterator(custodyKeyPrefix, custodyPrefixEnd())
defer iterator.Close()
out := []CustodyEntry{}
for ; iterator.Valid(); iterator.Next() {
var e CustodyEntry
if err := json.Unmarshal(iterator.Value(), &e); err == nil {
out = append(out, e)
}
}
return out
}