Skip to content

A wrong ADMIN_PARTY produces a registry that reports healthy and serves nothing #73

Closed

Description

@lmcorbalan

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);
  • /healthz returns ok and /readyz returns ready;
  • /registry/metadata/v1/info advertises the typo'd party as adminId;
  • GET /registry/metadata/v1/instruments returns { "instruments": [] };
  • every get-by-id, transfer-factory, allocation-factory and choice-context
    returns 404 instrument 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

  1. Set ADMIN_PARTY to a party id with a wrong namespace suffix (right hint,
    wrong fingerprint).
  2. Start the service against a sandbox that has at least one InstrumentConfig.
  3. GET /readyz returns 200 ready.
  4. GET /registry/metadata/v1/instruments returns an empty list.
  5. 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 /readyz on 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 InstrumentConfig set is non-empty but contains
    no row whose payload.admin equals config.adminParty.
    This was the
    original proposal here and it is close to worthless: InstrumentConfig is
    signatory admin with 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.

Activity

  1. lmcorbalan commented on Aug 14, 2026

    @lmcorbalan
    CollaboratorAuthor

    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.

    /readyz used to probe activeConfigs, a party- and template-scoped read. It now calls ledgerEnd() (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 wrong ADMIN_PARTY at 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_TOKEN that authenticates but carries no readAs right 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:

    • A bounded party-scoped probe would restore the signal without restoring the cost. There is no active-contracts query that is cheap and party-scoped at the same time, so this likely means a different endpoint rather than a different filter.
    • Whatever is chosen has to stay honest about template ids too. Boot only shape-checks them against /^#[^:]+:[^:]+:[^:]+$/, 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 /readyz changed and the log only appears once a request arrives.

    SPEC.md section 9 now carries the narrowed guarantee as a known gap.

  2. moved this from Backlog to Ready in Canton - dAppBooster (#390)on Aug 18, 2026
  3. lmcorbalan commented on Aug 18, 2026

    @lmcorbalan
    CollaboratorAuthor

    Re-scoped the body against main at 3edc927. The failure is unchanged and
    unfixed; what changed is which fix is worth doing.

    Verified still true in source:

    • registry/src/routes/metadata.ts:29 still serves adminId: deps.config.adminParty with no cross-check against any config's
      payload.admin.
    • registry/src/config.ts:87 is still a bare require_('ADMIN_PARTY'), now the
      only required variable in Config with no validation, since the template ids
      and all three numeric settings gained boot checks around it.
    • Every read is party-scoped to it (activeConfigs, activePreapprovals,
      findByContractId, findEscrow in registry/src/routes/lookup.ts), so a
      wrong party still empties the listing and 404s every by-id, factory and
      choice-context route.
    • No warning was ever added: there is no logger.warn anywhere in
      registry/src.

    What moved:

    • The original proposal (warn on a non-empty config set with no matching
      payload.admin) is demoted to a "deliberately not taken" option. The body
      already 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.
    • The startup check that was filed as an alternative is now the proposal. It
      catches the typo directly, and it also covers the LEDGER_API_TOKEN without
      readAs fault noted in the comment above, which the ledger-end probe cannot
      see either.
    • Added the caveat that decides between warning and refusing to boot: the
      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.md but may not hold for a properly scoped one. Worth one live check
      before implementing.
    • Partly stale in the original body, so noted rather than left standing: "nothing
      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 wrong
      party.

    Staying priority: low. The reason to keep it rather than close it is that
    SPEC.md section 9 now publishes the narrowed readiness guarantee as a known
    gap marked "Tracked", and this is the issue it tracks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: lowNice to have, can wait

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions