[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: getrawtransaction of a tx that is in zebra's block 3363449 returns FOUND — so zebra's lookup works and the negatives are authoritative.
  • getblock 3363449 / getblock 3363445 on 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

  1. removes the stale outbound from OutTxs (so the ZEC Actions item becomes dangling), and clears the stale hash from the consensus tx and observation out_hashes (reopening their status);
  2. marks the stale outbound's own ObservedTxOutVoter as reverted (SetReverted);
  3. credits the recorded coin+gas back 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;
  4. calls the existing requeueDanglingActions, which schedules a fresh outbound and sets voter.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:

  1. migration removes the stale OutTx (and the stale hash from the consensus out_hashes), credits the stale coin+gas back to the active asgard, and queues a fresh outbound to the same recipient/amount;
  2. asserts the vault ZEC balance increased by exactly coin+gas (proves AddFunds ran);
  3. the replacement outbound is observed (tx-observed-out);
  4. the outbound queue clears and mayachain/tx/details shows the new outbound in out_txs;
  5. asserts the vault ZEC balance returns to its pre-migration value — the AddFunds credit 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 + requeueDanglingActions interaction (dangling detection after OutTx removal, OutboundHeight set).
  • Fund restoration (SetReverted + AddFunds coin+gas to the active asgard) matches the migrateStoreV113 precedent and is backed by the verified vault over-solvency.
Edited by Bitol Maya

Merge request reports

Loading
Loading