[V135] ZEC: stabilize signing, expiry and reorg recovery
Summary
ZEC validators can construct different signing candidates when they use different local chain heights or UTXO views. Retries can also become inconsistent if they refresh transaction parameters or lose the checkpoint associated with saved signed bytes.
This MR pins signing attempts to shared historical state, preserves their original parameters across retries, and treats definitive expiry as an explicit terminal outcome. It also improves reorg handling and extends lifecycle coverage to MAYA’s transparent and shielded transactions.
What this adds beyond !717 (merged)
This MR adds stable signing attempts and recovery after expiry on top of !717 (merged)’s input-selection, reorg, and broadcast safeguards.
| Area | Already in !717 (merged) | Added by !856 (merged) |
|---|---|---|
| Signing heights | Expiry based on each validator’s local ZEC height | Use a shared historical ZEC reference; keep transaction parameters and TSS signing height fixed |
| Failed signing and retries | Persist successful signed transactions before broadcast | Also preserve checkpoints and any completed payload when signing returns an error |
| Expiry handling | Set expiry and align cache cleanup | Check whether the transaction confirmed, retire definitively expired attempts, and await MAYA rescheduling without recording false success |
| Reservations and consolidation | Input reservations, selection rules, signing halts | Persist reservation expiry and consolidation attempts across restarts, with guarded recovery |
| Shared ZEC height | Existing height tracking | Update it through fee quorums every ten ZEC blocks, plus a V133 migration to seed it |
| Verification | Input selection, reorg and persistence tests | Broader lifecycle/native tests and the passing four-validator expiry → retirement → replacement scenario |
The central benefit: validators use the same signing reference, retries preserve the original attempt, and an expired attempt can be retired so the existing MAYA rescheduler can produce a replacement.
The bounded reorg recovery, stable input ordering, dust validation, and basic persist-before-broadcast protection are inherited from !717 (merged).
Signing and input selection
- Derive the ZEC target height and expiry from the agreed ZEC height recorded at the outbound’s committed MAYA height. Both values remain fixed for the attempt.
- Pin TSS round selection to the same MAYA signing height. Separate signing batches by vault, algorithm and pinned height.
- Preserve the original checkpoint and any completed signed payload across failures and restarts. Persist signed bytes before broadcasting and replay those exact bytes on retry.
- Select confirmed live UTXOs in a stable order: creation height, transaction ID and output index.
- For ordinary payments, select only enough inputs to fund the payment and fee. Consolidation retains its configured input-count target.
- Defer a selected plan when local spent or reserved records conflict with it, rather than substituting different inputs.
- Keep confirmed change eligible when it was created after the pinned reference, avoiding stalls in queued payments. Preserve the confirmed vault-dust exception with transaction-script validation.
- Resolve historical signing state once per fresh attempt and reuse selected consolidation inputs, preserving checkpoint and retry safeguards.
Expiry, reservations and consolidation
- Introduce a typed terminal expiry result and handle it explicitly in the shared signer. Retiring an expired attempt never creates a success observation.
- Before retiring an expired signed transaction, reconcile its canonical Zcash transaction ID. A confirmed transaction is recognized; unavailable or ambiguous RPC responses remain retryable.
- Validate serialized expiry and consensus-branch information, including v5/v6 wire headers.
- Persist reservation ownership and expiry across restarts. Retrying an attempt does not refresh its lifetime. Preserve legacy zero-expiry checkpoints.
- Journal consolidation checkpoints before signing and signed payloads before broadcasting.
- Allow a fresh self-consolidation attempt to be discarded only when every input demonstrably failed before joining a signing party. Restored attempts and failures that may have exposed signing shares retain their original checkpoint.
- Honor both global and ZEC-specific signing halts for internal consolidation.
Activation and agreed-height updates
The new signing policy activates at network version 1.133.0. Bifrost gates retain the earlier policy for preactivation attempts during a rolling deployment. Each gate has a cleanup TODO describing when it can be removed.
Quorum-approved ZEC network-fee reports now advance LastChainHeight, including when fees are unchanged. The periodic reporting interval changes from 100 to 10 ZEC blocks.
The V133 migration seeds LastChainHeight from the highest ZEC height already approved by a fee quorum, preserving any higher existing value. This runs before activation-block outbound scheduling and reduces dependence on a stale inbound-only height.
The migration follows the existing convention: it logs failures locally and allows the store version to advance. A failed seed is not automatically retried; subsequent fee quorums provide the normal refresh path. Stale or absent approved reports still require a fresh quorum, and existing historical outbound snapshots remain unchanged.
Reorg handling and supporting changes
- Emit reorg errata with the ZEC chain identifier.
- Bound reconfirmation by the configured rescan window and stop at the common ancestor.
- Avoid issuing errata for inconclusive RPC failures and reuse fetched canonical block data.
- Make REST query height request-local and expose the actual queried height so historical signing queries can validate their response.
- Fix shared-state synchronization in the signer constants provider and TSS version cache.
- Extend Rust/Go bindings for canonical transaction identity and explicit shielded expiry.
- Rebuild production and simulation ZEC archives with Rust 1.98.1.
- Keep the isolated Zebra and validator-churn harnesses optional.
THORNode implementation parity
This MR adapts THORNode’s ZEC signing and retry behavior to MAYA’s dedicated Zcash client and Rust-backed transaction builder.
The primary reference is THORNode !5058, merged as e34c4d416269. The test comparison is documented in test/simulation/ZEC_TEST_COVERAGE.md.
Behavior ported or mirrored from THORNode
- Historical signing reference: derive expiry from the agreed ZEC height at the outbound’s fixed consensus height. Reject unavailable or mismatched historical responses and preserve the original parameters across retries.
- Expiry lifecycle: check expiry before and after signing, reconcile confirmation before retiring an expired broadcast, and return a typed terminal outcome. Expired attempts leave the signer queue without being automatically observed as successful.
- Agreed-height refresh: report network fees every ten ZEC blocks, including when the fee is unchanged.
- Height-aware TSS batching: mirror THORNode’s existing separation of signing tasks by vault, algorithm and scheduled signing height.
- Simulation reliability: use sequential mining, wait for chain readiness and confirmed funding, and recover ambiguous broadcasts using the original signed bytes and transaction-specific acceptance checks.
- Regression coverage: adapt THORNode’s historical-height, checkpoint, expiry-boundary, terminal-expiry, broadcast-retry and isolated transparent-funding tests.
Companion confirmed-input fix
The confirmed-input ordering, sufficient-prefix selection, local-conflict deferral and vault-dust validation mirror the companion THORNode change on zly/zec-confirmed-inputs, commit 4c6eee48458d.
This is a separate follow-up to !5058. MAYA’s core selection policy is inherited through !717 (merged); this MR integrates it with historical signing references and the broader transaction lifecycle.
MAYA-specific adaptations and additions
- Preserve MAYA’s 35-ZEC-block expiry policy and compatibility with legacy zero-expiry checkpoints.
- Apply the lifecycle to transparent, Sapling and unified-address payments, including Rust-built v5 transactions and canonical ZIP-244 transaction IDs.
- Persist consolidation attempts across retries and restarts, with guarded recovery from failures before signing-party formation.
- Add MAYA’s
1.133.0activation gates, network-fee height updates and store migration. - Extend live tests with shielded receipts, Sapling/Orchard round trips and exact signed-payload replay.
- Add the optional four-to-five-validator churn harness and supporting storage, RPC and concurrency fixes.
Verification
| Area | Recorded result |
|---|---|
| Go packages | Full Zcash and shared UTXO tests passed during implementation, with targeted bridge, signer, TSS, REST and simulation-package checks |
| Latest activation migration | Store-manager and fee-handler suites passed under mocknet and mainnet builds: 13 tests each |
| Formatting and lint | Configured Trunk checks passed for the full port and subsequent changed scopes; final formatting and whitespace checks passed |
| Native bindings | Source-hash and binding-contract verification passed; both archives carry Rust 1.98.1 provenance |
| ZEC E2E harness | All four Rust/Go FFI flows passed, including shielded round trips and lost-response retries |
| Isolated real Zebra | 40 transparent and four shielded payments passed with recipient, amount and retry assertions |
| Race and regression checks | Targeted race checks and two network-fee regression fixtures passed |
THORNode lifecycle and retry tests were adapted for MAYA’s historical signing references, 35-block expiry policy, Sapling receivers and unified addresses.
Multi-chain simulation results
- One validator: an earlier complete run passed all 210 actors. This predates the later input-selector and activation changes.
- Four validators: expiry simulation passed. All four signers retired the unconfirmed expired attempt, and Zebra rejected its original signed payload specifically for expiry. MAYA rescheduled the payment after 300 blocks, and the replacement confirmed and was observed. After another 600 MAYA blocks, the recipient had exactly one payment and no pending outbound remained. This verifies a transparent Asgard payment with a fixed validator set and expiry before rescheduling.
Operational considerations and remaining coverage
- Validators need sufficient historical MAYA state for signing queries. Pruning everything is incompatible with this policy.
- More frequent fee reports increase observation traffic and potential slash-point exposure for lagging scanners.
- Confirmed live-input selection still permits disagreement when necessary inputs appear or disappear between RPC reads. Cross-vault membership changes can also affect dust eligibility.
- The existing timing overlap remains: 35 ZEC blocks are approximately 44 minutes, while 300 MAYA blocks are approximately 25 minutes. MAYA can therefore reschedule while an earlier broadcast remains valid. This MR does not change that policy.