SSkilltecabyclaudinhocode
Enviar skill
← Voltar para o catálogo

midnight-ops-doctor

DevOps e Infra

Diagnostic playbook for Midnight Network backend ops. Triggers on wallet sync stalls (appliedIndex, isStrictlyComplete, Custom error 139), deploy hangs (deployContract, watchForTxData), three-address confusion (Zswap coinPublicKey, NightExternal bech32m, Dust publicKey), DUST zero-balance and registerNightUtxosForDustGeneration, cloud-IP RPC 403 from AWS ELB on GCP/DO/AWS, AWS WAF 8KB submitTx (us

2estrelas
Ver no GitHub ↗Autor: samuelarogbonloLicença: MIT

midnight-ops-doctor

Notes from running a Midnight Network backend in production. Look up your symptom in the triage table, follow the link to the runbook.


Triage

Match the user's words against the left column. Load only the doc on the right. Don't pre-load others.

Dev says...Load
"stuck at X%", "wallet not syncing", "appliedIndex", "isStrictlyComplete"references/wallet-lifecycle.md § Sync stalls
"Custom error: 139", "transaction rejected by node", "Invalid Transaction"references/symptom-catalog.md § err-139
"deployContract hangs", "watchForTxData never returns", "deploy timed out"references/wallet-lifecycle.md § Six-phase deploy
"403", "WAF", "ELB", "blocked from my server", "GCP/AWS/DO + Midnight"references/network-chooser.md § Cloud-IP block
"wrong address", "faucet didn't arrive", "balance shows 0", "which address"references/three-addresses.md
"DUST", "NIGHT registration", "dust generation", "dustReceiverAddress", "tDUST"references/dust-night-registration.md
"verifier returns false", "vk mismatch", "Groth16 deploy verification"run scripts/deploy-verifier.mjs; for incident response read references/groth16-vk-mismatch.md
"persistentHash", "SHA256 doesn't match", "hashlock mismatch", "cross-family"references/cross-family-hashlocks.md
"ERR_UNSUPPORTED_DIR_IMPORT", "ESM in CJS", "@midnight-ntwrk import fails"references/symptom-catalog.md § cjs-esm
"submitTx timeout", "tx >8KB fails", "AWS WAF on RPC"references/symptom-catalog.md § waf-8kb
"snapshot won't restore", "GCS scope", "wallet warm-restart broken"references/symptom-catalog.md § snapshot-gcs
"what proof-server version", "ledger-v7 vs v8", "SDK compat"references/version-matrix.md

Routing rules:

  • One symptom, one doc. Never broadcast-load.
  • If the user describes two symptoms, fix the one that blocks the other first (sync before deploy, network before sync).
  • If no row matches, ask one clarifying question. Don't guess.

Three addresses

The single most-mis-applied concept in Midnight backend code. Inline here so triage doesn't require loading a reference doc.

One seed, three addresses

A single MIDNIGHT_SEED (32-byte or 64-byte hex) derives three distinct addresses through HDWallet.fromSeed(seed).selectAccount(0).selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust]).deriveKeysAt(0). The roles enum is exported by @midnight-ntwrk/wallet-sdk-hd. Each role yields a separate keypair with a different on-chain semantic.

import { HDWallet, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import {
  ZswapSecretKeys,
  DustSecretKey,
} from '@midnight-ntwrk/ledger-v8';
import { createKeystore, PublicKey } from '@midnight-ntwrk/wallet-sdk-address-format';
import { MidnightBech32m, DustAddress } from '@midnight-ntwrk/wallet-sdk-address-format';

const seed = Buffer.from(process.env.MIDNIGHT_SEED!, 'hex');
const hd = HDWallet.fromSeed(new Uint8Array(seed));
if (hd.type !== 'seedOk') throw new Error(`HD seed failed: ${hd.type}`);

const derived = hd.hdWallet
  .selectAccount(0)
  .selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust] as const)
  .deriveKeysAt(0);
if (derived.type !== 'keysDerived') throw new Error(`derive failed: ${derived.type}`);

const networkId = 'preview'; // or 'preprod' / 'mainnet'

const zswapKeys = ZswapSecretKeys.fromSeed(derived.keys[Roles.Zswap]);
const dustSecret = DustSecretKey.fromSeed(derived.keys[Roles.Dust]);
const unshieldedKs = createKeystore(derived.keys[Roles.NightExternal], networkId);

