Skip to content

Contract lookups download whole active sets and now scale with the admin's ledger footprint #71

Closed

Description

@lmcorbalan

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:

  • TokenAllocation is signatory admin, sender with executor and receiver
    as observers.
  • TokenTransferInstruction is signatory admin, transfer.sender with
    transfer.receiver as observer.
  • Both escrows are LockedToken created with holders = [admin] on a
    signatory admin, owner, holders template.
  • InstrumentConfig is signatory 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

  • LedgerClient exposes 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.
  • LedgerClient exposes 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.ts against the recorded
    request shape (URL, method, body), not just the decoded response.

Call sites

  • findByContractId in registry/src/routes/lookup.ts no longer fetches
    and filters, or is deleted in favour of the new client call.
  • findEscrow resolves the escrow without a full LockedToken scan.
  • The allocation and transfer choice-context routes each issue a bounded
    number of ledger calls, independent of active-set size.
  • GET /readyz no longer calls activeConfigs; 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 that pageSize is ignored. Rejected as the
    primary fix, though it may fall out of the same work.
  • Leave /readyz as 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

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

No one assigned

    Labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions