Skip to content

Transactions silently dropped when chain started via 'pnpm dev' (turbo); 'node' direct works #519

@zkokio

Description

@zkokio

Transactions silently dropped when chain is started via pnpm dev (turbo) — node direct start works

Summary

When the in-memory chain is started via pnpm dev (which invokes turbo run dev), every transaction submitted by a node client is silently dropped: blocks are produced normally but every block reports 0 txs. Starting the same chain directly with node ./src/start.ts start ./core/environments/inmemory/chain.config.ts makes the exact same transactions land in blocks and execute successfully. No error is logged in either case.

This is a hard-to-find class of bug — there is no failure signal, just 0 txs forever — so writing it up in case it helps anyone else hitting it.

Environment

  • starter-kit cloned earlier in 2026, ~16 commits ahead of develop with custom runtime modules added
  • @proto-kit/* 0.2.0
  • o1js 2.14.0-dev.e1080
  • tsyringe ^4.10.0
  • Node 18.18.0
  • pnpm 9.x (whatever the starter ships with)
  • turbo 2.6.1
  • Ubuntu 24, Hetzner CX/CPX instance
  • Custom runtime modules added beyond the starter (Treasury, Ledger, UnitRegistry, Tax, Sales, DevelopmentRegistry) — not relevant to the reproduction; the bug also affects starter modules.

What we observed

Running the chain via pnpm dev:

chain:dev: Produced block #1 (0 txs)
chain:dev: Produced block #2 (0 txs)
chain:dev: Produced block #3 (0 txs)
...

Submitting a transaction from a node test client — await tx.sign(); await tx.send(); — completes without throwing. The chain log keeps producing blocks at 0 txs. State reads return empty/null because the writes never happened. No error message anywhere — stdout, stderr, GraphQL response, or test client.

GraphQL schema introspection works (the chain is reachable). Module registration happens correctly at startup (visible with --logLevel debug). The test client successfully calls .start() against the GraphQL endpoint. Everything looks fine.

Reproduction

  1. Clone the starter and add a couple of runtime modules with a few @runtimeMethods and StateMap state (or just use the stock starter — the failure is independent of the runtime contents).
  2. Start the chain via pnpm dev.
  3. Wait for it to produce blocks.
  4. Run any test that submits a tx via a node client (buildNodeClient from core/environments/node.config.ts, client.transaction(...), tx.sign(), tx.send()).
  5. Observe: every block produced after the tx submission is still 0 txs. State remains empty. No errors anywhere.

Workaround

Start the chain process directly, bypassing pnpm dev / turbo:

cd packages/chain
nohup node \
  --loader ts-node/esm \
  --experimental-vm-modules \
  --experimental-wasm-modules \
  --es-module-specifier-resolution=node \
  ./src/start.ts \
  start ./core/environments/inmemory/chain.config.ts \
  --logLevel debug \
  > /tmp/protokit.log 2>&1 &

The same environment variables are still required:

export PROTOKIT_ENV_FOLDER=inmemory
export PROTOKIT_GRAPHQL_PORT=8080
export PROTOKIT_TRANSACTION_FEE_RECIPIENT_PUBLIC_KEY=B62q...

With this direct invocation, txs land in blocks as expected — same code, same env, same tests, same everything except the wrapper.

Suspected cause

We don't know. Things we noticed but didn't confirm:

  • turbo.json has "envMode": "loose" so it isn't a basic env-stripping issue.
  • The dev task in turbo.json is marked persistent: true and cache: false, which is correct for long-running processes.
  • The pnpm dev chain is: top-level pnpm devturbo run dev --env-mode=loosechain:devpnpm run sequencer:dev → the actual node ./src/start.ts ... command.
  • There are at least three levels of process indirection (turbo → pnpm → node-with-loaders). One of them appears to be interfering with tx submission, but we couldn't pin which.

Things we ruled out:

  • Build failure (build is clean in both modes).
  • GraphQL schema mismatch (introspection succeeds, mutations are discoverable).
  • Module registration (debug log shows all modules registered correctly under both invocations).
  • Auth/signing errors (no errors logged anywhere; the test client's .send() resolves without throwing).
  • Env variables (the same env vars are exported in both cases; loose mode is on).

A guess worth testing: turbo may be capturing or modifying stdio in a way that breaks the GraphQL transport between the node test client and the sequencer's mempool. But this is speculation.

Impact

For anyone using the starter as-is (pnpm dev) and running tests against it, every test that exercises chain mutations will silently fail. The tests will appear to "run" — assertions just fire against empty state — and the failure mode is consistent (everything returns null/0). It took us several hours of debugging code that turned out to be fine before we suspected the wrapper.

Suggestions

A startup banner from the chain process saying "submitting txs via X; using transport Y" would help users confirm the path is working. Or: a simple chain:dev: warning: 30 blocks produced with 0 txs and 0 pending in mempool — is your test client connecting to the right endpoint? periodic check would have saved us. Anything that breaks the silence.

Happy to share more detail / repo state / chain logs if helpful. Real working example of the starter + custom modules running under the direct-node invocation is at https://github.com/zkokio/minalia-protokit.

— Pete (zkokio), MINALIA

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions