What the consumer is
Custody protocol v1 is owned by the parentcustody-protocol package in the KeyStone-OS/keystone repository: its normative specification, EIP-712 type strings, closed JSON schemas, constants and golden vectors. This API consumes one accepted revision of it and recomputes what that revision pins. It never defines a format of its own, and a vector is never edited to make this consumer pass; a disagreement is a defect to return to the parent.
The runtime pin is
ACCEPTED_PROTOCOL in keystone/settlement/custody/constants.py; the artifact tests fail if the fixture copy, the provenance file or this page cite a different commit or digest, and if any artifact byte differs from the manifest.
Where it lives
Function names are Python snake_case of the parent’s names (
validateBundle is validate_bundle, applyFact is apply_fact). Inputs are custody models or closed raw wire values; outputs are lowercase hex strings, frozen result records or a CustodyProtocolError carrying the pinned code. No function reads a clock, a setting, a database or a key, and none performs I/O.
Wire models keep the parent’s camelCase keys as attribute names, so a model dump is the exact object the protocol digests. The one keyword, the EVM artifact’s from, carries an alias; every dump goes through .wire(), which applies aliases and omits absent optional fields without inserting nulls.
What it enforces
- Commitments.
manifestHash = hashStruct(CustodyHeader) = bundleIdunder theKeystoneCustodyRegistrydomain pinned by the cash group’s chain and Registry. Hashing goes through eth-account and eth-abi; the typehashes, struct hashes, domain separators and signing digests of both profiles’ vectors recompute, and the fixture’s public test keys reproduce every committed signature byte for byte through the same library. - Signatures. Exactly one encoding: 65 lowercase hex bytes,
randsin[1, n-1], lows,vin . The high-s twin and any out-of-range scalar are refused before the recovery library runs; an unrecoverable point is refused asSIGNATURE_INVALIDwith the safe identity and no library text. Hash-only types never get a signing digest. - Closed JSON. RFC 8785 over strings, booleans, null, lists and string-keyed objects. Numbers,
Decimal, non-string keys, arbitrary objects and lone surrogates are refused; keys sort by UTF-16 code units, not code points. The API request digestcustody-api-request-jcs-keccak256/v1and the deployment, package, artifact and readiness digests all use it. The escrowsha256-canonical-json/v1request digest is a different function and is not an RFC 8785 implementation. - Strict schemas. A JSON number, a coercible boolean or a
Decimalnever becomes a decimal-string field, and numeric zero never becomes the false-only trust label; width overflow, zero commitments, zero trade amounts, checksum casing, whitespace and Unicode normalization are refused, while a zero balance stays representable. A malformed child is a sanitizedSCHEMA_INVALIDnaming paths only; the dependent refinements run only on validated children. - Admission order.
validate_bundlerefuses in the frozen order (profile, deployment digest, admitted keys, group domains, timing, topology, commitments, addressed identity);verify_recovery_packageadds recipient, own-term inclusion and the source signature;verify_package_admissionbinds mandate and acknowledgement to the exact digest sent and the admitted payer key. - Evidence. Exactly five expected facts per header; monotonic versions per fact key; identical replay is a no-op, an equal version with a changed payload conflicts, a lower version is stale, nothing changes after SETTLE or ABORT; expiry equality is invalid; registration is exactly the three non-readiness facts, each current, strictly before cutoff.
- Projections. A custodian view carries only its own private terms, the source view both and no account-wide books; the groups are the fixed layout with recomputed ids;
completion.satisfiedmust equal the canonical predicate; events pages, exports and provenance are refused when their correlations do not hold rather than repaired.
Conformance executed here
Every family of the parent’s section 16 matrix for the Python API runs intests/settlement/custody/ and tests/schemas/test_custody.py, each case through the entrypoint its check names; an unmapped check fails the run.
The parent’s draft 2020-12 JSON Schemas are also loaded with an independent validator: every positive sample passes both validators, every negative is refused by the models, and the exact set of negatives only the code refuses (numeric upper bounds and the executable refinements) is pinned, so a refinement silently moving into or out of JSON Schema is noticed.
What is explicitly not claimed
- No route, query or body is served; the route tables in
constants.pydescribe the contract a later ticket mounts and are not registered with FastAPI. The custody API error codes are not emitted by this API and do not appear in the error codes guide. - No authorization storage:
select_viewandcloseout_authorizedevaluate grants handed to them; no grant is persisted or looked up. - No durable command, replay cache or revision counter:
admit_commandandadmit_intakedecide over a supplied stored record and resource state. The ticket that mounts the routes owns authentication, the storage lock, replay before revision under it, transactional acceptance and post-commit dispatch. - No Registry, Canton or provider effect: eligibility and registration are computations over supplied facts and an explicit time, never an observation that a fact or a native effect occurred.
- No signer and no spending key: the fixture keys are public test material under
tests/fixtures/custody/and are never copied into runtime modules or configuration. - No Solidity parity, no live Canton or provider qualification and no native transaction atomicity: those belong to their own tickets.