3c52aa1bb2
---ci--- project: oy phase: 4 milestone: v0.5 status: complete requirements: covered: [REQ-036] partial: [] ---/ci---
169 lines
6.9 KiB
Go
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
|
|
}
|