Debugging the Midnight SDK: Signing Bugs, Network IDs, and a Deploy Pipeline
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:
| Step | Duration | Notes |
|---|---|---|
| Wallet sync | ~76 seconds | HD Wallet SDK syncs full UTXO set |
| ZK proof generation | < 1 second | Proof Server (Docker, local) is fast |
| TX submission | ~3 seconds | WebSocket to rpc.preprod.midnight.network |
| Indexer confirmation | ~10 seconds | GraphQL 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:
- Read the
example-countersource before the docs — the reference implementation is more accurate than the written documentation - Call
setNetworkId()first — before wallet creation, before contract deployment, before anything - Don't use
signRecipe()— use manual signing viawallet.sign()on the raw intent - Run the Proof Server locally —
docker run midnightntwrk/proof-server:7.0.0eliminates network latency for proof generation - 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.
