Version: zallet 0.1.0-beta.1 (rebuilt against zebra-rpc 11.1.0), zebra backend,
zcash_client_backend 0.24.0-rc.1, testnet.
Summary
A wallet created from scratch — no migrate-zcashd-wallet — reaches a state where it holds N ZEC
of spendable transparent funds and cannot pay N ZEC in one transaction: the funds sit on
several of its own addresses and no z_sendmany source can gather them. The wallet creates this
condition itself, on every payment.
Mechanics
Change fragments by design. Transparent change goes to the next available internal address —
reserve_next_n_internal_addresses(account_id, …) in zcash_client_backend/src/data_api/wallet.rs:
"Add any transparent change outputs, sending them to the next available internal-scope (change)
transparent addresses of the account". GreedyInputSelector also stops once the target is covered,
leaving tails behind. After a few rounds the funds are spread over A/B/C/D…
No source gathers them. fromaddress accepts only "ANY_TADDR", a t-addr, a z-addr, or a UA
(Address::decode — an account UUID is rejected, contrary to what one might expect):
| source |
policy |
result |
| bare t-addr |
TransparentSpendPolicy::from_one_address |
that address's UTXOs only |
| UA |
SpendPolicy::default() → shielded-only |
asserted by your own shielded_source_permits_no_transparent_spending test — "even though the unified address it names does carry a transparent receiver" |
"ANY_TADDR" |
any_account_addr() — does gather |
but only via get_legacy_pool_account() |
Across all of Zallet, TransparentSpendPolicy is constructed in exactly two places:
z_send_many.rs:87 (from_one_address) and z_send_many.rs:110 (any_account_addr, inside
legacy_pool_spend_policy). TransparentSpendPolicy::from_addresses(…), which accepts a set of
addresses, is never called.
get_legacy_pool_account() requires both features.legacy_pool_seed_fingerprint and an
account at ZIP 32 index 0x7FFFFFFF — is_legacy_pool_account =
seed_fingerprint == legacy_seed_fp && account_index == ZCASH_LEGACY_ACCOUNT. A clean wallet's
account is index 0, and "a regular account of the legacy seed is therefore not the legacy pool".
The capability is not missing from the backend: any_account_addr() is documented as "non-coinbase
UTXOs from arbitrary transparent receivers belonging to the account", with no legacy requirement.
Only the RPC layer gates it.
Why it matters
We run a mining pool. Miners are paid transparent→transparent, and that is not negotiable: most
payout addresses are exchange deposits, and exchanges block a deposit whose transaction has shielded
inputs. So the path is coinbase → z_shieldcoinbase → unshield to our own t-addr → one z_sendmany
to all payees. Splitting the payout is not an option either — ZIP 317 charges per output, and a
500-payee round is expensive enough once.
Real state after a single payout: 151.43815934 on the internal change address, 0.1 on the public
receiver, 0 on the address our config names. A 170 ZEC round is unpayable while the wallet reports
the total as spendable.
Workaround, verified end-to-end
is_legacy_pool_account checks only the seed fingerprint and the ZIP 32 index, not provenance — so a
clean wallet can manufacture the legacy pool:
zallet rpc z_recoveraccounts '[{"name":"LEGACYPOOL","seedfp":"<this wallet's own seedfp>","zip32_account_index":2147483647,"birthday_height":<tip>}]'
# then in zallet.toml: features.legacy_pool_seed_fingerprint = "<the same seedfp>"
Before the option, "ANY_TADDR" → -11 "The legacy pool of funds is disabled". After it, it
resolves. With the account's funds split across four addresses — 3.0 / 2.0 / 2.0 / 0.99985,
the last being a change address the wallet itself created — a single z_sendmany "ANY_TADDR" of
7 ZEC, more than any one address holds, succeeded:
txid a9efd22b75628c340bb7c1cee6dff7b1bd31cc2a5581526d3a28b7ce2c396429
transparent inputs : 4
transparent outputs: 2
So the capability works perfectly well for an ordinary HD account. The only thing standing between a
clean wallet and it is the 0x7FFFFFFF index check.
Ask
Let a regular account spend its own transparent pool — any of:
- allow
any_account_addr() for a normal account (an explicit account/UA source opting into
transparent selection, guarded by AllowLinkingAccountAddresses, which already governs the
linkage);
- expose
TransparentSpendPolicy::from_addresses(…) through z_sendmany (accept a list in
fromaddress);
- or drop the
0x7FFFFFFF condition so legacy_pool_seed_fingerprint alone enables one-pool
semantics for the named seed.
Otherwise please bless the workaround above and document it — but as it stands, the documented modern
semantics ("each account is a separate pool of funds … the semantics used throughout Zallet, and
that should be used by everyone going forward") are unusable for transparent funds: the wallet
scatters its change and then refuses to gather it unless you pretend to be an imported zcashd wallet.
Version:
zallet 0.1.0-beta.1(rebuilt againstzebra-rpc 11.1.0), zebra backend,zcash_client_backend 0.24.0-rc.1, testnet.Summary
A wallet created from scratch — no
migrate-zcashd-wallet— reaches a state where it holds N ZECof spendable transparent funds and cannot pay N ZEC in one transaction: the funds sit on
several of its own addresses and no
z_sendmanysource can gather them. The wallet creates thiscondition itself, on every payment.
Mechanics
Change fragments by design. Transparent change goes to the next available internal address —
reserve_next_n_internal_addresses(account_id, …)inzcash_client_backend/src/data_api/wallet.rs:"Add any transparent change outputs, sending them to the next available internal-scope (change)
transparent addresses of the account".
GreedyInputSelectoralso stops once the target is covered,leaving tails behind. After a few rounds the funds are spread over A/B/C/D…
No source gathers them.
fromaddressaccepts only"ANY_TADDR", a t-addr, a z-addr, or a UA(
Address::decode— an account UUID is rejected, contrary to what one might expect):TransparentSpendPolicy::from_one_addressSpendPolicy::default()→ shielded-onlyshielded_source_permits_no_transparent_spendingtest — "even though the unified address it names does carry a transparent receiver""ANY_TADDR"any_account_addr()— does gatherget_legacy_pool_account()Across all of Zallet,
TransparentSpendPolicyis constructed in exactly two places:z_send_many.rs:87(from_one_address) andz_send_many.rs:110(any_account_addr, insidelegacy_pool_spend_policy).TransparentSpendPolicy::from_addresses(…), which accepts a set ofaddresses, is never called.
get_legacy_pool_account()requires bothfeatures.legacy_pool_seed_fingerprintand anaccount at ZIP 32 index
0x7FFFFFFF—is_legacy_pool_account=seed_fingerprint == legacy_seed_fp && account_index == ZCASH_LEGACY_ACCOUNT. A clean wallet'saccount is index 0, and "a regular account of the legacy seed is therefore not the legacy pool".
The capability is not missing from the backend:
any_account_addr()is documented as "non-coinbaseUTXOs from arbitrary transparent receivers belonging to the account", with no legacy requirement.
Only the RPC layer gates it.
Why it matters
We run a mining pool. Miners are paid transparent→transparent, and that is not negotiable: most
payout addresses are exchange deposits, and exchanges block a deposit whose transaction has shielded
inputs. So the path is coinbase →
z_shieldcoinbase→ unshield to our own t-addr → onez_sendmanyto all payees. Splitting the payout is not an option either — ZIP 317 charges per output, and a
500-payee round is expensive enough once.
Real state after a single payout:
151.43815934on the internal change address,0.1on the publicreceiver,
0on the address our config names. A 170 ZEC round is unpayable while the wallet reportsthe total as spendable.
Workaround, verified end-to-end
is_legacy_pool_accountchecks only the seed fingerprint and the ZIP 32 index, not provenance — so aclean wallet can manufacture the legacy pool:
Before the option,
"ANY_TADDR"→-11 "The legacy pool of funds is disabled". After it, itresolves. With the account's funds split across four addresses —
3.0/2.0/2.0/0.99985,the last being a change address the wallet itself created — a single
z_sendmany "ANY_TADDR"of7 ZEC, more than any one address holds, succeeded:
So the capability works perfectly well for an ordinary HD account. The only thing standing between a
clean wallet and it is the
0x7FFFFFFFindex check.Ask
Let a regular account spend its own transparent pool — any of:
any_account_addr()for a normal account (an explicit account/UA source opting intotransparent selection, guarded by
AllowLinkingAccountAddresses, which already governs thelinkage);
TransparentSpendPolicy::from_addresses(…)throughz_sendmany(accept a list infromaddress);0x7FFFFFFFcondition solegacy_pool_seed_fingerprintalone enables one-poolsemantics for the named seed.
Otherwise please bless the workaround above and document it — but as it stands, the documented modern
semantics ("each account is a separate pool of funds … the semantics used throughout Zallet, and
that should be used by everyone going forward") are unusable for transparent funds: the wallet
scatters its change and then refuses to gather it unless you pretend to be an imported zcashd wallet.