Skip to content

compact-deploy fails on preprod: dust-sync OOM and strict isSynced never completes (pins dust-wallet@4.0.0) #115

@0xisk

Description

@0xisk

Deploying any contract on preprod with compact-deploy fails before a transaction is ever submitted. The wallet first crashes with a JS-heap OOM during dust sync, and once that is worked around the sync gate never completes. Both problems trace back to @openzeppelin/compact-deployer@0.0.1 pinning the pre-fix @midnight-ntwrk/wallet-sdk-dust-wallet@4.0.0.

This is the deployer-side manifestation of midnightntwrk/midnight-wallet#425, which was fixed upstream in dust-wallet@4.1.0 (PRs midnightntwrk/midnight-wallet#442 and #450).

Note: the affected package is shipped as a vendored tarball (@openzeppelin/compact-deployer + @openzeppelin/compact-cli) and isn't under packages/ in this repo today. Filing here per the tooling umbrella; please redirect if it tracks elsewhere.

Environment

  • @openzeppelin/compact-deployer@0.0.1 (with compact-cli, compact-deploy bin)
  • Network: preprod (https://rpc.preprod.midnight.network, indexer https://indexer.preprod.midnight.network/api/v4/graphql)
  • Node v24.15.0; local proof server on :6300
  • SDK pins: wallet-sdk-dust-wallet@4.0.0, wallet-sdk-facade@4.0.0, wallet-sdk-shielded@3.0.0, wallet-sdk-unshielded-wallet@3.0.0, ledger-v8@8.0.3, testkit-js@4.1.0

📝 Details

Three distinct issues, in the order they surface:

1. Dust sync OOMs: no batchUpdates, on the leaky dust-wallet@4.0.0.

preprod's dust history is ~1M events (dustLedgerEvents(id: 0) is a global, unfiltered stream every client must replay). The deployer builds the wallet without setting the wallet-sdk batchUpdates knob, so dust sync uses the SDK default batch size of 10 and dust-wallet@4.0.0 exhausts the V8 heap mid-replay:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

This is exactly midnightntwrk/midnight-wallet#425. The upstream fix is dust-wallet@4.1.0; the documented workaround is batchUpdates: { size: 5000, timeout: 1, spacing: 4 } (Ura labs validated ~146 MB peak, ~1000 events/sec at size 5000).

The knob must be set on the config used by wallet/handler.js (the cache-restore + fresh-build path, around the line const config = builderForConfig.config). buildDustConfig spreads that config into DustWallet(config).restore(...), so injecting config.batchUpdates there covers both the restore and fresh-sync paths, shielded and dust. Setting it only on wallet/build-deployer.js is not enough: when an on-disk snapshot exists, handler.js builds the dust wallet on a separate config and the workaround is bypassed, OOMing within ~90 seconds.

2. Sync gate uses strict isSynced, which never completes on a live chain.

The syncAndVerifyFunds gate in deployer.js waits on:

Rx.filter((s) => s.isSynced)

FacadeState.isSynced requires shielded, dust and unshielded to each be isStrictlyComplete() (applyGap === 0). On a live network the dust stream advances continuously, so the dust wallet stays 1-2 events behind the tip indefinitely and isSynced never flips true. The gate then times out (--sync-timeout) even though the wallet is fully usable. The SDK already exposes SyncProgress.isCompleteWithin(maxGap) (default 50) for this, and the unshielded wallet uses it internally. Switching the gate to a tolerant check lets sync complete the instant the wallet reaches the tip:

Rx.filter((s) =>
  s.shielded.state.progress.isCompleteWithin(50n) &&
  s.dust.state.progress.isCompleteWithin(50n) &&
  s.unshielded.progress.isCompleteWithin(50n))

3. Node default heap is the binding cap.

Even with batching, the restored dust tree plus shielded trial-decryption spikes past V8's ~2 GB default old-space (the host had 30 GB free). NODE_OPTIONS=--max-old-space-size=8192 was needed to finish. Worth documenting, or setting a sane --max-old-space-size default in the compact-deploy bin.

What worked once patched. With all three applied locally, ShieldedFungibleToken deployed cleanly on preprod (block 1236787) after a ~24 min dust sync. The state-snapshot resume (.states/*.gz, checkpointed every 5 min) is a nice touch: the retry resumed dust at ~493k/1.06M instead of from zero. The v4 indexer, args/seed/signing-key resolution, and the submit all worked.

Suggested fix. Rebuild the deployer against dust-wallet@4.1.0 (removes the OOM and the need for the batchUpdates workaround) and change the sync gate from isSynced to isCompleteWithin. Optionally raise or document the Node heap limit.

🔢 Code to reproduce bug

# compact.toml with a [networks.preprod] block (indexer api/v4) and any
# [contracts.X] entry, plus a prefunded preprod seed file.
compact-deploy <Contract> \
  --network preprod \
  --config ./compact.toml \
  --seed-file ./deploy/preprod.seed \
  --proof-server http://127.0.0.1:6300 \
  --sync-timeout 5400 -v

# Observed: dust sync climbs for a bit, then:
#   FATAL ERROR: ... JavaScript heap out of memory
#
# After forcing batchUpdates={size:5000,...} and raising --max-old-space-size,
# sync reaches the tip but the strict `isSynced` gate never fires, so
# --sync-timeout eventually trips.

Any prefunded preprod seed reproduces it (per #425 the dust commitment tree is global, so every wallet replays the same ~1M-event history).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions