hkchain.reserve.v1
The reserve module carries proof that circulating stablecoin is backed. It holds the two-signature attestation lifecycle, the auditor registry, and the daily par-value statement the chain writes for itself at each day boundary.
Schema: proto/hkchain/reserve/v1/{query,tx,events,reserve}.proto.
It never touches supply. Its one influence on issuance is indirect and
important: x/stablecoin refuses to mint for an issuer whose latest attestation
here has gone stale.
The attestation lifecycle
An attestation is a claim about one period — how much was circulating, how much reserve backed it, and the hash of the off-chain audit package that evidences both. It becomes trustworthy through two signatures from two parties.
| State | Value | Reached by |
|---|---|---|
ATTESTATION_STATE_PENDING_AUDIT | 2 | MsgSubmitAttestation — the issuer's claim, on the record but not yet attested |
ATTESTATION_STATE_ATTESTED | 3 | MsgCoSignAttestation — a registered auditor co-signs. Terminal. |
ATTESTATION_STATE_DRAFT (1) exists in the enum for a future off-chain drafting
flow and is not reachable today: submission goes straight to pending.
ATTESTATION_STATE_UNSPECIFIED (0) is not a lifecycle state — but you will meet
it as a zero value, and one query returns it deliberately. See
ReserveForAddress below.
Only ATTESTED counts. Every consumer — the mint freshness check, the daily
statement, the public lookup — reads the latest attested row and ignores
pending ones.
Queries
| Method | GET path | Parameters | Returns |
|---|---|---|---|
LatestAttestation | /hkchain/reserve/v1/attestations/{issuer}/latest | issuer | Latest attested attestation; 404 if none |
AttestationForPeriod | /hkchain/reserve/v1/attestations/{issuer}/period | issuer, period_start, period_end | The attestation for exactly that period; 404 if none |
ReserveForAddress | /hkchain/reserve/v1/reserve_for_address/{address} | address | bindings[] — one per held denom |
DailyStatement | /hkchain/reserve/v1/daily_statement/{issuer} | issuer, date | The statement for that UTC day; 404 if none |
Auditors | /hkchain/reserve/v1/auditors | include_revoked, pagination | auditors[] |
Attestations | /hkchain/reserve/v1/attestations/{issuer} | issuer, pagination | attestations[], every state |
Params | /hkchain/reserve/v1/params | — | params |
Note the path family: /attestations/{issuer} is the list, and /latest and
/period hang off it.
Timestamp parameters are RFC 3339, not calendar dates. period_start,
period_end and date are protobuf timestamps, so the gateway expects a full
instant:
curl -s "$REST/hkchain/reserve/v1/daily_statement/hkchain1...?date=2026-08-09T00:00:00Z"
For date the handler truncates to the UTC day, so any instant inside the day
resolves to that day's statement — but a bare 2026-08-09 does not parse.
LatestAttestation
The single most useful call in this module: it answers "is this issuer's backing current, and as of when".
curl -s "$REST/hkchain/reserve/v1/attestations/hkchain1.../latest"
{
"attestation": {
"id": "4",
"issuer": "hkchain1...",
"period_start": "2026-08-01T00:00:00Z",
"period_end": "2026-08-08T00:00:00Z",
"random_day": "2026-08-05T00:00:00Z",
"circulation": { "denom": "hkd.hsbc", "amount": "125000000" },
"reserve_market_value": { "denom": "hkd", "amount": "130000000" },
"attestation_hash": "sha256:1f0a...",
"issuer_sig": "",
"auditor_sig": "...",
"auditor": "hkchain1...",
"state": "ATTESTATION_STATE_ATTESTED",
"submitted_at": "2026-08-08T03:20:11Z",
"attested_at": "2026-08-08T09:47:02Z"
}
}
Two details in that payload:
- Both denominations are issuer-supplied, and the chain does not check
either. The intent is that
circulationcarries the issuer's stablecoin denom andreserve_market_valuecarrieshkd, the off-chain currency — they are deliberately different, so never compare them as coins; compare the amounts with each denom's decimals in mind. ButMsgSubmitAttestationvalidates only that both coins parse and are positive, so any syntactically valid denom is accepted and stored verbatim, and the daily statement copies the submittedreserve_market_valuecoin through unchanged. Read thedenomon every coin before doing arithmetic on it, and reject the row in your own client if it is not the pair you expect. random_dayis the unannounced business-day snapshot the supervisory regime requires. It must fall inside the period, and the chain enforces that on submission — unlike the denominations.
404 means "no attested attestation", not "no such issuer." An issuer with a pending submission and nothing finalised answers 404 here.
AttestationForPeriod
Exact match on both boundaries — this is a lookup, not a range search. There is no "which attestation covers this date" query; ask for the period you already know. If two submissions carry the same period, the highest id wins.
curl -s "$REST/hkchain/reserve/v1/attestations/hkchain1.../period?period_start=2026-08-01T00:00:00Z&period_end=2026-08-08T00:00:00Z"
Unlike LatestAttestation, this one returns the row in whatever state it is in.
Check state before presenting it as attested.
ReserveForAddress
The public verifiability endpoint: given any address, which reserve attestation covers the stablecoin it currently holds. One binding per registered denom the address holds a positive balance of.
curl -s "$REST/hkchain/reserve/v1/reserve_for_address/hkchain1..."
{
"bindings": [
{
"issuer": "hkchain1...",
"denom": "hkd.hsbc",
"balance": { "denom": "hkd.hsbc", "amount": "25000" },
"attestation": { "id": "4", "state": "ATTESTATION_STATE_ATTESTED", "…": "…" }
}
]
}
Two answers look similar and mean different things:
bindingsis empty — the address holds no registered stablecoin. Nothing to attest.- A binding whose
attestation.stateisATTESTATION_STATE_UNSPECIFIED— the address holds the denom, but its issuer has no attested attestation. The attestation object is present and zero-valued rather than absent, because the field is not nullable.
Gate any "backed by" display on state == ATTESTATION_STATE_ATTESTED. Treating
a zero-valued attestation as real would show a holder that their balance is
backed by an attestation of zero.
Only denoms registered in x/stablecoin are considered; a native ahkc balance
never appears here.
Auditors and Attestations
Both paginate in memory, by offset and limit, over a registry small enough that
this is free. next_key is unused; page with pagination.offset, and send
pagination.count_total=true if you want total populated.
Auditors hides revoked rows by default. Pass include_revoked=true to see the
full history — revocation sets a timestamp rather than deleting the record, so
the registry stays auditable.
Messages
| Message | Signer field | Authorised when |
|---|---|---|
MsgSubmitAttestation | issuer | Signer is LEVEL_LICENSED_ISSUER in x/kyc |
MsgCoSignAttestation | auditor | Signer is in the auditor registry and not revoked |
MsgRegisterAuditor | authority | Signer is the governance authority |
MsgRevokeAuditor | authority | Signer is the governance authority |
MsgUpdateParams | authority | Signer is the governance authority |
MsgSubmitAttestation —
{ issuer, period_start, period_end, random_day, circulation, reserve_mv, attestation_hash }
→ { attestation_id }.
Validation, in order: issuer level → period_end strictly after period_start
→ random_day inside [period_start, period_end) → both coins valid and
positive → attestation_hash non-empty. The response carries the new id, which
is what the auditor co-signs and what MsgMint references as
attestation_ref.
"Valid and positive" is the whole coin check: the denominations are not
validated against the issuer's registered denom or against hkd. Getting them
right is the submitter's responsibility, and a consumer should verify them
rather than assume them.
Nothing prevents an issuer from submitting overlapping or repeated periods. The chain records claims; it does not adjudicate between them.
MsgCoSignAttestation — { auditor, attestation_id, signature }.
The auditor must be registered and un-revoked, the attestation must exist and be
in PENDING_AUDIT, and signature must be non-empty. On success the row moves
to ATTESTED, recording the auditor address, the signature bytes and the block
time.
Two properties worth designing around:
- The signature bytes are stored, not verified. The chain records what the auditor supplied; it does not check it against a key. The trust comes from which registered account signed the transaction, which the SDK does verify.
- The co-sign threshold parameter is not enforced.
min_auditor_cosign_countexists in params and defaults to 1, but the handler finalises on the first valid co-signature regardless of its value. Raising it by governance would not today require a second auditor.
MsgRegisterAuditor — { authority, address, name, license_ref }. name
must be non-empty; re-registering an existing address fails, including one that
was revoked. MsgRevokeAuditor — { authority, address } — stamps
revoked_at; revoking twice fails.
Typed events
| Event | Emitted by | Carries |
|---|---|---|
EventAttestationSubmitted | MsgSubmitAttestation | attestation_id, issuer, circulation, reserve_market_value, attestation_hash |
EventAttestationCosigned | MsgCoSignAttestation | attestation_id, auditor |
EventAttestationFinalized | MsgCoSignAttestation | attestation_id, issuer |
EventAuditorRegistered | MsgRegisterAuditor | address, name, license_ref |
EventAuditorRevoked | MsgRevokeAuditor | address |
EventDailyStatement | The end-of-block hook, not a message | issuer, date, circulation, reserve_market_value, latest_attestation_id |
A single co-sign emits both EventAttestationCosigned and
EventAttestationFinalized, in that order and in the same transaction. The
split anticipates a multi-signature threshold where the two would separate; an
indexer today should key finalisation on the second and not assume the first
implies a non-terminal state.
The daily statement
At the first block of a new UTC day the module writes one statement per issuer: circulation now, the reserve market value from that issuer's latest attested attestation, and the id of that attestation. It is written to state and emitted as an event, so history is queryable without replaying the event stream.
Three behaviours to know:
- It has no transaction.
EventDailyStatementis emitted from the block's end hook, so it carries no transaction hash and cannot be found by transaction search. Recover a missed one from theDailyStatementquery, or from the block results at that height. - An issuer with no attested attestation is skipped entirely, and skipped without marking the day done — the module retries on the next block rather than publishing a statement with zero backing.
- One row per issuer, not per denom. An issuer owning several denoms gets a
single statement, and its
circulationfigure is taken from the first denom in store order. Read per-denom circulation fromx/stablecoininstead of inferring it here.
date on the event is a plain YYYY-MM-DD string; date on the stored
statement is a timestamp at UTC midnight. The event field is not a timestamp —
do not parse the two the same way.
Parameters
| Parameter | Type | Default | Effect |
|---|---|---|---|
min_auditor_cosign_count | uint32 | 1 | Declared threshold of auditor co-signatures. Validation rejects 0; the co-sign handler does not read it. |
The parameter that actually governs this module's interaction with issuance
lives elsewhere: max_attestation_staleness in
hkchain.stablecoin.v1 decides how old the latest attested
attestation may be before minting stops.
Errors
Codespace reserve; the code table is in
Errors and rejections. The distinctive ones are 8
(attestation is in the wrong state for this transition — usually a second
co-sign attempt), 9/11 (auditor not registered / already revoked) and 13
(invalid period — random_day outside the window is the common case).
Queries in this module answer with gRPC NotFound / HTTP 404 rather than module
error codes.