[V130] store migration: reschedule stale ZEC outbounds from hardfork chain-split
Summary
v130 store migration that re-sends four ZEC swap outbounds which MAYAChain recorded as sent but which do not exist on the ZEC chain the network now follows. A ZEC hardfork chain-split (early June 2026) left those outbounds only on an abandoned fork, so the recipients were never paid — while the vault's ZEC was never actually spent on the canonical chain.
Problem
For each of the four swaps below, MAYAChain observed and recorded a ZEC outbound (the inbound voter has a matching OutTx, so the swap looks settled). But the outbound tx is absent from the canonical ZEC chain, so the recipient never received funds.
| # | Amount | Recipient | Inbound (voter) | Stale outbound |
|---|---|---|---|---|
| 1 | 1.00018984 ZEC | t1aLMFcHF6cvEjcBuZhWVy9j6mAbJdEWpih |
C2DBEE…A99A |
A5B57B9A…66C2 |
| 2 | 0.13111969 ZEC | t1NPPMW3Y7GJ6Yhab5AiUSy1LozVbfwUUYj |
B6673E…6945 |
98AAEC3E…4EB7 |
| 3 | 0.21402018 ZEC | u1n06w50jg…qy73x |
8243FE…3348 |
0698DED5…CABAC |
| 4 | 0.3012387 ZEC | u1j0q25fvn…qfxzf |
FD5540…52B6 |
4B086B37…F141 |
Verification (authoritative — the network's own zebra node)
Public explorers disagree (blockchair shows Tx3/Tx4 as confirmed), so they were not trusted. The only authoritative source is the chain bifrost observes — the zebra-daemon node — queried via RPC:
getrawtransaction <hash> 1→ "No such mempool or main chain transaction" for all four outbound hashes.- Positive control:
getrawtransactionof a tx that is in zebra's block 3363449 returns FOUND — so zebra's lookup works and the negatives are authoritative. getblock 3363449/getblock 3363445on zebra return blocks whose hashes differ from blockchair's at the same heights, and which do not contain the outbound txs. i.e. blockchair follows the abandoned fork; zebra follows canonical.
Since the outbounds never happened on zebra's chain, the vault UTXOs were never spent there — re-sending cannot double-spend.
Fix
New shared helper rescheduleStaleOutbounds (manager_store_common.go): for each affected inbound voter it
- removes the stale outbound from
OutTxs(so the ZECActionsitem becomes dangling), and clears the stale hash from the consensus tx and observationout_hashes(reopening their status); - marks the stale outbound's own
ObservedTxOutVoteras reverted (SetReverted); - credits the recorded
coin+gasback to an active asgard — because MAYAChain debited the vault when it observed the outbound, but the funds were never actually spent on the canonical chain; - calls the existing
requeueDanglingActions, which schedules a fresh outbound and setsvoter.OutboundHeight/FinalisedHeight.
Steps 2–3 mirror the proven precedent migrateStoreV113 ("Observed, but not paid txs"), which handled the identical situation. The difference is that this helper removes a specific stale outbound by hash (preserving any other outbounds on the voter) and restores exactly that outbound's coin+gas, rather than v113's clear-all OutTxs = nil / Actions[0] shortcut.
Setting OutboundHeight is important: it is exactly what the buggy v129 requeue (migrateStoreV129RequeueDroppedXRD) omitted, causing the replacement outbounds to be unmatched on observation and the signing vault to be falsely slashed (which migrateStoreV130CompensateInvalidXRDSlashes later had to compensate). This migration reuses the corrected requeueDanglingActions path, so the replacements are matched cleanly.
migrateStoreV130RescheduleStaleZECOutbounds (manager_store_chaosnet.go) supplies the four mainnet (inboundHash, staleOutHash) pairs and is wired into migrateStoreV130. Recipient and amount are taken from the preserved on-chain Actions item, not hardcoded — the migration only needs the inbound + stale-out hashes.
Vault accounting — verified against the live chain
The fund restoration was validated by comparing what MAYAChain tracks against the canonical ZEC chain (the network's own zebra-daemon, which 3xpl agrees with; blockchair follows the abandoned fork and was not trusted):
| Source | ZEC.ZEC | ZEC |
|---|---|---|
MAYAChain tracks (vaults/asgard) |
369,324,921,576 |
3693.24921576 |
Actually on-chain (zebra getaddressbalance) |
370,593,199,809 |
3705.93199809 |
| On-chain − tracked | +1,268,278,233 |
+12.68 ZEC |
The vault is over-solvent — it physically holds more ZEC than MAYAChain tracks, confirming the phantom outbounds' funds (~1.65 ZEC of that surplus) are really still there. Re-sending removes ~1.65 ZEC from a vault with a 12.68 ZEC surplus, so there is no insolvency/halt risk, and crediting the coin+gas back keeps the books from drifting further (it is backed by real on-chain funds, not fictional).
A full reconciliation of every on-chain tx touching the vault post-hardfork (3,141 txs from zebra) against MAYAChain found no missing inbounds — all are observed except two 0.00001 ZEC sub-dust deposits that MAYAChain ignores by design. So the four stale outbounds are the only discrepancy that needs fixing.
Testing
test/regression/suites/store/reschedule-stale-zec-outbound.yaml seeds an inbound voter that looks settled (one ZEC Action with a matching stale OutTx) and drives the full round-trip:
- migration removes the stale
OutTx(and the stale hash from the consensusout_hashes), credits the stalecoin+gasback to the active asgard, and queues a fresh outbound to the same recipient/amount; - asserts the vault ZEC balance increased by exactly
coin+gas(provesAddFundsran); - the replacement outbound is observed (
tx-observed-out); - the outbound queue clears and
mayachain/tx/detailsshows the new outbound inout_txs; - asserts the vault ZEC balance returns to its pre-migration value — the
AddFundscredit and the real send net to zero, proving the re-send neither double-spends nor leaks funds.
Passes with EXPORT=1 (goldens generated) and deterministically without. Builds clean under default and regtest tags.
Deployment
Runs once on the 1.130.0 upgrade (mainnet is currently on 1.129.7, store version 129). No go.mod / dependency changes.
Review checklist
- The four inbound/stale-out hash pairs match the affected swaps.
- Comfortable that zebra-node verification (not public explorers) is the correct source of truth.
-
rescheduleStaleOutbounds+requeueDanglingActionsinteraction (dangling detection afterOutTxremoval,OutboundHeightset). - Fund restoration (
SetReverted+AddFundscoin+gas to the active asgard) matches themigrateStoreV113precedent and is backed by the verified vault over-solvency.