const shieldedCoinPublicKey = zswapKeys.coinPublicKey;                     // 32-byte hex
const unshieldedAddress = unshieldedKs.getBech32Address().toString();      // mn_addr_<net>1...
const dustAddress = MidnightBech32m
  .encode(networkId, new DustAddress(dustSecret.publicKey))
  .toString();                                                             // mn_dust_<net>1...

hd.hdWallet.clear();

Address shapes

RoleFormatExample
Roles.Zswap32-byte hex (no prefix in SDK; backend pads to 0x + 64 hex)4f1c...3a9b
Roles.NightExternalbech32mmn_addr_preview1qwerty... / mn_addr_preprod1...
Roles.Dustbech32mmn_dust_preview1... / mn_dust_preprod1...

Operation, address, why

OperationUse this addressWhy
Faucet POST address fieldunshielded bech32mFaucet drops land in the unshielded UTXO set
Lace "Receive" tab displayunshielded bech32mLace's UI is unshielded-default
Indexer balance queryunshielded bech32mPublic chain state is keyed on unshielded
Native NIGHT transfer (send)unshielded bech32mNIGHT lives unshielded
Shielded zswap tx, coinPublicKey API paramsshielded hex (Zswap)Shielded ledger uses coin public keys
HTLC sender / receiver Bytes<32> argshielded hex (Zswap)HTLC contract takes raw 32-byte keys
DUST balance lookup, dust-credit recipientdust bech32mDust accrual uses the dust public key
dustReceiverAddress arg to registerNightUtxosForDustGenerationdust bech32mDesignation targets the dust address

The getWalletAddress() trap

Many wallet adapter wrappers expose a getWalletAddress() method that returns ONLY the shielded coin public key (32-byte hex padded to 0x + 64 hex). Backend code that says "the wallet address" almost always means this one. It's correct for HTLC sender/receiver args, and wrong for everything balance-related (faucet, Lace, indexer).

When in doubt, expose all three on a diagnostics endpoint and pick by use case:

{
  shieldedCoinPublicKey: '0x4f1c...3a9b',
  unshieldedAddress: 'mn_addr_preview1...',
  dustAddress: 'mn_dust_preview1...',
}

Seed normalization gotcha

A 24-word BIP39 mnemonic can be normalized into a seed three ways. Only one matches Lace.

NormalizationBytesMatches Lace?
bip39.mnemonicToSeedSync(mnemonic, '') (PBKDF2 full)64YES
First 32 of PBKDF2 output32NO
bip39.mnemonicToEntropy() (BIP39 entropy)32NO

If your derived addresses don't match what Lace shows, you almost certainly used the wrong normalization. The bundled scripts/address-derive.mjs runs the canonical PBKDF2-full path.

For deeper diagnostics (faucet didn't arrive, balance still zero, address mismatch), load references/three-addresses.md.


Quick fixes

Top twelve errors. One-line diagnosis, minimum-viable fix. If the fix doesn't stick, escalate to the deeper doc.

Custom error: 139

Diagnosis: wallet submitted a transaction before chain sync completed. Node rejected stale UTXO inputs.

Fix:

await waitForWalletSyncState(WALLET_SYNC_TIMEOUT_MS, 'startup');
// only then: submitTx, deploy, lock, etc.

Deeper: references/wallet-lifecycle.md § Sync-completion check.

ERR_UNSUPPORTED_DIR_IMPORT from @midnight-ntwrk/*

Diagnosis: @midnight-ntwrk/* packages are ESM-only. CJS require() fails on bare-directory imports.

Fix:

// In a CJS file, use type-only static imports + dynamic import for runtime.
import type { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
const { WalletFacade } = await import('@midnight-ntwrk/wallet-sdk-facade');

Deeper: references/symptom-catalog.md § cjs-esm.

submitTx times out around 25-30s, tx is ~90 KB

Diagnosis: the SDK's HTTP submitTx posts to RPC. AWS WAF rejects bodies >8 KB. Deploy txs are typically 50-100 KB.

Fix: route submission through wallet.submitTransaction(tx). That uses the WebSocket relay (PolkadotNodeClient), bypassing the HTTP body limit.

const midnightProvider = {
  async submitTx(tx) { return wallet.submitTransaction(tx); },
};

Deeper: references/symptom-catalog.md § waf-8kb.

403 Forbidden from https://rpc.preprod.midnight.network (only on cloud VMs)

Diagnosis:

Como adicionar

/plugin marketplace add samuelarogbonlo/midnight-ops-doctor

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Comentários · Nenhum comentário

Entre para comentar. Entrar

  • Ainda não há comentários. Seja o primeiro.