Skip to content

Active-contract queries run at offset 0 and return an empty set on a live node #40

Closed

Description

@lmcorbalan

Description

Active-contract queries are issued at ledger offset zero, the beginning of the ledger. On a live participant the active contract set at the beginning of the ledger is empty, so every query returns nothing and the service behaves as though no instrument, holding, or instruction exists.

Steps to reproduce

  1. Start a Canton participant with the JSON Ledger API enabled and upload the DAR.
  2. Seed an operator, a registry, and one instrument.
  3. Ask the service to list instruments.

Expected vs actual behavior

Expected: the seeded instrument is returned.

Actual: an empty list, because the query snapshots the ledger at its beginning rather than at the current offset.

Additional context

Part of #23. Blocks the end-to-end transfer test and the CLI conformance check, and should be fixed alongside the filter defect since both surface on the first live-node request.

Not caught by the current tests: the in-memory ledger stub ignores the offset entirely.

Activity

  1. added
    bugSomething isn't working
    priority: criticalBlocking work, system down, or security issue
    on Jul 22, 2026
  2. moved this from Backlog to Ready in Canton - dAppBooster (#390)on Jul 22, 2026
  3. lmcorbalan commented on Aug 4, 2026

    @lmcorbalan
    CollaboratorAuthor

    Confirmed against a live node, and the fix is one line

    Reproduced exactly as described, then proved the diagnosis by fixing it. Environment: Canton 3.5.6 via dpm sandbox, seeded with scripts/seed.mjs (an operator, an instrument admin, a TokenRegistry, and one CC InstrumentConfig created through propose/accept).

    Reproduction

    With the service pointed at the seeded sandbox, the instrument exists on the ledger but the read path reports nothing:

    GET /registry/metadata/v1/instruments   ->   {"instruments":[]}
    

    /healthz, /readyz, and /registry/metadata/v1/info all answer correctly, so the service is up, the config is valid, and the operator party resolves. /readyz is worth calling out: it reports ready in this state, because the underlying query succeeds and simply returns an empty set. Readiness cannot distinguish "ledger reachable" from "ledger reachable but every query is blind".

    Proof of the diagnosis

    Patching only activeAtOffset in the built output, so that it reads the current offset first, and changing nothing else:

    activeAtOffset: (await (await this.fetchFn(`${cfg.ledgerApiUrl}/v2/state/ledger-end`)).json()).offset

    Both metadata read paths immediately return the seeded instrument:

    GET /registry/metadata/v1/instruments
    {"instruments":[{"id":"CC","name":"Canton Coin Forge","symbol":"CC","decimals":10,"supportedApis":{...}}]}
    
    GET /registry/metadata/v1/instruments/CC
    {"id":"CC","name":"Canton Coin Forge","symbol":"CC","decimals":10,"supportedApis":{...}}
    

    The patch was reverted; it was a throwaway experiment to isolate the cause, not the fix. The registry suite is still 67/67 green.

    So the offset really is the whole defect on the read path, and no other change is needed to make metadata reads work live.

    Technical notes for the fix

    • The offset comes from GET /v2/state/ledger-end, which returns {"offset": <n>}. On a freshly started sandbox with a DAR uploaded it was already 8, which is why offset 0 returns nothing.
    • Every activeContracts call needs a current offset, so the natural shape is to resolve the ledger end inside HttpLedgerClient.activeContracts rather than at each of the call sites in lookup.ts and the routes.
    • That costs one extra round trip per query. Several call sites already run two queries concurrently via Promise.all, so if the extra hop matters the offset can be fetched once per request and threaded through, but correctness first.
    • This must be fixed together with the identifier-form half of Choice-context routes pass interface ids but the ledger client only builds template filters #39 and the command-envelope defect, since all three surface on the first live request and share one adjustment site (registry/src/ledger.ts).
    • The in-memory test stub ignores the offset entirely, so no existing test will fail if this regresses. A live-node test is the only real guard, which is what the guarded end-to-end test (Drive an end-to-end transfer through the registry service against a sandbox #42) is for.

    Full detail of the conventions this participant enforces is recorded in tracked RUNBOOK.md.

  4. moved this from Ready to In progress in Canton - dAppBooster (#390)on Aug 4, 2026
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: criticalBlocking work, system down, or security issue

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions