Skip to content

[CHIA-4376] [Bug] Reorged secure Offer cancellation remains CANCELLED after the original Offer confirms #21301

Description

@okp74103-ops

What happened?

A securely cancelled Offer can remain permanently recorded as CANCELLED after the block confirming the cancellation is removed by a reorg. The cancellation spend returns to the full-node mempool, but the maker's TradeRecord is not rolled back.

If a taker still has the originally signed Offer, a higher-fee acceptance can then replace the rebroadcast cancellation under the normal mempool replacement rules. The original Offer confirms, while the connected maker wallet continues to report the trade as CANCELLED.

This report is about wallet/trade-state consistency after a reorg. The exchange still follows the terms of the originally signed Offer; no signature bypass or arbitrary asset transfer is claimed.

Deterministic reproduction

The reproduction uses the real wallet simulator, TradeManager, CAT wallet, full-node mempool, block farming, and reorg path.

  1. Create and share a signed CAT-for-XCH Offer.
  2. Securely cancel it on chain using the default zero fee and no expiry condition.
  3. Farm a block confirming the cancellation.
  4. Verify that the maker's record is CANCELLED, but confirmed_at_index == 0.
  5. Reorg out the cancellation block with FullNodeSimulator.reorg_from_index_to_new_index().
  6. Verify that the cancellation spend returns to the full-node mempool.
  7. Submit the original Offer through TradeManager.respond_to_offer() with a fee of 20,000,000 mojos.
  8. Verify that this higher-fee acceptance replaces the cancellation in the mempool.
  9. Farm a block and verify that the original Offer spend confirms.
  10. Query the maker's original TradeRecord; it still reports CANCELLED with confirmed_at_index == 0.

Observed output:

POC_STEP_1 secure cancellation confirmed: stored_status=CANCELLED stored_confirmed_at_index=0
CONTROL_NO_EXPIRY reorged secure cancellation returned to mempool=True
CONTROL_NO_EXPIRY higher-fee original offer replaced cancellation in mempool=True
CONTROL_NO_EXPIRY higher-fee take confirmed while maker status remains CANCELLED

The positive sequence passed 8/8 fresh Windows executions. It also passed under Linux Docker and was reconfirmed against the latest origin/main immediately before filing this issue.

Negative controls

  • With no reorg, the securely cancelled Offer remains invalid.
  • After the same reorg, a zero/equal-fee acceptance is rejected with MEMPOOL_CONFLICT, and the cancellation remains in the mempool.
  • The expiry condition used in an earlier hypothesis is not required; the minimal no-expiry sequence above reproduces the bug.

Aggregate results:

Windows / Python 3.12.13:
4 passed, 12 skipped, 64 deselected

Linux Docker / Python 3.12.14:
2 passed, 6 skipped, 72 deselected

Latest origin/main / Windows / Python 3.12.13:
2 passed, 6 skipped, 72 deselected

Expected behavior

When the cancellation confirmation is rolled back, the maker's Offer record should no longer remain authoritatively CANCELLED. The wallet should restore an appropriate pending/reorged state and continue tracking the cancellation spend.

At minimum, if the original Offer subsequently confirms, the stale maker record should be updated to reflect that execution instead of remaining CANCELLED.

Actual behavior

  • The cancellation spend is correctly returned to the mempool after the reorg.
  • A higher-fee original Offer acceptance can replace it and confirm.
  • The maker's original record remains CANCELLED while the wallet is connected.
  • The stale state also persisted across a wallet restart in the extended lifecycle test.

Root cause

  1. chia/wallet/trade_manager.py, coins_of_interest_farmed() sets a confirmed cancellation without passing the block height:

    await self.trade_store.set_status(trade.trade_id, TradeStatus.CANCELLED)
  2. chia/wallet/trading/trade_store.py, set_status() only changes confirmed_at_index when index != 0, so the cancelled record retains confirmed_at_index == 0.

  3. TradeStore.rollback_to_block() only deletes records with confirmed_at_index > block_index. A cancellation recorded at index 0 cannot be reverted by this rule.

  4. WalletStateManager.reorg_rollback() rolls back several wallet stores but does not invoke TradeStore rollback.

  5. TradeManager.get_coins_of_interest() excludes CANCELLED trades, and get_trades_by_coin() skips them. The original Offer's later confirmation therefore does not correct the maker's stale record.

Related issues

Version

Initially tested commit: 0b88565359087cc09655cb5ad8cdd93354c30931

Reconfirmed immediately before filing against origin/main at 9492119b2f26acb034d0d7cfc881d071aa4f904a. The positive higher-fee sequence and equal-fee negative control both passed (2 passed, 6 skipped, 72 deselected).

What platform are you using?

Windows (also reproduced in Linux Docker)

What ui mode are you using?

CLI / wallet simulator

Relevant log output

POC_STEP_1 secure cancellation confirmed: stored_status=CANCELLED stored_confirmed_at_index=0
CONTROL_NO_EXPIRY reorged secure cancellation returned to mempool=True
CONTROL_NO_EXPIRY higher-fee original offer replaced cancellation in mempool=True
CONTROL_NO_EXPIRY higher-fee take confirmed while maker status remains CANCELLED

CONTROL_EQUAL_FEE original offer rejected while reorged cancellation remains in mempool=True

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions