test(xmr): XMR regression coverage + fix pending-outbound queries hiding XMR outbounds
Summary
Closes most of the XMR regression-coverage gaps from the gap analysis on develop @ 9a80838b. It adds six new regression suites, all of which run in CI through make test-regression. While writing them I found and fixed one querier bug.
Bug fix: pending-outbound queries hid XMR outbounds for 2/3 of their signing window
/thorchain/queue/outbound (queryPendingOutbound) and /thorchain/queue (queryQueue) only scanned height - SigningTransactionPeriod. XMR outbounds, and EVM token or aggregator outbounds, stay live for 3x that period (TxOutItem.SigningTransactionPeriod, ExtendedOutboundSigningPeriodMultiplier). LackSigning, the keeper's GetPendingOutbounds and queue telemetry all account for that. The two queriers did not.
- An unsigned XMR outbound dropped out of both endpoints after 1x the period, even though it was still pending and not yet rescheduled.
- The queriers now scan
MaxTxOutSigningTransactionPeriodand filter each item by its own period. Output for 1x-window items does not change. - These queries are not on the WASM accept list (
app/wasm.go), so the change is API-only and has no consensus impact. - Covered by
TestQueryPendingOutboundKeepsExtendedSigningPeriodItems, which fails on develop, and byeddsa/xmr-signing-window.yaml, which is how the bug was found.
New regression suites (test/regression/suites/eddsa/)
| Suite | Covers |
|---|---|
xmr-inbound-swaps |
XMR→RUNE and XMR→BTC with an xmrunknown1… sender, observed against the monovault FROST key. The BTC leg is paid from the regular vault. A MIGRATE inbound from an unknown source vault is not credited (handler_observed_tx_helpers.go:583-620). |
xmr-add-liquidity |
RUNE-first staged add completed by an unknown-sender XMR add (+:XMR.XMR:<pair>/<xmr>). A different RUNE address trying to bind to an existing XMR asset address is refunded to the memo's asset address. Memoless add via reference: + r:00001 stages the asset under the registered pair, and the RUNE side completes it. A memoless XMR ADD registration without an asset address is rejected. |
xmr-refunds |
Quote API: XMR source requires a refund address, and integrated addresses are rejected as an "unsupported XMR address form". A price-limit refund goes to the memo refund address. A bad memo from an unknown sender with no refund address gets no outbound, and the coin stays with the pool. A late inbound to an inactive XMR vault via a memoless ADD reference is refunded to the registered ADD asset address (handler_observed_tx_helpers.go:401). |
xmr-signing-window |
With SIGNING_TRANSACTION_PERIOD=3, BTC reschedules every 3 blocks while XMR waits 9 (3x) and stays on the same monovault. No slash points are added. Both rescheduled outbounds still complete when observed. |
xmr-errata-before-quorum |
5 validators. 1 of 5 observes the inbound, 4 of 5 commit the errata, then quorum is reached. The inbound is reverted on arrival with no vault credit, pool change, swap or outbound (applyCommittedXMRErrataToInboundConsensus). Mutation-checked: disabling the function makes the suite fail. |
xmr-monovault-keygen |
Fresh single-node genesis schedules the XMR keygen at 1+128. A result reported before height 129 is rejected. A successful FROST keygen creates an XMR-only vault, activates it, stores vault_creation_height_xmr from observed_monero_height, and exposes the XMR inbound address. It creates 127 blocks, about 2 minutes locally. |
Supporting changes
- Harness:
tx-tss-poolacceptspool_pub_key_frostandobserved_monero_height(viaNewMsgTssPoolV3). Before this, a successful XMR keygen could not be expressed. The V3 constructor is identical to V2 for non-XMR keygens. - Template: new
btc-xmr-split-vault-pool-state.yaml, which pairs the XMR monovault with a regular BTC vault whose only member is the single validator.- The existing
btc-xmr-quote-pool-state.yamllistspubkey_catas the BTC vault member. That promotes cat to an active validator at block 3 (and back to standby at block 4), so single-signer observations stop reaching consensus. It is fine for quote-only suites but can't be used for tx flows. - Templates must stay one YAML doc. The harness counts suite lines before template expansion, and a two-doc template panics
run.go:291.
- The existing
- Unit tests for what the harness can't express. The harness runs
ValidateBasicclient-side and aborts the suite on error, so validation rejections go here:- exact error strings for XMR outbound with a missing, blank or malformed
xmr_tx_key, on bothMsgObservedTxOutand quorum - new: inbound
MsgObservedTxIncarryingxmr_tx_keyor spent refs - new:
MsgTssPoolXMR keygen with an empty or non-ed25519 FROST key, or Monero height ≤ 0; failure reports carrying blame still pass - new direct tests for
hasDedicatedXMRMonovault/shouldMaintainXMRMonovaultandxmrVaultMembershipNeedsKeygen(Init/Active count; Retiring, mismatched-membership and regular vaults don't)
- exact error strings for XMR outbound with a missing, blank or malformed
xmr-e2e-gates:XMR_E2E_RUNnamed four tests that exist nowhere in the repo and missed two that do exist ine2e_gates_test.go. It now names the four real tests, anchored. All four also run inunit-tests, which covers every package with-tags=mocknet. So this job is a fast, named re-run of scanner-attribution and solvency gates against a fakehttptestmonerod, not separate coverage. Nothing here overlaps with the regression suites.
Not expressible in regtest (covered by unit tests or left as-is)
- Validation rejections: see the unit tests above.
- Final-round XMR migration dust buffer: needs a real churn and migration loop. It stays covered by
TestMigrateFundsLeavesXMRDustBufferForFinalRoundandTestMigrateFundsFinalXMRMigrationCoversFrostFeeEstimate.- Side note: the buffer at
manager_network_current.go:914-921is subtracted in every round whereamt > dust, not only the final one as the comment says. That is harmless over-reservation, but the comment is misleading. Not changed here.
- Side note: the buffer at
Simulation: XMR coverage (closed upstream by S00)
When this MR was opened, CI never exercised XMR in simulation:
- Bifrost skipped every
XMRMonovaultKeygenunconditionally (bifrost/signer/sign.go), so no vault ever carried XMR. - The 30s optional wait in
configureXMRSimulationProfiletherefore always expired and logged "skipping XMR simulation coverage". The timeout itself was working as designed; raising it would not have helped. test-simulation-clusteralso ran withTEST_SIMULATION_REQUIRE_XMR=0and withoutxmr-coveragein its stage list.
Fixing that was not cheap, because it depended on the S00 keygen work. S00 (!5026 (merged), 8c5c57ed7f) has since landed on develop, and this branch is rebased on top of it. It closes the gap:
- the bifrost keygen skip is now gated by the
EnableMonovaultKeygen/HaltMonovaultKeygenmimirs, and mocknet setsENABLE_MONOVAULT_KEYGEN=1 TEST_SIMULATION_REQUIRE_XMR ?= 1xmr-coverageandxmr-spent-refsare now inTEST_SIMULATION_CLUSTER_STAGES
So this MR makes no sim change. The regression suites here complement the sim by giving deterministic, state-level coverage of XMR handler paths: errata ordering, memoless and unknown-sender LP, inactive-vault refunds, the signing window, and keygen validation. A live mocknet run cannot reliably target those paths.
Testing
make test-regression RUN=xmr CI=1 EXPORT=1 DEBUG=1: 9/9 suites pass (6 new + 3 existing, existing exports unchanged).- Full regression suite (PARALLELISM=8, TIME_FACTOR=2): 190/190 pass; the only export diff was a pre-existing trailing-newline nit in
api/logs.json(reverted, unrelated). go test -tags=mocknet ./x/thorchain/... ./common/...: pass.ebifrostneeds a solo re-run for its known fixed-port collision.make test-xmr-e2e: 4 tests pass.- golangci-lint v1.63.4 (
--new-from-rev): clean.