Known limitations
Current limitations and their workarounds, plus the future work each one points at. Intentional design boundaries (what zecd will never do) are on the compatibility boundary page; this page is about gaps that may close.
listunspent outpoints are synthesized
Shielded notes are not bitcoin-style outpoints, so listunspent reports each unspent note
with a synthesized (txid, vout) identifying the shielded action that created it, and no
transparent scriptPubKey. The address field is the diversified address the note was
received on when recorded, and empty for change/internal notes. Treat the pair as a stable
opaque identifier for dedupe, not as something you can feed to transparent-UTXO tooling. See
wallet history and unspent.
Shielding is explicit, never automatic
Since 0.6.1 a received transparent UTXO can be moved into the shielded pool, by naming a
transparent source on z_sendmany under the
AllowRevealedSenders privacy policy. What is still missing:
- No auto-shielding. Nothing sweeps transparent receives into the shielded pool on its own;
every shield is a call an operator or an integration makes. Wiring librustzcash's
propose_shieldinginto a caught-up sync pass is the planned path to a background sweep. Coinbase keeps its own route,z_shieldcoinbase, because consensus leaves it no other one: a transaction spending transparent coinbase may not have any transparent output, change included. - No mixed inputs. Transparent UTXOs and shielded notes cannot fund a single send together.
One source funds one send, so a shortfall on the named source is
-6rather than a top-up from the other pool. - No per-address shielded coin control. Notes are account-scoped, so a shielded
fromaddressnames the account rather than selecting that address's notes. Per-address selection works on the transparent side only.
Under the default policy a transparent-only wallet's sendtoaddress/sendmany still
returns -6: those methods take no fromaddress, so they only ever spend shielded notes.
No transparent receive reconciliation pass
Transparent receive discovery is a forward-only block-scan matcher, bounded by which
addresses are exposed at scan time. A receive on an address exposed only after its funding
block was scanned (out-of-order funding within the gap, with a small transparent_gap_limit)
is missed until a from-seed rescan. The planned follow-up is a periodic reconciliation pass
that batches all exposed addresses into Zebra's always-on transparent address index
(getaddressbalance/getaddressutxos) to cross-check the scanned balance and backfill
anything missed, kept off the per-block hot path. Workaround today: set [pools] transparent_initial_scan to your issuance high-water mark so the whole issued range is
pre-exposed before scanning, and keep transparent_gap_limit small, covering only the addresses
you have handed out that are still unfunded. Depth belongs to the initial scan, not the gap: the
gap window is re-derived on every recorded receive, so a large one stalls the scan (zecd warns
above 1000 and logs an error above 10000). See transparent support.
One account per wallet
Each wallet surfaces exactly one ZIP-32 account (the first in its database);
multi-account-per-seed is not exposed, and Bitcoin Core's legacy string-account API is not
implemented. Workaround: use multiwallet. Each [wallets.<name>] entry is an independent
seed, database, and directory, addressed bitcoind-style at POST /wallet/<name> (see
multiwallet routing). Note the constraint that at most one loaded wallet may
hold spending keys; the rest must be watch-only.
Per-wallet send throughput is one actor
Sends to one wallet serialize on its single-writer actor (the cs_wallet analog), so
per-wallet throughput is one core's worth of Orchard proving. [spend] pipeline_proving
(default off) addresses the liveness half only: it runs a send's prove-and-sign off the
actor, so a long send no longer freezes background sync, reads of status, and mempool
processing for its whole duration. Sends still serialize (at most one uncommitted transaction
at a time), so it does not raise multi-send throughput. It engages only on the cached-Orchard
PCZT proving path (cache_proving_key = true, the default). True concurrent sends
(disjoint-note selection across in-flight sends) remain a design proposal, not something zecd
implements. Workaround: shard the hot float across multiple wallets; K actors already overlap
their proofs across cores with no shared state.
No -rpcthreads worker pool
bitcoind processes RPC on a configurable thread pool (-rpcthreads, default 16) in front of
a bounded queue (-rpcworkqueue, default 64). zecd does not replicate the pool model;
requests run on the async runtime, and the [rpc] work_queue semaphore (default 100)
provides the same user-visible bound: beyond it the server returns HTTP 503 Work queue depth exceeded, as bitcoind does when its queue fills. There is no thread-count knob to tune. See
conventions and wire format.
help introspection is a stub
help returns a static one-line summary and ignores its optional command argument, where
bitcoind lists every command and returns per-method usage for help <method>. Tooling that
discovers a node's surface by introspecting help gets nothing useful from zecd. Workaround:
the method index is the authoritative surface list, and probing a
method directly distinguishes implemented (any non--32601 response) from absent (-32601,
HTTP 404). One caveat: with an [rpc] allowed_methods safelist configured, a method blocked by
the safelist also returns -32601, so probing cannot tell a disabled method from an absent one
(the safelist deliberately discloses nothing about the surface it hides).
PostgreSQL wallet backend is blocked upstream
The wallet store is SQLite only (zcash_client_sqlite). The one structural coupling blocking
an alternative backend is in reorg recovery: perform_rewind in src/sync/engine.rs must
match the concrete SqliteClientError::RequestedRewindInvalid error to retry a truncation at
a shallower bound, because zcash_client_backend's WalletWrite trait has no portable
"rewind invalid" error contract. Until upstream grows one (the TODO(upstream) on
perform_rewind tracks it), a PostgreSQL WalletDb backend cannot be wired in without
losing correct reorg recovery. No workaround; scale reads via the WAL-mode short-lived read
connections zecd already uses (see architecture).