Skip to content

Command submissions use a flat envelope without userId and are rejected by a live node #53

Closed

Description

@lmcorbalan

Description

The registry service builds a flat command-submission body and never sends a userId. A live Canton participant rejects both, so every write path the service exposes fails on the first request. The read paths are covered by #39 and #40; this issue covers the write half, which nothing had exercised until the sandbox runbook landed.

HttpLedgerClient.submitAndWait in registry/src/ledger.ts posts:

{ "commands": [...], "actAs": [...], "commandId": "...", "disclosedContracts": [...] }

Two things are wrong with that body:

  1. The command list must be nested inside a commands object. The participant expects {"commands": {"commands": [...], "actAs": [...], "commandId": ..., "userId": ..., "disclosedContracts": [...]}}. Our flat body loses every field, because the parser looks for them one level deeper.
  2. userId is required and never sent. When the participant runs without authentication there are no claims to default it from, so the submission is refused outright.

A third, narrower encoding defect sits on the same path: Daml Int64 fields must be JSON strings, not numbers. This bites POST /admin/instruments, which forwards decimals as a number straight from the request body into TokenRegistry_ProposeInstrument.

Affected endpoints: every one that submits. That is POST /admin/instruments (propose), the accept and reject admin routes, and any future route that writes. The factory and choice-context routes are unaffected here because they only build a context for the caller to submit, though they are separately blocked by #39 and #40.

Steps to reproduce

  1. Start a sandbox and seed it: bash scripts/sandbox.sh, then node scripts/seed.mjs
  2. Configure the service with the .env block the seed prints and start it
  3. POST /admin/instruments with a valid admin party, instrument id, name, symbol, and decimals

Expected vs actual behavior

Expected: the service submits TokenRegistry_ProposeInstrument as the admin, with the operator-signed registry disclosed, and returns the created proposal.

Actual: the participant rejects the submission. With the flat body:

400 Invalid value for: body (Missing required field at 'commands.commands',
Missing required field at 'commands.commandId',
Missing required field at 'commands.actAs')

Once nested but with no userId:

400 INVALID_TOKEN: The submitted request is missing a user-id: Cannot default
user_id field because claims do not specify an user-id. Is authentication turned on?

Once both are fixed but with decimals sent as a number:

500 LEDGER_API_INTERNAL_ERROR: Expected ujson.Str (data: 10)

Environment

Canton: 3.5.6 (what `dpm sandbox` resolves; the SDK pin is 3.4.11)
JSON Ledger API: v2, http://localhost:7575, authentication off
Node: 18+
Service: registry/ at the tip of feat/registry-http-service

Additional context

Part of #23. Sibling of #39 (identifier form and env var naming) and #40 (active-contract snapshot offset). All three surface on the first live request and share one adjustment site, registry/src/ledger.ts, so they should be fixed and verified together rather than in three passes.

Confirmed working once corrected. scripts/seed.mjs performs exactly these submissions with the nested envelope, an explicit userId of participant_admin, and decimals as a string. Against a live sandbox it creates a TokenRegistry, exercises TokenRegistry_ProposeInstrument as the admin with the registry explicitly disclosed, and exercises InstrumentConfigProposal_Accept as the operator. So the fix is known-good in shape, not speculative. The script is the reference implementation.

Disclosure was verified on this path too: {templateId, contractId, createdEventBlob, synchronizerId} taken from an active-contracts row is accepted, which is the pattern the admin propose route already uses.

Not caught by the test suite and it will not be. The in-memory ledger stub records whatever body it is handed and asserts on the fields the tests care about, so both the flat shape and the missing userId pass 67/67 today. Only a live-node test can guard this, which is what #42 is for.

Open question for the implementer: which userId should the service send? The sandbox accepts participant_admin, but that is a sandbox convenience. On a real deployment this is an operator-configured ledger user with rights over the operator party, so it likely wants its own env var rather than a hardcoded value.

The full set of conventions this participant enforces is recorded in tracked RUNBOOK.md, under "JSON Ledger API conventions this sandbox enforces".

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: criticalBlocking work, system down, or security issue

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions