Skip to main content

Debugging the Midnight SDK: Signing Bugs, Network IDs, and a Deploy Pipeline

· 4 min read
Frederico Santana
Founder & Technical Writer, DPO2U

The Midnight SDK documentation says "deploy your contract." It does not mention the two hours you'll spend figuring out why signRecipe() silently corrupts your transaction intent, or why every contract call fails because the network ID defaults to 'undeployed'.

The setup

DPO2U's first Midnight contract — ComplianceRegistry.compact — compiles to zero-knowledge circuits via Halo2. The deploy pipeline should be straightforward: compile the Compact source, generate prover/verifier keys, create a wallet, sign the deployment transaction, and submit it to the Midnight Preprod network.

In practice, the pipeline exposed two SDK bugs that cost me an afternoon of debugging and a complete rewrite of the signing logic.

Bug #1: signRecipe corrupts the intent

The Midnight Wallet SDK provides wallet.signRecipe() as the recommended way to sign deployment transactions. The problem: it silently mutates proof markers inside the transaction intent. The resulting signed transaction is structurally valid but semantically wrong — the proof server rejects it because the markers no longer match the expected circuit layout.

The fix came from studying the example-counter repository — Midnight's official reference implementation. Instead of signRecipe(), you extract the raw intent, sign it manually, and reconstruct the transaction envelope:

// Don't use wallet.signRecipe() — it corrupts proof markers
const intent = deployTx.intent;
const signedIntent = await wallet.sign(intent);
const tx = { ...deployTx, signedIntent };

This is not documented anywhere. The example-counter repository quietly uses this pattern without explaining why.

Bug #2: network ID defaults to 'undeployed'

The midnight-js-network-id module initializes with the value 'undeployed'. If you don't explicitly call setNetworkId('preprod') before any contract operation, every RPC call silently targets a nonexistent network. No error is thrown — the calls simply return empty results or hang indefinitely.

import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';

// This MUST be called before any wallet or contract operation
setNetworkId('preprod');

I lost 40 minutes to this because the wallet sync phase appeared to work — it just returned zero balances on a wallet that had tDUST from the faucet.

The deploy pipeline

After fixing both bugs, the pipeline stabilized into a predictable sequence:

Key timings from the pipeline:

StepDurationNotes
Wallet sync~76 secondsHD Wallet SDK syncs full UTXO set
ZK proof generation< 1 secondProof Server (Docker, local) is fast
TX submission~3 secondsWebSocket to rpc.preprod.midnight.network
Indexer confirmation~10 secondsGraphQL query confirms deployment

The wallet sync is the bottleneck. 76 seconds every time the script runs. There's no incremental sync — it rebuilds the entire UTXO state from genesis. For a deploy pipeline that runs once, this is acceptable. For an agent that needs to submit attestations frequently, it will need a persistent wallet process.

The RPC WebSocket blocker

One issue remains unresolved: the Midnight Preprod RPC endpoint (wss://rpc.preprod.midnight.network) occasionally drops WebSocket connections during proof submission. The connection establishes successfully, the proof is sent, and then — silence. No acknowledgment, no error, no timeout signal.

My current workaround is a 30-second timeout with automatic retry. It's ugly, but it works for testnet. For production, this will need a proper connection health monitor.

What I'd tell another developer

If you're starting with the Midnight SDK today:

  1. Read the example-counter source before the docs — the reference implementation is more accurate than the written documentation
  2. Call setNetworkId() first — before wallet creation, before contract deployment, before anything
  3. Don't use signRecipe() — use manual signing via wallet.sign() on the raw intent
  4. Run the Proof Server locallydocker run midnightntwrk/proof-server:7.0.0 eliminates network latency for proof generation
  5. Budget 2 minutes per script run for wallet sync — it's not a bug, it's the architecture

The SDK is beta software. Bugs are expected. What matters is that the underlying cryptography works — the ZK proofs are valid, the attestations are verifiable, and the privacy guarantees hold. The developer experience will improve. The math won't need to.

For the contract architecture behind ComplianceRegistry, see Smart Contracts. For the strategic context of why we're building on Midnight, see About DPO2U.