Repository navigation
Description
User story / Problem statement
Every contract the registry service looks up by id is found by downloading a
template's entire active set and scanning it in memory. There is no
by-contract-id query, no instrument filter, and no pagination anywhere in the
ledger client.
This was harmless while the service read as a separate operator party: the
operator was a stakeholder of nothing, so activeContracts returned zero rows
and every in-memory scan was O(0). Now that the service reads as the
instrument admin, the same code paths pull the admin's whole ledger footprint,
and the admin is a stakeholder of essentially every contract the token model
creates:
TokenAllocationissignatory admin, senderwithexecutorandreceiver
as observers.TokenTransferInstructionissignatory admin, transfer.senderwith
transfer.receiveras observer.- Both escrows are
LockedTokencreated withholders = [admin]on a
signatory admin, owner, holderstemplate. InstrumentConfigissignatory admin.
Concrete cost per request today:
| Request | Full active sets downloaded |
|---|---|
POST /registry/allocations/v1/:id/choice-contexts/* |
every TokenAllocation, then every LockedToken |
POST /registry/transfer-instruction/v1/:id/choice-contexts/* |
every TokenTransferInstruction, every InstrumentConfig, every LockedToken |
POST .../transfer-factory |
every InstrumentConfig, every TokenTransferPreapproval |
GET /readyz |
every InstrumentConfig |
Each row carries a createdEventBlob, so this is not a cheap count query: an
instrument with a few thousand open escrows makes every choice-context request
transfer megabytes, and the probe on /readyz runs on the orchestrator's
interval forever.
/readyz is the sharpest instance of the general problem. It previously
probed a TokenRegistry singleton, so it was O(1) by construction; that
template is gone and the probe now scans a set that grows without bound. A
readiness probe should be the cheapest request the service serves.
Expected outcome
The ledger client grows a bounded way to answer "does this contract id exist,
and what is it", and a bounded way to answer "is the ledger reachable", and
the four call sites use them. No request cost should scale with the number of
active contracts the admin happens to be a stakeholder of.
Acceptance criteria
Ledger client
-
LedgerClientexposes a lookup that resolves a single contract by id
without downloading a template's active set, or documents in code why the
JSON Ledger API cannot do it and what the bounded alternative is. -
LedgerClientexposes a cheap liveness/reachability call for the
readiness probe (ledgerEnd()or equivalent) that does not depend on
any template's active set size. - Both are covered by
registry/test/ledger.test.tsagainst the recorded
request shape (URL, method, body), not just the decoded response.
Call sites
-
findByContractIdinregistry/src/routes/lookup.tsno longer fetches
and filters, or is deleted in favour of the new client call. -
findEscrowresolves the escrow without a fullLockedTokenscan. - The allocation and transfer choice-context routes each issue a bounded
number of ledger calls, independent of active-set size. -
GET /readyzno longer callsactiveConfigs; the health suite asserts
what it does call instead.
Non-regression
- The existing 404 behaviour is unchanged: an unknown allocation,
instruction or escrow still produces its own distinct message
(allocation not found,transfer instruction not found,
escrow not found). - The registry suite stays green and the party-aware ledger stub still
models stakeholder visibility, so a route reading as a non-stakeholder
still sees nothing.
Alternatives considered
- Filter server-side by instrument instead of by contract id. Reduces the
set but does not bound it: one popular instrument is exactly the case that
hurts. Rejected as a half measure. - Cache active sets in the service. Introduces staleness into choice
contexts, which are consumed immediately by an on-ledger submission and must
reflect the current ledger. Rejected. - Paginate and stop at the first match. Still
O(n)in the worst case
(the contract is not there, which is exactly the 404 path an attacker can
drive), and pageSize and pageToken are documented on the instrument list endpoint but ignored by its handler #46 already tracks thatpageSizeis ignored. Rejected as the
primary fix, though it may fall out of the same work. - Leave
/readyzas is and fix only the lookups. The probe runs on a
fixed interval whether or not anyone is using the service, so it is the one
call guaranteed to be issued forever. Rejected.
Technical notes
- All four call sites are in
registry/src/routes/lookup.tsand
registry/src/server.ts; the helpers were deliberately centralized there so
this change has a small blast radius. - This is the reason the escrow lookup got its own helper (
findEscrow)
rather than staying inline: the bounded version replaces one function. - Interacts with pageSize and pageToken are documented on the instrument list endpoint but ignored by its handler #46 (pageSize ignored) and with the live-node fixes in Choice-context routes pass interface ids but the ledger client only builds template filters #39,
Active-contract queries run at offset 0 and return an empty set on a live node #40 and Command submissions use a flat envelope without userId and are rejected by a live node #53, all of which touchregistry/src/ledger.ts. Land after those
or expect a conflict in that file. - The JSON Ledger API's exact request shape for a by-contract-id read is
unverified against a live node, the same standing caveat that applies to
the command envelope and theAnyValueencoding. Verify against a running
sandbox before assuming the endpoint exists in the form the docs suggest. - Do not "fix" this by having the routes read as the requesting party: the
service holds one static ledger token for the admin and never forwards a
caller's JWT.
Additional context
Surfaced by the high-effort code review of the branch that repointed the
service's reads from the operator party to the instrument admin. The review
verified the mechanism against the Daml templates; it is a consequence of the
read-party change, not of any defect in it, and was deliberately kept out of
that branch because it needs a new ledger-client capability.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity