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 }