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 MaxTxOutSigningTransactionPeriod and 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 by eddsa/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-pool accepts pool_pub_key_frost and observed_monero_height (via NewMsgTssPoolV3). 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.yaml lists pubkey_cat as 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.
  • Unit tests for what the harness can't express. The harness runs ValidateBasic client-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 both MsgObservedTxOut and quorum
    • new: inbound MsgObservedTxIn carrying xmr_tx_key or spent refs
    • new: MsgTssPool XMR keygen with an empty or non-ed25519 FROST key, or Monero height ≤ 0; failure reports carrying blame still pass
    • new direct tests for hasDedicatedXMRMonovault / shouldMaintainXMRMonovault and xmrVaultMembershipNeedsKeygen (Init/Active count; Retiring, mismatched-membership and regular vaults don't)
  • xmr-e2e-gates: XMR_E2E_RUN named four tests that exist nowhere in the repo and missed two that do exist in e2e_gates_test.go. It now names the four real tests, anchored. All four also run in unit-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 fake httptest monerod, 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 TestMigrateFundsLeavesXMRDustBufferForFinalRound and TestMigrateFundsFinalXMRMigrationCoversFrostFeeEstimate.
    • Side note: the buffer at manager_network_current.go:914-921 is subtracted in every round where amt > dust, not only the final one as the comment says. That is harmless over-reservation, but the comment is misleading. Not changed here.

Simulation: XMR coverage (closed upstream by S00)

When this MR was opened, CI never exercised XMR in simulation:

  • Bifrost skipped every XMRMonovaultKeygen unconditionally (bifrost/signer/sign.go), so no vault ever carried XMR.
  • The 30s optional wait in configureXMRSimulationProfile therefore always expired and logged "skipping XMR simulation coverage". The timeout itself was working as designed; raising it would not have helped.
  • test-simulation-cluster also ran with TEST_SIMULATION_REQUIRE_XMR=0 and without xmr-coverage in 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 / HaltMonovaultKeygen mimirs, and mocknet sets ENABLE_MONOVAULT_KEYGEN=1
  • TEST_SIMULATION_REQUIRE_XMR ?= 1
  • xmr-coverage and xmr-spent-refs are now in TEST_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. ebifrost needs 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.

🤖 Generated with Claude Code

Edited by Son of Odin

Merge request reports

Loading
Loading