You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
z_getbalances / getbalances reports transparent balance figures that are internally inconsistent with their own documented contract, and every RPC-visible balance number ends up either including un-spendable coinbase funds or omitting the transparent pool entirely. There is no RPC that correctly answers "how much can I currently pay to a transparent recipient?", which makes automated payout flows (e.g. mining pools) fail unpredictably.
structTransparentBalance{/// The transparent balance excluding coinbase outputs.regular:Option<Balance>,/// The transparent balance in coinbase outputs./// Omitted if no coinbase funds are present.coinbase:Option<Balance>,}
But the implementation (same file, L180-212) does not honor this:
// TODO: Separate out transparent coinbase and standalone balances.let transparent_regular = account.unshielded_balance();
...Ok(Some(TransparentBalance{regular:opt_balance_from(regular)?,coinbase:None,// never populated}))
regular is populated from AccountBalance::unshielded_balance(), which is the entire transparent balance including coinbase outputs — contradicting the doc comment. coinbase is hardcoded to None and is never populated regardless of actual coinbase holdings.
Separately, total.spendable (from balance_from → AccountBalance::spendable_value()) only covers the shielded pools, so the transparent balance never contributes to total at all.
Reproduction
Environment: Zallet 0.1.0-beta.1, zebra backend, Zebra v28, mainnet, mining pool wallet with coinbase paid to a wallet-owned t-address.
Every transparent UTXO was independently classified against the node (getrawtransaction <txid> 1 → vin[0].coinbase): two coinbase outputs of 1.25 ZEC each, plus one non-coinbase 1.24035 ZEC output (from a prior z_sendmany z→t). The real spendable-to-transparent balance is therefore 1.24035 ZEC.
regular = 8.74035 ZEC — the two coinbase outputs are folded in; the true non-coinbase balance is 1.24035 ZEC.
No coinbase key is present at all.
total.spendable is byte-identical to sapling.spendable — transparent contributes 0.
Reproduced on a second sample: transparent balance 102.49 ZEC, sapling 0.00985 ZEC → total.spendable reported as exactly 985000 zats (i.e. only the sapling balance).
Impact
Since coinbase outputs can only be shielded, never spent transparently, every currently available balance figure is wrong for the "can I pay this transparent recipient" decision:
z_gettotalbalance.transparent and z_getbalances.transparent.regular both include coinbase.
total.spendable ignores the transparent pool entirely.
Acting on any of these numbers and calling z_sendmany fails with Insufficient balance (have 0, need N including fee). This is what took down payouts on our pool: z_getbalances reported 102.49 ZEC available, the pool attempted to pay out, and z_sendmany immediately failed with have 0, halting the payout cycle.
Current workaround
Enumerate z_listunspent, then call getrawtransaction on every txid to identify and exclude coinbase outputs — one extra node round-trip per transaction, per payout cycle. z_listunspent itself carries no coinbase marker (zcashd's listunspent exposed a generated field for this).
Suggested fix
Either of:
Finish the // TODO: Separate out transparent coinbase and standalone balances. in get_balances.rs — likely needs AccountBalance (upstream, in zcash_client_backend) to carry the coinbase/non-coinbase split so regular/coinbase can be populated correctly, and total.spendable to include the non-coinbase transparent balance.
Lower-cost for integrators in the meantime: add a generated/coinbase flag to z_listunspent output entries, mirroring zcashd's listunspent, so callers can filter without an extra getrawtransaction round-trip per UTXO.
Reported by a mining pool operator running Zallet in production.
Summary
z_getbalances/getbalancesreports transparent balance figures that are internally inconsistent with their own documented contract, and every RPC-visible balance number ends up either including un-spendable coinbase funds or omitting the transparent pool entirely. There is no RPC that correctly answers "how much can I currently pay to a transparent recipient?", which makes automated payout flows (e.g. mining pools) fail unpredictably.Details
TransparentBalanceinzallet-core/src/components/json_rpc/methods/get_balances.rsdocuments:But the implementation (same file, L180-212) does not honor this:
regularis populated fromAccountBalance::unshielded_balance(), which is the entire transparent balance including coinbase outputs — contradicting the doc comment.coinbaseis hardcoded toNoneand is never populated regardless of actual coinbase holdings.Separately,
total.spendable(frombalance_from→AccountBalance::spendable_value()) only covers the shielded pools, so the transparent balance never contributes tototalat all.Reproduction
Environment: Zallet 0.1.0-beta.1, zebra backend, Zebra v28, mainnet, mining pool wallet with coinbase paid to a wallet-owned t-address.
Every transparent UTXO was independently classified against the node (
getrawtransaction <txid> 1→vin[0].coinbase): two coinbase outputs of 1.25 ZEC each, plus one non-coinbase 1.24035 ZEC output (from a priorz_sendmanyz→t). The real spendable-to-transparent balance is therefore 1.24035 ZEC.z_getbalancesreported:regular= 8.74035 ZEC — the two coinbase outputs are folded in; the true non-coinbase balance is 1.24035 ZEC.coinbasekey is present at all.total.spendableis byte-identical tosapling.spendable— transparent contributes 0.Reproduced on a second sample: transparent balance 102.49 ZEC, sapling 0.00985 ZEC →
total.spendablereported as exactly 985000 zats (i.e. only the sapling balance).Impact
Since coinbase outputs can only be shielded, never spent transparently, every currently available balance figure is wrong for the "can I pay this transparent recipient" decision:
getwalletinfois a stub (rpc: Implementgetwalletinfo#55).z_gettotalbalance.transparentandz_getbalances.transparent.regularboth include coinbase.total.spendableignores the transparent pool entirely.Acting on any of these numbers and calling
z_sendmanyfails withInsufficient balance (have 0, need N including fee). This is what took down payouts on our pool:z_getbalancesreported 102.49 ZEC available, the pool attempted to pay out, andz_sendmanyimmediately failed withhave 0, halting the payout cycle.Current workaround
Enumerate
z_listunspent, then callgetrawtransactionon every txid to identify and exclude coinbase outputs — one extra node round-trip per transaction, per payout cycle.z_listunspentitself carries no coinbase marker (zcashd'slistunspentexposed ageneratedfield for this).Suggested fix
Either of:
// TODO: Separate out transparent coinbase and standalone balances.inget_balances.rs— likely needsAccountBalance(upstream, inzcash_client_backend) to carry the coinbase/non-coinbase split soregular/coinbasecan be populated correctly, andtotal.spendableto include the non-coinbase transparent balance.generated/coinbase flag toz_listunspentoutput entries, mirroring zcashd'slistunspent, so callers can filter without an extragetrawtransactionround-trip per UTXO.Reported by a mining pool operator running Zallet in production.