Repository navigation
Description
Description
A wrong ADMIN_PARTY produces a registry that reports healthy and serves
nothing, with no diagnostic anywhere pointing at the variable.
GET /registry/metadata/v1/info returns adminId straight from
config.adminParty:
res.json({ adminId: deps.config.adminParty, supportedApis: SUPPORTED_APIS })Nothing cross-checks that party against the Daml admin signatory of any
InstrumentConfig the service can actually read, and nothing checks at boot
that the party exists on the participant or that the configured token may read
as it. ADMIN_PARTY is now the only required variable in Config with no boot
validation of any kind: every template id goes through requireTemplateId, and
PORT, SHUTDOWN_TIMEOUT_MS and DIRECT_TRANSFER_MARGIN_MS all parse and
range-check, while the admin party is a bare require_.
/readyz does not close the gap. As filed it called activeConfigs and treated
any successful response as ready, including an empty one. Since #71 it reads
the ledger end instead, which is scoped to neither the admin party nor any
template, so it cannot fail for a wrong ADMIN_PARTY at all (see the comment
below).
So with a typo'd namespace suffix in ADMIN_PARTY:
- the service boots (the variable is present, so
require_is satisfied); /healthzreturnsokand/readyzreturnsready;/registry/metadata/v1/infoadvertises the typo'd party asadminId;GET /registry/metadata/v1/instrumentsreturns{ "instruments": [] };- every get-by-id, transfer-factory, allocation-factory and choice-context
returns 404instrument not found; - nothing is logged.
A wallet that reads adminId from /info and builds
InstrumentId { admin, id } from it gets a 404 on every subsequent call, and
the operator has no signal distinguishing "misconfigured party" from "no
instruments created yet".
This is newly consequential. When adminId carried an unrelated operator id,
it was not the value clients keyed instruments by, and the read party being
wrong was not the same failure as the advertised identity being wrong. After
the single-admin collapse they are the same value, so one typo breaks both.
Steps to reproduce
- Set
ADMIN_PARTYto a party id with a wrong namespace suffix (right hint,
wrong fingerprint). - Start the service against a sandbox that has at least one
InstrumentConfig. GET /readyzreturns 200ready.GET /registry/metadata/v1/instrumentsreturns an empty list.- Nothing in the logs mentions
ADMIN_PARTY.
Expected vs actual behavior
Expected: a party that can read nothing produces a diagnostic that names
the variable.
Actual: indistinguishable from a correctly configured, empty registry.
Proposed fix
Validate the admin party at startup, the way every other required variable
is already validated: confirm that config.adminParty exists on the
participant and that LEDGER_API_TOKEN may read as it, and refuse to boot
otherwise, with an error naming the variable at fault.
This catches the typo directly rather than inferring it from an empty result
set, and it covers a second fault that reaches the same blind spot: a token
that authenticates but carries no readAs right for the configured admin
passes the ledger-end probe, so the service reports {"status":"ready"} and
then fails every route. A boot check kills both.
It needs a new ledger-client call (a parties lookup, or whatever the cheapest
party-scoped read on the participant turns out to be), so it lands in
registry/src/ledger.ts alongside ledgerEnd and lookupByContractId.
Settle first, because it decides between warning and refusing to boot: the
silent-empty chain above assumes the participant accepts a filter party the
token has no readAs for and answers an empty set. That holds for the wildcard
sandbox token in RUNBOOK.md. Against a properly scoped token the participant
may well reject the query outright, in which case the wrong party is already
loud (500s on every route) rather than silent, and the boot check is a
usability improvement rather than a fix for an invisible failure. Confirm
against a live participant before choosing the failure mode.
Two options deliberately not taken:
- Failing
/readyzon an empty active set. A freshly deployed registry
legitimately has zero instruments until the admin creates one, and a
readiness probe that fails then would make the service flap on every clean
deploy. The empty case is genuinely ambiguous and must not be treated as an
error. - Warning when the readable
InstrumentConfigset is non-empty but contains
no row whosepayload.adminequalsconfig.adminParty. This was the
original proposal here and it is close to worthless:InstrumentConfigis
signatory adminwith no observers, so on a single-party token every
readable row is by construction administered by the read party, and a wrong
party yields an empty set rather than a mismatched one. The check can only
ever fire on a multi-party ledger token, which is not the deployment this
failure actually bites on.
Environment
registry/src/config.ts (ADMIN_PARTY, and the require_ that does not check
it), registry/src/ledger.ts (where a party-scoped check would live),
registry/src/server.ts (/readyz), registry/src/routes/metadata.ts
(/info).
Additional context
Found by the high-effort code review of the read-as-admin branch. That branch
did add the missing loadConfig coverage (ADMIN_PARTY is now asserted to
bind to Config.adminParty, and to throw when absent), so a mis-binding is
now caught by the suite. This issue is about a correctly bound but
wrong-valued party, which no test can catch.
SPEC.md section 9 ships the narrowed guarantee as a published known gap
("Readiness does not prove the service can serve ... Tracked."), and this issue
is what that "Tracked" refers to.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
lmcorbalan commentedon Aug 14, 2026
The readiness probe changed on
perf/bounded-contract-lookups(#71), and it widens this issue's blind spot rather than narrowing it, so recording the new starting point here./readyzused to probeactiveConfigs, a party- and template-scoped read. It now callsledgerEnd()(GET /v2/state/ledger-end), which was the point: the probe's cost no longer grows with the admin's ledger footprint. The consequence for this issue is that the probe is now scoped to neither the admin party nor any template, so it cannot fail for a wrongADMIN_PARTYat all. Previously it would at least have surfaced a party the participant does not host as an error from the active-contracts query.A second configuration fault now reaches the same place. A
LEDGER_API_TOKENthat authenticates but carries noreadAsright for the configured admin passes the ledger-end call, so the service reports{"status":"ready"}and then fails every route.What a fix has to weigh, which is why it belongs here and not in #71:
/^#[^:]+:[^:]+:[^:]+$/, so a well-formed id naming a template that does not exist still boots.Partly mitigated in the same branch: a by-id lookup the participant rejects for any reason other than reporting a missing contract is now logged with its status and body. That covers the silent-404 half of the failure (a wrong template id, a rejected event format, a participant not serving the endpoint), but not this issue, since nothing about
/readyzchanged and the log only appears once a request arrives.SPEC.mdsection 9 now carries the narrowed guarantee as a known gap.lmcorbalan commentedon Aug 18, 2026
Re-scoped the body against
mainat3edc927. The failure is unchanged andunfixed; what changed is which fix is worth doing.
Verified still true in source:
registry/src/routes/metadata.ts:29still servesadminId: deps.config.adminPartywith no cross-check against any config'spayload.admin.registry/src/config.ts:87is still a barerequire_('ADMIN_PARTY'), now theonly required variable in
Configwith no validation, since the template idsand all three numeric settings gained boot checks around it.
activeConfigs,activePreapprovals,findByContractId,findEscrowinregistry/src/routes/lookup.ts), so awrong party still empties the listing and 404s every by-id, factory and
choice-context route.
logger.warnanywhere inregistry/src.What moved:
payload.admin) is demoted to a "deliberately not taken" option. The bodyalready conceded it can only fire on a multi-party ledger token, which is not
the deployment this bites on, so it is not worth an implementer's time.
catches the typo directly, and it also covers the
LEDGER_API_TOKENwithoutreadAsfault noted in the comment above, which the ledger-end probe cannotsee either.
silent-empty chain assumes the participant answers an empty set for a filter
party the token cannot read as, which holds for the wildcard sandbox token in
RUNBOOK.mdbut may not hold for a properly scoped one. Worth one live checkbefore implementing.
is logged" is no longer absolute, since Contract lookups download whole active sets and now scale with the admin's ledger footprint #71 logs by-id rejections that are not
CONTRACT_EVENTS_NOT_FOUND. That covers a wrong template id, not a wrongparty.
Staying
priority: low. The reason to keep it rather than close it is thatSPEC.mdsection 9 now publishes the narrowed readiness guarantee as a knowngap marked "Tracked", and this is the issue it tracks.