fix(terraclassic): bound withdrawal polling with an active index (#139) #158

Merged
PlasticDigits merged 4 commits from fix/gl-139-terra-active-withdrawals into main 2026-08-31 12:32:27 +00:00
PlasticDigits commented 2026-08-31 05:57:48 +00:00 (Migrated from gitlab.com)

Summary

  • Add ACTIVE_WITHDRAW_HASHES so Terra list queries used by the operator and canceler iterate in-flight withdrawals only, not lifetime executed/cancelled history (GL-139).
  • Keep PENDING_WITHDRAWS as canonical status/replay storage. pending_withdrawals remains all-status; new active_withdrawals / active_withdraw_index are versioned.
  • Reconstruct the index with a batched, resumable migrate. Operator and canceler prefer the active query and fall back to pending_withdrawals on old contracts or incomplete migrate.
  • Review follow-up (d5aeda4): migrating from a wasm that does not maintain the index (< 2.1.0, including rollback to 2.0.0 then re-upgrade) resets leftover complete=true and rebuilds (insert active, remove terminal leftovers). Admin ContinueActiveIndexMigrate continues batches when same-code_id wasm migrate is rejected, and supports an emergency rebuild. active_withdrawals scan work is capped; clients continue via next_start_after. Canceler fallback clears accumulated candidates.

Related #139 (kept open: migrate gas measurement, soak, frontend historical-status manual check)

Invariants / docs

Test plan

  • Contract unit tests for index insert/remove, batched/idempotent migrate, orphan skip, incomplete-migrate query error
  • Property tests: randomized lifecycle keeps INV-TC-AW1; migrate rebuilds mixed history
  • Integration: empty, submit/approve/cancel/uncancel/execute, replay, status mix, pagination, pending_withdrawals still all-status
  • contract::migrate entry-point tests: empty, mixed resume, idempotent 2.1 continue, stale complete=true after 2.0, terminal leftover removal, cw-multi-test wiring
  • Admin ContinueActiveIndexMigrate unauthorized + rebuild/resume
  • Active query scan cap + next_start_after continuation
  • Full packages/contracts-terraclassic cargo test (then retest withdraw-flow + proptest + migrate tests)
  • cargo check --bins operator and canceler; clippy via pre-commit
  • multichain-rs Terra query serialization tests
  • On-chain migrate of a large historical PENDING_WITHDRAWS map with measured gas (needs deployed state)
  • Operator/canceler soak against a contract with substantial terminal history
  • Frontend transfer-status still shows executed Terra withdrawals via pending_withdraw

Acceptance criteria (from #139)

  • Actionable list queries iterate an active index (work proportional to active page; skip-capped scan with cursor)
  • Executed withdrawals leave the active index; canonical replay/status remains
  • Cancelled withdrawals stay canonical for uncancel and re-enter the index on uncancel
  • Submit/approve/cancel/uncancel/unlock/mint execute update canonical + index together
  • Duplicate hash and used (src_chain, nonce) still rejected after archival and after migrate
  • Single-hash pending_withdraw unchanged
  • pending_withdrawals all-status semantics preserved (no silent break)
  • Pagination deterministic (exclusive hash cursor, capped limit, next_start_after for skip-capped pages)
  • Migrate is batched, resumable; completed migrate is a no-op on 2.1.x; rebuilds when previous version does not maintain the index
  • Operator and canceler prefer active_withdrawals with legacy fallback; canceler clears accumulators on fallback; no per-entry terminal debug logs
  • Metrics/logs: active poll counts, query mode, inconsistent skips, cycle summaries
  • Schema, compatibility, deployment/rollback/emergency-rebuild documented
  • Existing deployed state migrated within measured gas limits (still needs on-chain evidence)

Not in this MR

  • Operator RPC livelock / stale-unapproved retry backoff — #138
  • Bounding attacker-created unapproved spam (active set can still grow; #138)
  • Measured mainnet/testnet migrate gas against production-sized history
  • Switching the frontend hash monitor to active_withdrawals (would hide completed transfers)
  • Closing #139 until gas + soak + frontend historical-status are done
## Summary - Add `ACTIVE_WITHDRAW_HASHES` so Terra list queries used by the operator and canceler iterate in-flight withdrawals only, not lifetime executed/cancelled history (GL-139). - Keep `PENDING_WITHDRAWS` as canonical status/replay storage. `pending_withdrawals` remains all-status; new `active_withdrawals` / `active_withdraw_index` are versioned. - Reconstruct the index with a batched, resumable migrate. Operator and canceler prefer the active query and fall back to `pending_withdrawals` on old contracts or incomplete migrate. - **Review follow-up (`d5aeda4`):** migrating from a wasm that does not maintain the index (`< 2.1.0`, including rollback to 2.0.0 then re-upgrade) resets leftover `complete=true` and rebuilds (insert active, remove terminal leftovers). Admin `ContinueActiveIndexMigrate` continues batches when same-`code_id` wasm migrate is rejected, and supports an emergency rebuild. `active_withdrawals` scan work is capped; clients continue via `next_start_after`. Canceler fallback clears accumulated candidates. Related #139 (kept open: migrate gas measurement, soak, frontend historical-status manual check) ## Invariants / docs - [docs/TERRACLASSIC_BRIDGE_INVARIANTS.md](docs/TERRACLASSIC_BRIDGE_INVARIANTS.md) — **INV-TC-AW1–AW4** - [skills/agent-terraclassic-active-withdrawals.md](skills/agent-terraclassic-active-withdrawals.md) - [docs/deployment-terraclassic-upgrade.md](docs/deployment-terraclassic-upgrade.md) v2.1 migrate loop, rollback+re-upgrade, emergency rebuild - [docs/FRONTEND_BRIDGE_INVARIANTS.md](docs/FRONTEND_BRIDGE_INVARIANTS.md) **INV-FE-TC-AW1** (hash monitor stays on historical `pending_withdrawals`) ## Test plan - [x] Contract unit tests for index insert/remove, batched/idempotent migrate, orphan skip, incomplete-migrate query error - [x] Property tests: randomized lifecycle keeps INV-TC-AW1; migrate rebuilds mixed history - [x] Integration: empty, submit/approve/cancel/uncancel/execute, replay, status mix, pagination, `pending_withdrawals` still all-status - [x] `contract::migrate` entry-point tests: empty, mixed resume, idempotent 2.1 continue, stale `complete=true` after 2.0, terminal leftover removal, cw-multi-test wiring - [x] Admin `ContinueActiveIndexMigrate` unauthorized + rebuild/resume - [x] Active query scan cap + `next_start_after` continuation - [x] Full `packages/contracts-terraclassic` `cargo test` (then retest withdraw-flow + proptest + migrate tests) - [x] `cargo check --bins` operator and canceler; clippy via pre-commit - [x] `multichain-rs` Terra query serialization tests - [ ] On-chain migrate of a large historical `PENDING_WITHDRAWS` map with measured gas (needs deployed state) - [ ] Operator/canceler soak against a contract with substantial terminal history - [ ] Frontend transfer-status still shows executed Terra withdrawals via `pending_withdraw` ## Acceptance criteria (from #139) - [x] Actionable list queries iterate an active index (work proportional to active page; skip-capped scan with cursor) - [x] Executed withdrawals leave the active index; canonical replay/status remains - [x] Cancelled withdrawals stay canonical for uncancel and re-enter the index on uncancel - [x] Submit/approve/cancel/uncancel/unlock/mint execute update canonical + index together - [x] Duplicate hash and used `(src_chain, nonce)` still rejected after archival and after migrate - [x] Single-hash `pending_withdraw` unchanged - [x] `pending_withdrawals` all-status semantics preserved (no silent break) - [x] Pagination deterministic (exclusive hash cursor, capped limit, `next_start_after` for skip-capped pages) - [x] Migrate is batched, resumable; completed migrate is a no-op on 2.1.x; **rebuilds** when previous version does not maintain the index - [x] Operator and canceler prefer `active_withdrawals` with legacy fallback; canceler clears accumulators on fallback; no per-entry terminal debug logs - [x] Metrics/logs: active poll counts, query mode, inconsistent skips, cycle summaries - [x] Schema, compatibility, deployment/rollback/emergency-rebuild documented - [ ] Existing deployed state migrated within **measured** gas limits (still needs on-chain evidence) ## Not in this MR - Operator RPC livelock / stale-unapproved retry backoff — #138 - Bounding attacker-created **unapproved** spam (active set can still grow; #138) - Measured mainnet/testnet migrate gas against production-sized history - Switching the frontend hash monitor to `active_withdrawals` (would hide completed transfers) - Closing #139 until gas + soak + frontend historical-status are done
PlasticDigits commented 2026-08-31 05:58:02 +00:00 (Migrated from gitlab.com)

marked this merge request as ready

marked this merge request as **ready**
PlasticDigits commented 2026-08-31 09:14:18 +00:00 (Migrated from gitlab.com)

RECOMMEND: FIX

Reviewed against #139. The active-index design is sound for the happy path (canonical PENDING_WITHDRAWS, membership index, atomic save_pending_and_sync_index, replay maps unchanged, pending_withdrawals all-status preserved). It is not a BLOCK: this does not create a double-payout or strip RBAC. It does need a migrate/rollback fix before it is safe to document “rollback then re-upgrade”.

Security / contract

Vector Result
Replay after hash leaves the active index MET — duplicate hash still hits PENDING_WITHDRAWS; (src_chain, nonce) still WITHDRAW_NONCE_USED
Unauthorized cancel / uncancel / execute Unchanged RBAC; index is not an authorization source
Admin delete of actionable state No privileged prune path added
Index without canonical / canonical without index Query skips orphans (inconsistent_skipped); lifecycle writes both in one tx
Incomplete migrate active_withdrawals errors → operator/canceler fall back to pending_withdrawals
Rollback wasm to v2.0.0 then re-upgrade to 2.1.0 HIGH — see below
Pagination / query-limit Returned page is capped; scan work is not (orphan keys)
Unapproved spam Out of scope (called out; companion #138)
Oracle / tokenomic N/A for this index
Secret / DB leak CosmWasm state is public; metrics avoid addresses/credentials
Prompt injection None

High: stale index after rollback + re-upgrade

migrate_active_index_batch is a no-op when complete=true. v2.0.0 does not maintain ACTIVE_WITHDRAW_HASHES.

Sequence:

  1. Migrate to 2.1.0 until active_index_complete=true.
  2. Roll back code to 2.0.0 (documented as supported). New submit/approve/execute update only PENDING_WITHDRAWS.
  3. Re-upgrade to 2.1.0. Migrate sees complete=true and does not rebuild.

Operator/canceler then query active_withdrawals successfully (no fallback) and omit withdrawals submitted during the v2.0 window. That is missed approvals / missed cancel-window coverage, not an extra payout. Fail-closed for those hashes is still “user funds wait forever until someone notices”.

Additionally, reconstruction is insert-only. Terminal rows that stayed in the index while v2.0 executed them are not removed; they become orphans that inflate ACTIVE_WITHDRAW_COUNT and query scan cost.

Fix: If previous CONTRACT_VERSION is not 2.1.0 (or whenever migrating from 2.0.x), reset ACTIVE_INDEX_MIGRATION (complete=false, last_key=None) and either clear the index or, on each scanned canonical row, remove_active when !is_active. Do not treat a leftover complete=true as valid across a version downgrade.

Medium: repeat-migrate on columbus-5

Docs say to re-run terrad tx wasm migrate $BRIDGE_ADDRESS $NEW_CODE_ID until complete. Confirm wasmd on Terra Classic allows migrate to the same code_id. If it does not, add ExecuteMsg::ContinueActiveIndexMigrate { limit } (admin-only) instead of requiring dummy code stores.

Medium: unbounded skip loop

query_active_withdrawals breaks when withdrawals.len() >= limit, but inconsistent keys do not count toward that limit. A polluted index can make one query walk the entire map (gas griefing / LCD timeout). Cap total range iterations (e.g. limit + MAX_SKIP or a hard max) and document that a short page may need another cursor.

#139 acceptance criteria

Criterion Status
Actionable list work ∝ active page MET on a consistent index
Executed leave index; canonical remains MET on the live 2.1 path
Cancelled stay canonical; uncancel reinserts MET
All lifecycle paths update both MET — only save_pending_and_sync_index writes PENDING_WITHDRAWS in contract code
Replay after archival / migrate MET (tests + proptest)
Single-hash pending_withdraw unchanged MET
pending_withdrawals all-status MET
Pagination deterministic exclusive cursor MET for consistent data
Batched / resumable / idempotent migrate PARTIAL — resume works until complete; rollback breaks the assumption
Operator/canceler prefer active + fallback MET
Metrics/logs MET
Schema / deploy / rollback docs PARTIAL — rollback docs omit stale-index re-upgrade

Testing

Strong: index insert/remove, batched migrate, orphan skip, incomplete query error, proptest INV-TC-AW1, withdraw-flow integration.

Missing vs issue verification:

  • Measured migrate gas on production-sized PENDING_WITHDRAWS (explicitly unchecked).
  • Operator/canceler soak with large terminal history.
  • Frontend historical status still via pending_withdrawals (INV-FE-TC-AW1 is coded; manual unchecked).
  • Rollback → live traffic → re-upgrade test (would catch the HIGH).

Gas: batch 50 / cap 100 is a reasonable bound; still needs a real LCD measurement before mainnet migrate. Unapproved-spam growth of the active set remains an economic/DoS residual (#138).

RECOMMEND: FIX Reviewed against #139. The active-index design is sound for the happy path (canonical `PENDING_WITHDRAWS`, membership index, atomic `save_pending_and_sync_index`, replay maps unchanged, `pending_withdrawals` all-status preserved). It is **not** a BLOCK: this does not create a double-payout or strip RBAC. It **does** need a migrate/rollback fix before it is safe to document “rollback then re-upgrade”. ## Security / contract | Vector | Result | |---|---| | Replay after hash leaves the active index | MET — duplicate hash still hits `PENDING_WITHDRAWS`; `(src_chain, nonce)` still `WITHDRAW_NONCE_USED` | | Unauthorized cancel / uncancel / execute | Unchanged RBAC; index is not an authorization source | | Admin delete of actionable state | No privileged prune path added | | Index without canonical / canonical without index | Query skips orphans (`inconsistent_skipped`); lifecycle writes both in one tx | | Incomplete migrate | `active_withdrawals` errors → operator/canceler fall back to `pending_withdrawals` | | **Rollback wasm to v2.0.0 then re-upgrade to 2.1.0** | **HIGH** — see below | | Pagination / query-limit | Returned page is capped; **scan work is not** (orphan keys) | | Unapproved spam | Out of scope (called out; companion #138) | | Oracle / tokenomic | N/A for this index | | Secret / DB leak | CosmWasm state is public; metrics avoid addresses/credentials | | Prompt injection | None | ### High: stale index after rollback + re-upgrade `migrate_active_index_batch` is a **no-op when `complete=true`**. v2.0.0 does **not** maintain `ACTIVE_WITHDRAW_HASHES`. Sequence: 1. Migrate to 2.1.0 until `active_index_complete=true`. 2. Roll back code to 2.0.0 (documented as supported). New submit/approve/execute update only `PENDING_WITHDRAWS`. 3. Re-upgrade to 2.1.0. Migrate sees `complete=true` and **does not rebuild**. Operator/canceler then query `active_withdrawals` successfully (no fallback) and **omit withdrawals submitted during the v2.0 window**. That is missed approvals / missed cancel-window coverage, not an extra payout. Fail-closed for those hashes is still “user funds wait forever until someone notices”. Additionally, reconstruction is **insert-only**. Terminal rows that stayed in the index while v2.0 executed them are not removed; they become orphans that inflate `ACTIVE_WITHDRAW_COUNT` and query scan cost. **Fix:** If previous `CONTRACT_VERSION` is not `2.1.0` (or whenever migrating from 2.0.x), reset `ACTIVE_INDEX_MIGRATION` (`complete=false`, `last_key=None`) and either clear the index or, on each scanned canonical row, `remove_active` when `!is_active`. Do not treat a leftover `complete=true` as valid across a version downgrade. ### Medium: repeat-migrate on columbus-5 Docs say to re-run `terrad tx wasm migrate $BRIDGE_ADDRESS $NEW_CODE_ID` until complete. Confirm wasmd on Terra Classic allows migrate to the **same** code_id. If it does not, add `ExecuteMsg::ContinueActiveIndexMigrate { limit }` (admin-only) instead of requiring dummy code stores. ### Medium: unbounded skip loop `query_active_withdrawals` breaks when `withdrawals.len() >= limit`, but inconsistent keys do not count toward that limit. A polluted index can make one query walk the entire map (gas griefing / LCD timeout). Cap total range iterations (e.g. `limit + MAX_SKIP` or a hard max) and document that a short page may need another cursor. ## #139 acceptance criteria | Criterion | Status | |---|---| | Actionable list work ∝ active page | MET on a consistent index | | Executed leave index; canonical remains | MET on the live 2.1 path | | Cancelled stay canonical; uncancel reinserts | MET | | All lifecycle paths update both | MET — only `save_pending_and_sync_index` writes `PENDING_WITHDRAWS` in contract code | | Replay after archival / migrate | MET (tests + proptest) | | Single-hash `pending_withdraw` unchanged | MET | | `pending_withdrawals` all-status | MET | | Pagination deterministic exclusive cursor | MET for consistent data | | Batched / resumable / idempotent migrate | PARTIAL — resume works until `complete`; rollback breaks the assumption | | Operator/canceler prefer active + fallback | MET | | Metrics/logs | MET | | Schema / deploy / rollback docs | PARTIAL — rollback docs omit stale-index re-upgrade | ## Testing Strong: index insert/remove, batched migrate, orphan skip, incomplete query error, proptest INV-TC-AW1, withdraw-flow integration. Missing vs issue verification: - Measured migrate gas on production-sized `PENDING_WITHDRAWS` (explicitly unchecked). - Operator/canceler soak with large terminal history. - Frontend historical status still via `pending_withdrawals` (INV-FE-TC-AW1 is coded; manual unchecked). - Rollback → live traffic → re-upgrade test (would catch the HIGH). Gas: batch 50 / cap 100 is a reasonable bound; still needs a real LCD measurement before mainnet migrate. Unapproved-spam growth of the **active** set remains an economic/DoS residual (#138).
PlasticDigits commented 2026-08-31 09:14:54 +00:00 (Migrated from gitlab.com)

mentioned in merge request !160

mentioned in merge request !160
PlasticDigits commented 2026-08-31 09:16:01 +00:00 (Migrated from gitlab.com)

Follow-up (second pass): still RECOMMEND: FIX. Additional items not in the first note:

  1. Canceler fallback does not reset all_approvals. In packages/canceler/src/watcher.rs, a failed active_withdrawals page resets cursor / pages_fetched / total_seen but not the accumulated all_approvals vec. If page 1 of the active query succeeds and a later page fails, fallback re-walks pending_withdrawals and appends, duplicating cancel candidates. Re-cancel is mostly a no-op on-chain, but it is extra verify/cancel work. Clear all_approvals (and inconsistent_skipped) on fallback. Operator processes pages immediately so this is less severe there; still worth resetting counters on fallback.

  2. No contract::migrate entry-point test. Resume/idempotency tests call migrate_active_index_batch on mock storage only. Add a cw-multi-test that instantiates v2.0-shaped state and runs contract::migrate with MigrateMsg { active_index_batch_limit }: empty, mixed, resume, idempotent second call, active_withdrawals errors while incomplete, OK when complete. A wiring regression in contract.rs migrate would not be caught today.

  3. Please do not Closes #139 until migrate gas is measured on realistic PENDING_WITHDRAWS size. Batching (50/100) exists; issue AC required “within tested gas limits.” Keep #139 open (or a follow-up) for gas + soak + frontend historical-status. Unapproved-spam bounding stays #138.

  4. Agree with documenting an emergency rebuild (complete=false in a follow-up wasm) in addition to the rollback+re-upgrade reset requested in the first note. Canonical-active without an index key is invisible to active_withdrawals (orphans are skipped; holes omit operator work).

Follow-up (second pass): still **RECOMMEND: FIX**. Additional items not in the first note: 1. **Canceler fallback does not reset `all_approvals`.** In `packages/canceler/src/watcher.rs`, a failed `active_withdrawals` page resets cursor / `pages_fetched` / `total_seen` but not the accumulated `all_approvals` vec. If page 1 of the active query succeeds and a later page fails, fallback re-walks `pending_withdrawals` and **appends**, duplicating cancel candidates. Re-cancel is mostly a no-op on-chain, but it is extra verify/cancel work. Clear `all_approvals` (and `inconsistent_skipped`) on fallback. Operator processes pages immediately so this is less severe there; still worth resetting counters on fallback. 2. **No `contract::migrate` entry-point test.** Resume/idempotency tests call `migrate_active_index_batch` on mock storage only. Add a cw-multi-test that instantiates v2.0-shaped state and runs `contract::migrate` with `MigrateMsg { active_index_batch_limit }`: empty, mixed, resume, idempotent second call, `active_withdrawals` errors while incomplete, OK when complete. A wiring regression in `contract.rs` migrate would not be caught today. 3. **Please do not `Closes #139` until migrate gas is measured** on realistic `PENDING_WITHDRAWS` size. Batching (50/100) exists; issue AC required “within tested gas limits.” Keep #139 open (or a follow-up) for gas + soak + frontend historical-status. Unapproved-spam bounding stays #138. 4. Agree with documenting an emergency rebuild (`complete=false` in a follow-up wasm) in addition to the rollback+re-upgrade reset requested in the first note. Canonical-active without an index key is invisible to `active_withdrawals` (orphans are skipped; holes omit operator work).
PlasticDigits commented 2026-08-31 11:37:28 +00:00 (Migrated from gitlab.com)

added 1 commit

  • d5aeda49 - fix(terraclassic): rebuild active index after 2.0 rollback and bound queries (#139)

Compare with previous version

added 1 commit <ul><li>d5aeda49 - fix(terraclassic): rebuild active index after 2.0 rollback and bound queries (#139)</li></ul> [Compare with previous version](/PlasticDigits/cl8y-bridge-monorepo/-/merge_requests/18/diffs?diff_id=1993952707&start_sha=2fdb194052b5ceb763bda0147421cf7c069af73d)
PlasticDigits commented 2026-08-31 11:38:18 +00:00 (Migrated from gitlab.com)

Review follow-up (d5aeda4)

Addressed the two review notes on this MR (RECOMMEND: FIX). Work was done in the existing fix/gl-139-terra-active-withdrawals worktree, implemented, tested, retested, then pushed.

Completed from reviews

Review item What landed
HIGH: stale index after rollback → 2.0 → re-upgrade migrate reads cw2 before overwrite. If the previous version does not maintain the index (< 2.1.0), reconstruction is reset (complete=false, last_key=None) even when leftover complete=true remains. Scan uses sync_active_index (insert active, remove terminal leftovers).
MEDIUM: same code_id migrate on columbus-5 Admin-only ExecuteMsg::ContinueActiveIndexMigrate { limit, rebuild }. Use rebuild: false to continue batches if wasm migrate to the same code_id is rejected. Does not delete canonical rows.
MEDIUM: unbounded skip loop query_active_withdrawals visits at most limit + MAX_ACTIVE_QUERY_SKIPS (64) keys. Short/empty pages return next_start_after; operator/canceler continue on that cursor and stop on null.
Canceler fallback duplicates all_approvals Fallback clears all_approvals, skip counters, page counters, and cursor before the legacy walk. Operator also resets poll counters on fallback.
No contract::migrate entry-point test tests/test_active_index_migrate.rs: empty, mixed resume, idempotent 2.1 continue, stale complete after 2.0, terminal leftover removal, cw-multi-test .with_migrate wiring. Plus test_continue_active_index_migrate_admin_only_rebuild_and_resume.
Do not Closes #139 until gas is measured MR description no longer closes #139. Issue stays open for gas + soak + frontend historical-status.
Emergency rebuild Documented + implemented: ContinueActiveIndexMigrate { rebuild: true } once, then rebuild: false until complete. INV-TC-AW3, deployment notes, OPERATIONAL_NOTES, skill.

Invariants INV-TC-AW1–AW4 and skills/agent-terraclassic-active-withdrawals.md are updated and cross-linked with deployment + docs/contracts-terraclassic.md.

Not done (out of scope / needs deployed state)

  • Measured migrate gas on production-sized PENDING_WITHDRAWS (issue AC “within tested gas limits”).
  • Operator/canceler soak with large terminal history.
  • Manual frontend check that transfer-status still lists executed Terra withdrawals via pending_withdrawals (INV-FE-TC-AW1 is coded; not exercised in a browser here).
  • Bounding attacker-created unapproved spam — remains #138.
  • Confirming live columbus-5 allows/forbids same-code_id wasm migrate (execute path covers both).
  • Switching the frontend hash monitor to active_withdrawals (would hide completed transfers; still must not).

Tests run (then retested)

  • packages/contracts-terraclassic: full cargo test -p bridge (lib, integration, proptest, withdraw flow, new migrate tests).
  • Retest: --lib + test_active_index_migrate + test_withdraw_flow + proptest_active_withdraw.
  • packages/operator: cargo check --bins; cargo test terra.
  • packages/canceler: cargo check --bins; cargo test --lib.
  • packages/multichain-rs: cargo test --lib terra.
  • cargo clippy -p bridge -- -D warnings (lib).
  • Pre-commit: fmt, clippy (operator/canceler/terra), frontend typecheck/lint, gitleaks.

Checklist to verify on review / staging

  • Rollback + re-upgrade: migrate to 2.1 until complete → wasm back to 2.0.0 (version becomes 2.0.0) → live submit/approve/execute → re-upgrade to 2.1. First migrate attributes include active_index_reset=true. New in-flight hashes appear in active_withdrawals; hashes executed during the 2.0 window are absent from the index and still present in pending_withdraw.
  • Continue without a new code store: after the first wasm migrate, remaining batches via {"continue_active_index_migrate":{"limit":50,"rebuild":false}} until {"active_withdraw_index":{}} shows migration_complete: true.
  • Emergency rebuild: rebuild: true once only, then rebuild: false. Canonical rows unchanged. active_withdrawals errors (clients fall back) until complete.
  • Unauthorized continue: non-admin ContinueActiveIndexMigrate fails; index unchanged.
  • Skip-capped query: polluted index returns a short page with next_start_after set; operator/canceler keep paging instead of treating it as exhausted.
  • Canceler fallback: if an active page succeeds and a later active page fails, cancel candidates are not duplicated on the legacy walk.
  • Frontend: transfer status still discovers executed Terra hashes via pending_withdrawals + pending_withdraw (do not point the monitor at active_withdrawals).
  • Replay: after execute (index removal) and after migrate, duplicate hash and used (src_chain, nonce) still rejected.
  • Gas (follow-up on #139): measure a 50-row batch against realistic history before mainnet migrate; keep #139 open until that number exists.
## Review follow-up (`d5aeda4`) Addressed the two review notes on this MR (RECOMMEND: FIX). Work was done in the existing `fix/gl-139-terra-active-withdrawals` worktree, implemented, tested, retested, then pushed. ### Completed from reviews | Review item | What landed | |---|---| | **HIGH: stale index after rollback → 2.0 → re-upgrade** | `migrate` reads cw2 **before** overwrite. If the previous version does not maintain the index (`< 2.1.0`), reconstruction is reset (`complete=false`, `last_key=None`) even when leftover `complete=true` remains. Scan uses `sync_active_index` (insert active, **remove** terminal leftovers). | | **MEDIUM: same `code_id` migrate on columbus-5** | Admin-only `ExecuteMsg::ContinueActiveIndexMigrate { limit, rebuild }`. Use `rebuild: false` to continue batches if wasm migrate to the same code_id is rejected. Does **not** delete canonical rows. | | **MEDIUM: unbounded skip loop** | `query_active_withdrawals` visits at most `limit + MAX_ACTIVE_QUERY_SKIPS` (64) keys. Short/empty pages return `next_start_after`; operator/canceler continue on that cursor and stop on `null`. | | **Canceler fallback duplicates `all_approvals`** | Fallback clears `all_approvals`, skip counters, page counters, and cursor before the legacy walk. Operator also resets poll counters on fallback. | | **No `contract::migrate` entry-point test** | `tests/test_active_index_migrate.rs`: empty, mixed resume, idempotent 2.1 continue, stale complete after 2.0, terminal leftover removal, cw-multi-test `.with_migrate` wiring. Plus `test_continue_active_index_migrate_admin_only_rebuild_and_resume`. | | **Do not `Closes #139` until gas is measured** | MR description no longer closes #139. Issue stays open for gas + soak + frontend historical-status. | | **Emergency rebuild** | Documented + implemented: `ContinueActiveIndexMigrate { rebuild: true }` **once**, then `rebuild: false` until complete. INV-TC-AW3, deployment notes, OPERATIONAL_NOTES, skill. | Invariants **INV-TC-AW1–AW4** and [`skills/agent-terraclassic-active-withdrawals.md`](skills/agent-terraclassic-active-withdrawals.md) are updated and cross-linked with deployment + `docs/contracts-terraclassic.md`. ### Not done (out of scope / needs deployed state) - Measured migrate gas on production-sized `PENDING_WITHDRAWS` (issue AC “within tested gas limits”). - Operator/canceler soak with large terminal history. - Manual frontend check that transfer-status still lists executed Terra withdrawals via `pending_withdrawals` (**INV-FE-TC-AW1** is coded; not exercised in a browser here). - Bounding attacker-created **unapproved** spam — remains #138. - Confirming live columbus-5 allows/forbids same-`code_id` wasm migrate (execute path covers both). - Switching the frontend hash monitor to `active_withdrawals` (would hide completed transfers; still must not). ### Tests run (then retested) - `packages/contracts-terraclassic`: full `cargo test -p bridge` (lib, integration, proptest, withdraw flow, new migrate tests). - Retest: `--lib` + `test_active_index_migrate` + `test_withdraw_flow` + `proptest_active_withdraw`. - `packages/operator`: `cargo check --bins`; `cargo test terra`. - `packages/canceler`: `cargo check --bins`; `cargo test --lib`. - `packages/multichain-rs`: `cargo test --lib terra`. - `cargo clippy -p bridge -- -D warnings` (lib). - Pre-commit: fmt, clippy (operator/canceler/terra), frontend typecheck/lint, gitleaks. ### Checklist to verify on review / staging - [ ] **Rollback + re-upgrade:** migrate to 2.1 until complete → wasm back to 2.0.0 (version becomes `2.0.0`) → live submit/approve/execute → re-upgrade to 2.1. First migrate attributes include `active_index_reset=true`. New in-flight hashes appear in `active_withdrawals`; hashes executed during the 2.0 window are **absent** from the index and still present in `pending_withdraw`. - [ ] **Continue without a new code store:** after the first wasm migrate, remaining batches via `{"continue_active_index_migrate":{"limit":50,"rebuild":false}}` until `{"active_withdraw_index":{}}` shows `migration_complete: true`. - [ ] **Emergency rebuild:** `rebuild: true` **once** only, then `rebuild: false`. Canonical rows unchanged. `active_withdrawals` errors (clients fall back) until complete. - [ ] **Unauthorized continue:** non-admin `ContinueActiveIndexMigrate` fails; index unchanged. - [ ] **Skip-capped query:** polluted index returns a short page with `next_start_after` set; operator/canceler keep paging instead of treating it as exhausted. - [ ] **Canceler fallback:** if an active page succeeds and a later active page fails, cancel candidates are not duplicated on the legacy walk. - [ ] **Frontend:** transfer status still discovers executed Terra hashes via `pending_withdrawals` + `pending_withdraw` (do not point the monitor at `active_withdrawals`). - [ ] **Replay:** after execute (index removal) and after migrate, duplicate hash and used `(src_chain, nonce)` still rejected. - [ ] **Gas (follow-up on #139):** measure a 50-row batch against realistic history before mainnet migrate; keep #139 open until that number exists.
PlasticDigits commented 2026-08-31 11:38:18 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-31 12:20:17 +00:00 (Migrated from gitlab.com)

added 7 commits

  • d5aeda49...a5a7620c - 6 commits from branch main
  • 9518fabf - chore: merge main into fix/gl-139-terra-active-withdrawals (active index + writer isolation)

Compare with previous version

added 7 commits <ul><li>d5aeda49...a5a7620c - 6 commits from branch <code>main</code></li><li>9518fabf - chore: merge main into fix/gl-139-terra-active-withdrawals (active index + writer isolation)</li></ul> [Compare with previous version](/PlasticDigits/cl8y-bridge-monorepo/-/merge_requests/18/diffs?diff_id=1994058311&start_sha=d5aeda493d963248a4bb31771f80a2220ffac895)
PlasticDigits commented 2026-08-31 12:31:51 +00:00 (Migrated from gitlab.com)

added 4 commits

  • 9518fabf...50b593d4 - 3 commits from branch main
  • 0a2c0167 - chore: merge main into fix/gl-139-terra-active-withdrawals (token-rank docs)

Compare with previous version

added 4 commits <ul><li>9518fabf...50b593d4 - 3 commits from branch <code>main</code></li><li>0a2c0167 - chore: merge main into fix/gl-139-terra-active-withdrawals (token-rank docs)</li></ul> [Compare with previous version](/PlasticDigits/cl8y-bridge-monorepo/-/merge_requests/18/diffs?diff_id=1994089251&start_sha=9518fabfec674aacc82c3306c558120ef9342b7a)
PlasticDigits commented 2026-08-31 12:32:28 +00:00 (Migrated from gitlab.com)

mentioned in commit 9e820004dc

mentioned in commit 9e820004dc1cf9d6316cd32d6c256b8c95128acf
PlasticDigits (Migrated from gitlab.com) merged commit 9e820004dc into main 2026-08-31 12:32:28 +00:00
PlasticDigits commented 2026-08-31 12:39:12 +00:00 (Migrated from gitlab.com)

mentioned in issue #139

mentioned in issue #139
PlasticDigits commented 2026-09-01 08:00:22 +00:00 (Migrated from gitlab.com)

mentioned in merge request !163

mentioned in merge request !163
Sign in to join this conversation.
No description provided.