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).
Deploying any contract on preprod with
compact-deployfails 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.1pinning 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).Environment
@openzeppelin/compact-deployer@0.0.1(withcompact-cli,compact-deploybin)https://rpc.preprod.midnight.network, indexerhttps://indexer.preprod.midnight.network/api/v4/graphql)v24.15.0; local proof server on:6300wallet-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 leakydust-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-sdkbatchUpdatesknob, so dust sync uses the SDK default batch size of 10 anddust-wallet@4.0.0exhausts the V8 heap mid-replay:This is exactly midnightntwrk/midnight-wallet#425. The upstream fix is
dust-wallet@4.1.0; the documented workaround isbatchUpdates: { 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 lineconst config = builderForConfig.config).buildDustConfigspreads that config intoDustWallet(config).restore(...), so injectingconfig.batchUpdatesthere covers both the restore and fresh-sync paths, shielded and dust. Setting it only onwallet/build-deployer.jsis not enough: when an on-disk snapshot exists,handler.jsbuilds 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
syncAndVerifyFundsgate indeployer.jswaits on:FacadeState.isSyncedrequires shielded, dust and unshielded to each beisStrictlyComplete()(applyGap === 0). On a live network the dust stream advances continuously, so the dust wallet stays 1-2 events behind the tip indefinitely andisSyncednever flips true. The gate then times out (--sync-timeout) even though the wallet is fully usable. The SDK already exposesSyncProgress.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: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=8192was needed to finish. Worth documenting, or setting a sane--max-old-space-sizedefault in thecompact-deploybin.What worked once patched. With all three applied locally,
ShieldedFungibleTokendeployed 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 thebatchUpdatesworkaround) and change the sync gate fromisSyncedtoisCompleteWithin. Optionally raise or document the Node heap limit.🔢 Code to reproduce bug
Any prefunded preprod seed reproduces it (per #425 the dust commitment tree is global, so every wallet replays the same ~1M-event history).