bug(terraclassic): stop pending-withdrawal polling from scaling with terminal history #139

Open
opened 2026-08-31 04:29:54 +00:00 by PlasticDigits · 17 comments
PlasticDigits commented 2026-08-31 04:29:54 +00:00 (Migrated from gitlab.com)

Summary

Terra Classic's PendingWithdrawals query paginates the complete PENDING_WITHDRAWS history, including executed and cancelled records. Completed records remain in the same map indefinitely, so operator and canceler polling cost grows monotonically with bridge usage. The operator consequently downloads, parses, and logs historical terminal entries every cycle even when there is no actionable Terra withdrawal.

Implement a bounded active-withdrawal index/query and lifecycle maintenance while preserving replay protection, single-hash status/history, uncancel semantics, cursor correctness, and backwards compatibility. This contract/state change is separated from the operator RPC livelock issue because it requires CosmWasm migration and deployment planning.

Companion operator-side RPC fallback, cursor backoff, and stale-unapproved retry protection is tracked in #138.


Current codebase

Contract state and lifecycle

  • packages/contracts-terraclassic/bridge/src/state.rs stores every submitted withdrawal in PENDING_WITHDRAWS keyed by the 32-byte cross-chain hash.
  • execute_withdraw_submit rejects an existing hash using PENDING_WITHDRAWS and also checks WITHDRAW_NONCE_USED for already-approved (src_chain, nonce) pairs.
  • Approve, cancel, uncancel, and both execute paths update status flags by saving the full record back to PENDING_WITHDRAWS.
  • Execute does not remove the terminal record or remove it from a separate active set because no active set exists.
  • Cancelled records are not necessarily terminal: the operator can call WithdrawUncancel, so indiscriminate deletion on cancel would break current semantics.

Queries

  • packages/contracts-terraclassic/bridge/src/query.rs::query_pending_withdraw reads the canonical record by hash and returns status/details used by clients.
  • query_pending_withdrawals performs an ascending range over the entire PENDING_WITHDRAWS map and returns entries regardless of status.
  • The source contains a TODO noting that executed/cancelled entries remain, increase pagination cost, and cause unbounded state growth.
  • QueryMsg::PendingWithdrawals and existing tests explicitly describe/verify the current all-status behavior, so silently changing its semantics can break operator, canceler, frontend, scripts, and external clients.

Operator behavior

  • packages/operator/src/writers/terra.rs::poll_and_approve starts at the first page every cycle and follows hash cursors until the returned page has fewer than the maximum page size.
  • The operator filters statuses client-side. It still transfers and parses every historical record and emits repeated per-entry debug messages for completed items.
  • Poll cost therefore scales with total lifetime withdrawals rather than actionable withdrawals.

Why a new implementation is needed

The current state/query model makes steady-state operator work grow forever even when the bridge has no pending action. This increases LCD load, operator CPU/network/log use, and query latency. It can eventually make operators/cancelers slow enough to miss expected response windows.

Changing only the operator to filter locally cannot fix LCD query/storage iteration cost. Filtering the existing map inside the query may still scan large historical ranges before finding active records. A dedicated active index (or equivalent bounded index design) is needed for work proportional to actionable state.

The implementation must be migration-safe. Removing canonical records without a tombstone/history design could re-enable replay, make PendingWithdraw falsely report nonexistence, lose audit/status data, or break cancelled-withdrawal uncancel behavior.


Constraints and guardrails

  1. Preserve replay protection: an executed/cancelled/archived hash and an approved (src_chain, nonce) must not become resubmittable because active state was removed.
  2. Preserve canonical status: single-hash queries must continue to report the expected withdrawal details/status, or a versioned replacement and client migration must be provided.
  3. Preserve uncancel semantics: cancelled entries cannot be treated as terminal/deleted while WithdrawUncancel is supported.
  4. Watchtower safety: operators and cancelers must see every state relevant to approval, cancellation, uncancellation, and execution within the required window.
  5. Cursor correctness: pagination must be deterministic, duplicate-free, and omission-free under insertions and lifecycle transitions.
  6. Backwards compatibility: do not silently repurpose PendingWithdrawals if deployed clients expect all statuses. Prefer a versioned ActiveWithdrawals query/index or an explicit status-filter API with a documented rollout.
  7. Bounded migration: migration must not exceed chain gas limits for large historical state. Provide pagination/batching/resume/idempotency if one-shot reconstruction is not provably safe.
  8. No privileged data loss: cleanup/pruning must not give an admin/operator a way to erase actionable withdrawals or evidence.
  9. Fail closed: corrupt/missing index state must not cause unsafe approvals or executions.
  10. State growth remains explicit: an active index fixes terminal-history polling but not attacker-created unapproved spam by itself; coordinate bounded operator retry behavior with the companion operator polling issue.

Relevant files

Area Files
Withdrawal state packages/contracts-terraclassic/bridge/src/state.rs
Submit/approve/cancel/uncancel/execute lifecycle packages/contracts-terraclassic/bridge/src/execute/withdraw.rs
List and single-hash queries packages/contracts-terraclassic/bridge/src/query.rs
Query/execute/migrate messages and response schemas packages/contracts-terraclassic/bridge/src/msg.rs
Entry routing and migration packages/contracts-terraclassic/bridge/src/contract.rs, migration modules/scripts
Contract tests packages/contracts-terraclassic/bridge/tests/test_withdraw_flow.rs, test_hash_parity.rs
Operator Terra poller packages/operator/src/writers/terra.rs
Canceler/frontend consumers packages/canceler/, packages/frontend/, packages/multichain-rs/src/terra/
Deployment/migration docs docs/deployment-terraclassic-upgrade.md, docs/contracts-terraclassic.md

  1. Keep PENDING_WITHDRAWS (or a renamed/versioned canonical history map) as the authoritative by-hash record unless a separate tombstone/history design fully preserves replay and status semantics.
  2. Add an ACTIVE_WITHDRAW_HASHES-style index keyed by hash, or an indexed structure with equivalent bounded range behavior. Define precisely which states are active for each consumer:
    • unapproved/non-cancelled: operator approval candidates;
    • approved/non-cancelled/non-executed: canceler verification and execution candidates;
    • cancelled: retained canonically for uncancel, but excluded from ordinary action polling unless a dedicated consumer needs them;
    • executed: terminal and excluded from the active index.
  3. Add a versioned ActiveWithdrawals query (or explicit status-specific queries) that ranges the active index directly. Do not implement active filtering by scanning the entire historical map.
  4. Update every lifecycle transition atomically with the canonical record and index. Submit inserts; approve keeps/updates; cancel removes from ordinary active polling; uncancel reinserts; execute removes. A transaction failure must roll back both.
  5. Migrate existing state by reconstructing the index from canonical records. Make migration idempotent, observable, and bounded/batched if state size can exceed safe migrate gas.
  6. Update operator/canceler consumers to prefer the active query, with an explicit compatibility strategy for older deployed contract versions. Reduce terminal per-entry logging and retain cycle summaries/metrics.
  7. Add index-consistency invariants and an admin-free repair/rebuild strategy suitable for deterministic migration; avoid a discretionary privileged delete path.
  8. Document storage-retention semantics, client compatibility, deployment order, rollback behavior, and how old/new binaries behave during a rolling upgrade.

Acceptance criteria

  • Actionable list queries iterate an active index and have work proportional to active results/pages, not total historical withdrawals.
  • Executed withdrawals are removed from the active index while canonical replay/status evidence remains correct.
  • Cancelled withdrawals retain the data required for authorized uncancel and are restored to active polling on uncancel.
  • Submit, approve, cancel, uncancel, unlock execute, and mint execute update canonical state and index atomically.
  • Duplicate hash and used (src_chain, nonce) submissions remain rejected after archival/index removal and after migration.
  • Single-hash status remains compatible or all clients are migrated to a documented versioned replacement.
  • Existing all-status query behavior is either preserved or versioned with an explicit compatibility/deprecation plan; no silent semantic break.
  • Pagination remains deterministic with no duplicates/omissions at page boundaries.
  • Existing deployed state can be migrated safely and idempotently within tested gas limits, including resumable batching if required.
  • Operator and canceler use active/status-specific queries and do not rescan/log terminal history during steady state.
  • Metrics/logs expose active count, migration/index consistency failures, and query/poll summaries without user-identifying data.
  • Contract schema, operator/canceler compatibility, and deployment/rollback steps are documented.

Test plan: lifecycle, migration, and client paths

  1. Empty state: canonical and active queries return empty responses.
  2. Submit: canonical record exists, active query includes it once, duplicate hash is rejected.
  3. Approve: record remains visible to the required watchtower/execution consumers with correct cancel-window fields.
  4. Cancel: ordinary active query excludes it; single-hash query reports cancelled; unauthorized cancel still fails.
  5. Uncancel: record is reinserted exactly once and cancel window resets according to current rules.
  6. Execute unlock: canonical record reports executed, active index excludes it, balance/stats behavior unchanged.
  7. Execute mint: same index/status assertions as unlock; mint behavior unchanged.
  8. Replay: resubmit executed hash and reused (src_chain, nonce) after index removal/migration; both remain rejected.
  9. Pagination: zero, one, exact-page, multi-page, maximum limit, invalid cursor, insertion between pages, and lifecycle removal between pages.
  10. Status mixes: many unapproved, approved, cancelled, uncancelled, and executed entries; each consumer query sees exactly its intended set.
  11. Migration: empty, only active, only terminal, cancelled, mixed, large-history, interrupted batch, resumed batch, and repeated/idempotent migration.
  12. Compatibility: old query JSON against new contract and new client against old contract according to rollout plan.
  13. Operator integration: active entries are approved/executed correctly; a large terminal history causes no terminal-record polling.
  14. Canceler integration: approved active entries remain visible throughout the cancellation window; cancel and uncancel transitions are observed.
  15. Frontend/status integration: historical transfer status remains available where promised.
  16. Gas/scale: benchmark query and migration gas with realistic and stress-scale history/active ratios.

Test plan: attack, hack, and abuse vectors

Vector Expected result
Replay after executed record leaves active index Duplicate hash and used nonce remain rejected; no second payout
Cancel then cleanup then unauthorized resubmit/uncancel Canonical status and RBAC prevent state reset or unauthorized transition
Attacker creates many unapproved withdrawals Pagination and operator retry work remain bounded; active-index/cache limits and metrics expose pressure; legitimate work is not starved
Crafted cursors at missing/removed keys Query is deterministic, bounded, and does not duplicate/omit eligible records beyond documented pagination consistency
State transition during pagination Consumer safely re-polls; no permanent omission or double approval/execution
Malicious/compromised admin attempts cleanup of actionable state No privileged arbitrary deletion path; lifecycle authorization and audit evidence remain intact
Migration interrupted or replayed Idempotent resume produces one correct index entry per eligible canonical record
Huge historical map attempts migrate-gas exhaustion Bounded batch/resume path stays under measured limits and cannot brick future migration
Corrupt/missing active index entry Detection/repair fails closed; no unsafe approval or execution
Index entry without canonical record Consumer/query rejects or skips safely and emits a bounded consistency signal; no panic
Query-limit abuse Maximum page size remains capped and execution cost stays bounded

Verification criteria

  • State-machine/property tests assert the canonical-record/index invariant across randomized submit/approve/cancel/uncancel/execute sequences.
  • Replay and RBAC security tests pass for both execute modes and all terminal/index-removal paths.
  • Migration tests compare expected active membership before/after migration and prove repeat/resume idempotency.
  • Scale tests show active-query work does not increase materially when terminal history grows while active count remains fixed.
  • An operator/canceler integration soak with substantial terminal history emits only bounded summary logs and polls only actionable/status-relevant records.
  • Existing CosmWasm unit/integration tests, hash parity tests, operator tests, canceler tests, and Terra cross-chain E2E paths pass.
  • Deployment review confirms version compatibility, migration gas evidence, rollout order, rollback plan, and preservation of historical/status/replay semantics.
## Summary Terra Classic's `PendingWithdrawals` query paginates the complete `PENDING_WITHDRAWS` history, including executed and cancelled records. Completed records remain in the same map indefinitely, so operator and canceler polling cost grows monotonically with bridge usage. The operator consequently downloads, parses, and logs historical terminal entries every cycle even when there is no actionable Terra withdrawal. Implement a bounded active-withdrawal index/query and lifecycle maintenance while preserving replay protection, single-hash status/history, uncancel semantics, cursor correctness, and backwards compatibility. This contract/state change is separated from the operator RPC livelock issue because it requires CosmWasm migration and deployment planning. Companion operator-side RPC fallback, cursor backoff, and stale-unapproved retry protection is tracked in #138. --- ## Current codebase ### Contract state and lifecycle - `packages/contracts-terraclassic/bridge/src/state.rs` stores every submitted withdrawal in `PENDING_WITHDRAWS` keyed by the 32-byte cross-chain hash. - `execute_withdraw_submit` rejects an existing hash using `PENDING_WITHDRAWS` and also checks `WITHDRAW_NONCE_USED` for already-approved `(src_chain, nonce)` pairs. - Approve, cancel, uncancel, and both execute paths update status flags by saving the full record back to `PENDING_WITHDRAWS`. - Execute does not remove the terminal record or remove it from a separate active set because no active set exists. - Cancelled records are not necessarily terminal: the operator can call `WithdrawUncancel`, so indiscriminate deletion on cancel would break current semantics. ### Queries - `packages/contracts-terraclassic/bridge/src/query.rs::query_pending_withdraw` reads the canonical record by hash and returns status/details used by clients. - `query_pending_withdrawals` performs an ascending range over the entire `PENDING_WITHDRAWS` map and returns entries regardless of status. - The source contains a TODO noting that executed/cancelled entries remain, increase pagination cost, and cause unbounded state growth. - `QueryMsg::PendingWithdrawals` and existing tests explicitly describe/verify the current all-status behavior, so silently changing its semantics can break operator, canceler, frontend, scripts, and external clients. ### Operator behavior - `packages/operator/src/writers/terra.rs::poll_and_approve` starts at the first page every cycle and follows hash cursors until the returned page has fewer than the maximum page size. - The operator filters statuses client-side. It still transfers and parses every historical record and emits repeated per-entry debug messages for completed items. - Poll cost therefore scales with total lifetime withdrawals rather than actionable withdrawals. --- ## Why a new implementation is needed The current state/query model makes steady-state operator work grow forever even when the bridge has no pending action. This increases LCD load, operator CPU/network/log use, and query latency. It can eventually make operators/cancelers slow enough to miss expected response windows. Changing only the operator to filter locally cannot fix LCD query/storage iteration cost. Filtering the existing map inside the query may still scan large historical ranges before finding active records. A dedicated active index (or equivalent bounded index design) is needed for work proportional to actionable state. The implementation must be migration-safe. Removing canonical records without a tombstone/history design could re-enable replay, make `PendingWithdraw` falsely report nonexistence, lose audit/status data, or break cancelled-withdrawal uncancel behavior. --- ## Constraints and guardrails 1. **Preserve replay protection:** an executed/cancelled/archived hash and an approved `(src_chain, nonce)` must not become resubmittable because active state was removed. 2. **Preserve canonical status:** single-hash queries must continue to report the expected withdrawal details/status, or a versioned replacement and client migration must be provided. 3. **Preserve uncancel semantics:** cancelled entries cannot be treated as terminal/deleted while `WithdrawUncancel` is supported. 4. **Watchtower safety:** operators and cancelers must see every state relevant to approval, cancellation, uncancellation, and execution within the required window. 5. **Cursor correctness:** pagination must be deterministic, duplicate-free, and omission-free under insertions and lifecycle transitions. 6. **Backwards compatibility:** do not silently repurpose `PendingWithdrawals` if deployed clients expect all statuses. Prefer a versioned `ActiveWithdrawals` query/index or an explicit status-filter API with a documented rollout. 7. **Bounded migration:** migration must not exceed chain gas limits for large historical state. Provide pagination/batching/resume/idempotency if one-shot reconstruction is not provably safe. 8. **No privileged data loss:** cleanup/pruning must not give an admin/operator a way to erase actionable withdrawals or evidence. 9. **Fail closed:** corrupt/missing index state must not cause unsafe approvals or executions. 10. **State growth remains explicit:** an active index fixes terminal-history polling but not attacker-created unapproved spam by itself; coordinate bounded operator retry behavior with the companion operator polling issue. --- ## Relevant files | Area | Files | |---|---| | Withdrawal state | `packages/contracts-terraclassic/bridge/src/state.rs` | | Submit/approve/cancel/uncancel/execute lifecycle | `packages/contracts-terraclassic/bridge/src/execute/withdraw.rs` | | List and single-hash queries | `packages/contracts-terraclassic/bridge/src/query.rs` | | Query/execute/migrate messages and response schemas | `packages/contracts-terraclassic/bridge/src/msg.rs` | | Entry routing and migration | `packages/contracts-terraclassic/bridge/src/contract.rs`, migration modules/scripts | | Contract tests | `packages/contracts-terraclassic/bridge/tests/test_withdraw_flow.rs`, `test_hash_parity.rs` | | Operator Terra poller | `packages/operator/src/writers/terra.rs` | | Canceler/frontend consumers | `packages/canceler/`, `packages/frontend/`, `packages/multichain-rs/src/terra/` | | Deployment/migration docs | `docs/deployment-terraclassic-upgrade.md`, `docs/contracts-terraclassic.md` | --- ## Recommended direction 1. Keep `PENDING_WITHDRAWS` (or a renamed/versioned canonical history map) as the authoritative by-hash record unless a separate tombstone/history design fully preserves replay and status semantics. 2. Add an `ACTIVE_WITHDRAW_HASHES`-style index keyed by hash, or an indexed structure with equivalent bounded range behavior. Define precisely which states are active for each consumer: - unapproved/non-cancelled: operator approval candidates; - approved/non-cancelled/non-executed: canceler verification and execution candidates; - cancelled: retained canonically for uncancel, but excluded from ordinary action polling unless a dedicated consumer needs them; - executed: terminal and excluded from the active index. 3. Add a versioned `ActiveWithdrawals` query (or explicit status-specific queries) that ranges the active index directly. Do not implement active filtering by scanning the entire historical map. 4. Update every lifecycle transition atomically with the canonical record and index. Submit inserts; approve keeps/updates; cancel removes from ordinary active polling; uncancel reinserts; execute removes. A transaction failure must roll back both. 5. Migrate existing state by reconstructing the index from canonical records. Make migration idempotent, observable, and bounded/batched if state size can exceed safe migrate gas. 6. Update operator/canceler consumers to prefer the active query, with an explicit compatibility strategy for older deployed contract versions. Reduce terminal per-entry logging and retain cycle summaries/metrics. 7. Add index-consistency invariants and an admin-free repair/rebuild strategy suitable for deterministic migration; avoid a discretionary privileged delete path. 8. Document storage-retention semantics, client compatibility, deployment order, rollback behavior, and how old/new binaries behave during a rolling upgrade. --- ## Acceptance criteria - [ ] Actionable list queries iterate an active index and have work proportional to active results/pages, not total historical withdrawals. - [ ] Executed withdrawals are removed from the active index while canonical replay/status evidence remains correct. - [ ] Cancelled withdrawals retain the data required for authorized uncancel and are restored to active polling on uncancel. - [ ] Submit, approve, cancel, uncancel, unlock execute, and mint execute update canonical state and index atomically. - [ ] Duplicate hash and used `(src_chain, nonce)` submissions remain rejected after archival/index removal and after migration. - [ ] Single-hash status remains compatible or all clients are migrated to a documented versioned replacement. - [ ] Existing all-status query behavior is either preserved or versioned with an explicit compatibility/deprecation plan; no silent semantic break. - [ ] Pagination remains deterministic with no duplicates/omissions at page boundaries. - [ ] Existing deployed state can be migrated safely and idempotently within tested gas limits, including resumable batching if required. - [ ] Operator and canceler use active/status-specific queries and do not rescan/log terminal history during steady state. - [ ] Metrics/logs expose active count, migration/index consistency failures, and query/poll summaries without user-identifying data. - [ ] Contract schema, operator/canceler compatibility, and deployment/rollback steps are documented. --- ## Test plan: lifecycle, migration, and client paths 1. **Empty state:** canonical and active queries return empty responses. 2. **Submit:** canonical record exists, active query includes it once, duplicate hash is rejected. 3. **Approve:** record remains visible to the required watchtower/execution consumers with correct cancel-window fields. 4. **Cancel:** ordinary active query excludes it; single-hash query reports cancelled; unauthorized cancel still fails. 5. **Uncancel:** record is reinserted exactly once and cancel window resets according to current rules. 6. **Execute unlock:** canonical record reports executed, active index excludes it, balance/stats behavior unchanged. 7. **Execute mint:** same index/status assertions as unlock; mint behavior unchanged. 8. **Replay:** resubmit executed hash and reused `(src_chain, nonce)` after index removal/migration; both remain rejected. 9. **Pagination:** zero, one, exact-page, multi-page, maximum limit, invalid cursor, insertion between pages, and lifecycle removal between pages. 10. **Status mixes:** many unapproved, approved, cancelled, uncancelled, and executed entries; each consumer query sees exactly its intended set. 11. **Migration:** empty, only active, only terminal, cancelled, mixed, large-history, interrupted batch, resumed batch, and repeated/idempotent migration. 12. **Compatibility:** old query JSON against new contract and new client against old contract according to rollout plan. 13. **Operator integration:** active entries are approved/executed correctly; a large terminal history causes no terminal-record polling. 14. **Canceler integration:** approved active entries remain visible throughout the cancellation window; cancel and uncancel transitions are observed. 15. **Frontend/status integration:** historical transfer status remains available where promised. 16. **Gas/scale:** benchmark query and migration gas with realistic and stress-scale history/active ratios. --- ## Test plan: attack, hack, and abuse vectors | Vector | Expected result | |---|---| | Replay after executed record leaves active index | Duplicate hash and used nonce remain rejected; no second payout | | Cancel then cleanup then unauthorized resubmit/uncancel | Canonical status and RBAC prevent state reset or unauthorized transition | | Attacker creates many unapproved withdrawals | Pagination and operator retry work remain bounded; active-index/cache limits and metrics expose pressure; legitimate work is not starved | | Crafted cursors at missing/removed keys | Query is deterministic, bounded, and does not duplicate/omit eligible records beyond documented pagination consistency | | State transition during pagination | Consumer safely re-polls; no permanent omission or double approval/execution | | Malicious/compromised admin attempts cleanup of actionable state | No privileged arbitrary deletion path; lifecycle authorization and audit evidence remain intact | | Migration interrupted or replayed | Idempotent resume produces one correct index entry per eligible canonical record | | Huge historical map attempts migrate-gas exhaustion | Bounded batch/resume path stays under measured limits and cannot brick future migration | | Corrupt/missing active index entry | Detection/repair fails closed; no unsafe approval or execution | | Index entry without canonical record | Consumer/query rejects or skips safely and emits a bounded consistency signal; no panic | | Query-limit abuse | Maximum page size remains capped and execution cost stays bounded | --- ## Verification criteria - State-machine/property tests assert the canonical-record/index invariant across randomized submit/approve/cancel/uncancel/execute sequences. - Replay and RBAC security tests pass for both execute modes and all terminal/index-removal paths. - Migration tests compare expected active membership before/after migration and prove repeat/resume idempotency. - Scale tests show active-query work does not increase materially when terminal history grows while active count remains fixed. - An operator/canceler integration soak with substantial terminal history emits only bounded summary logs and polls only actionable/status-relevant records. - Existing CosmWasm unit/integration tests, hash parity tests, operator tests, canceler tests, and Terra cross-chain E2E paths pass. - Deployment review confirms version compatibility, migration gas evidence, rollout order, rollback plan, and preservation of historical/status/replay semantics.
PlasticDigits commented 2026-08-31 04:30:18 +00:00 (Migrated from gitlab.com)

mentioned in issue #138

mentioned in issue #138
PlasticDigits commented 2026-08-31 04:30:20 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-31 04:30:45 +00:00 (Migrated from gitlab.com)

marked as related to #138

marked as related to #138
PlasticDigits commented 2026-08-31 05:57:26 +00:00 (Migrated from gitlab.com)

mentioned in commit 2fdb194052

mentioned in commit 2fdb194052b5ceb763bda0147421cf7c069af73d
PlasticDigits commented 2026-08-31 05:57:49 +00:00 (Migrated from gitlab.com)

mentioned in merge request !158

mentioned in merge request !158
PlasticDigits commented 2026-08-31 11:24:44 +00:00 (Migrated from gitlab.com)

mentioned in merge request !160

mentioned in merge request !160
PlasticDigits commented 2026-08-31 11:37:27 +00:00 (Migrated from gitlab.com)

mentioned in commit d5aeda493d

mentioned in commit d5aeda493d963248a4bb31771f80a2220ffac895
PlasticDigits commented 2026-08-31 12:32:28 +00:00 (Migrated from gitlab.com)

mentioned in commit 9e820004dc

mentioned in commit 9e820004dc1cf9d6316cd32d6c256b8c95128acf
PlasticDigits commented 2026-08-31 12:39:11 +00:00 (Migrated from gitlab.com)

Merge status (!158 landed on main)

GitLab auto-closed this issue from the merge-commit closing keyword even though the MR was updated to Related #139. Reopened. Do not close again until the remaining AC is evidenced.

Landed

Active index ACTIVE_WITHDRAW_HASHES, lifecycle sync, rollback→2.0→re-upgrade rebuild, ContinueActiveIndexMigrate, skip-capped query, canceler fallback clears accumulators, operator/canceler prefer-active + legacy fallback.

Remaining (issue AC)

  • Measured on-chain migrate gas on production-sized PENDING_WITHDRAWS (batch 50 / cap 100 — needs a real LCD number)
  • Operator/canceler soak against a contract with substantial terminal history
  • Manual frontend check: transfer-status still lists executed Terra withdrawals via pending_withdraw / pending_withdrawals (INV-FE-TC-AW1 — do not point the monitor at active_withdrawals)
  • Confirm live columbus-5 same-code_id wasm migrate vs admin continue path
## Merge status (!158 landed on `main`) GitLab auto-closed this issue from the merge-commit closing keyword even though the MR was updated to **Related #139**. **Reopened.** Do not close again until the remaining AC is evidenced. ### Landed Active index `ACTIVE_WITHDRAW_HASHES`, lifecycle sync, rollback→2.0→re-upgrade rebuild, `ContinueActiveIndexMigrate`, skip-capped query, canceler fallback clears accumulators, operator/canceler prefer-active + legacy fallback. ### Remaining (issue AC) - [ ] Measured on-chain migrate gas on production-sized `PENDING_WITHDRAWS` (batch 50 / cap 100 — needs a real LCD number) - [ ] Operator/canceler soak against a contract with substantial terminal history - [ ] Manual frontend check: transfer-status still lists executed Terra withdrawals via `pending_withdraw` / `pending_withdrawals` (INV-FE-TC-AW1 — do not point the monitor at `active_withdrawals`) - [ ] Confirm live columbus-5 same-`code_id` wasm migrate vs admin continue path
PlasticDigits commented 2026-08-31 12:39:32 +00:00 (Migrated from gitlab.com)

mentioned in issue #140

mentioned in issue #140
PlasticDigits commented 2026-09-01 07:34:40 +00:00 (Migrated from gitlab.com)

mentioned in commit 21d03a523c

mentioned in commit 21d03a523c1a8d2243e798f5627329534e962227
PlasticDigits commented 2026-09-01 07:35:05 +00:00 (Migrated from gitlab.com)

mentioned in merge request !163

mentioned in merge request !163
PlasticDigits commented 2026-09-01 07:35:21 +00:00 (Migrated from gitlab.com)

Follow-up MR: !163 (fix/gl-139-migrate-gas-evidence). Does not close this issue.

Remaining notes from the reopen comment

  • Measured on-chain migrate gas on production-sized PENDING_WITHDRAWS (batch 50 / cap 100 — needs a real LCD gas_used). Not done: columbus-5 bridge is still v2.0 / code_id 10971; no migrate was broadcast. Offline evidence: 106 canonical rows reconstruct in 3×50 or 2×100 (test_active_index_scale.rs). Sample live execute txs that day used ~158–257k gas (gas_wanted 500k).
  • Operator/canceler soak against a deployed v2.1 contract with substantial terminal history. Not done (no v2.1 on chain yet). Unit soak: 106-row mix is 4 legacy pages / 1 active page; 2000 terminal + 11 active still one active page.
  • Frontend historical listing stays on pending_withdrawals / pending_withdraw (INV-FE-TC-AW1). Done in code + unit tests. Live SPA click-through still pending after deploy. Also fixed: page size 50 + len < 50 EOF dropped history after the first 30 rows (contract cap).
  • Live columbus-5 same-code_id wasm migrate vs admin continue: LCD 2026-09-01, terrad 4.0.1 / wasmd v0.61.8 does not reject same code_id; CosmWasm 1.5 ContractMigrateVersion is nil so migrate is invoked. Repeat wasm migrate until active_index_complete=true. Keep ContinueActiveIndexMigrate if a future chain upgrade rejects same-code migrate.

See INV-TC-AW3 / INV-TC-AW5 and skills/agent-terraclassic-active-withdrawals.md.

Follow-up MR: !163 (`fix/gl-139-migrate-gas-evidence`). Does **not** close this issue. ### Remaining notes from the reopen comment - [ ] Measured on-chain migrate gas on production-sized `PENDING_WITHDRAWS` (batch 50 / cap 100 — needs a real LCD `gas_used`). **Not done:** columbus-5 bridge is still v2.0 / code_id 10971; no migrate was broadcast. Offline evidence: 106 canonical rows reconstruct in 3×50 or 2×100 (`test_active_index_scale.rs`). Sample live **execute** txs that day used ~158–257k gas (`gas_wanted` 500k). - [ ] Operator/canceler soak against a **deployed** v2.1 contract with substantial terminal history. **Not done** (no v2.1 on chain yet). Unit soak: 106-row mix is 4 legacy pages / 1 active page; 2000 terminal + 11 active still one active page. - [x] Frontend historical listing stays on `pending_withdrawals` / `pending_withdraw` (INV-FE-TC-AW1). **Done in code + unit tests.** Live SPA click-through still pending after deploy. Also fixed: page size 50 + `len < 50` EOF dropped history after the first **30** rows (contract cap). - [x] Live columbus-5 same-`code_id` wasm migrate vs admin continue: LCD 2026-09-01, `terrad` 4.0.1 / wasmd **v0.61.8** does **not** reject same `code_id`; CosmWasm 1.5 `ContractMigrateVersion` is nil so `migrate` **is** invoked. Repeat wasm migrate until `active_index_complete=true`. Keep `ContinueActiveIndexMigrate` if a future chain upgrade rejects same-code migrate. See INV-TC-AW3 / INV-TC-AW5 and `skills/agent-terraclassic-active-withdrawals.md`.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-09-02 01:38:48 +00:00
PlasticDigits commented 2026-09-02 01:38:49 +00:00 (Migrated from gitlab.com)

mentioned in commit 5294018bf3

mentioned in commit 5294018bf3daf3ed5b2649ea5666f34575a3cd3c
PlasticDigits commented 2026-09-02 01:39:18 +00:00 (Migrated from gitlab.com)

!163 merged — remaining #139 gaps (2026-09-02)

Merged !163 into main (no automerge, no CI wait). Git auto-merged docs/FRONTEND_BRIDGE_INVARIANTS.md with !162 (different hunks, no conflict). Local sanity: frontend hashMonitor.test.ts 4/4. Prior review was ACCEPT.

Do not close this issue. Live migrate gas_used and post-deploy soak are still open.

What !163 closed in code

  • Pagination trap: contract cap is 30; clients that requested 50 and treated len < 50 as EOF only saw the first page. Operator, canceler, and hash monitor now clamp to 30 and follow additive next_start_after.
  • Hash monitor stays on pending_withdrawals / pending_withdraw (INV-FE-TC-AW1); it does not switch to active_withdrawals.
  • LCD-sized migrate batch evidence (106 columbus-5 rows: 3×50 or 2×100) is documented. Same-code_id wasm migrate is allowed on wasmd v0.61.8 / CosmWasm 1.5.

Lifecycle / replay / RBAC / fail-closed incomplete index from !158 is unchanged.

Acceptance criteria still open

Item Status
On-chain v2.1 wasm migrate gas_used (batch 50 / cap 100) on production-sized PENDING_WITHDRAWS NOT MET — contract still v2.0 code_id 10971
Operator/canceler soak against deployed v2.1 with substantial terminal history NOT MET — in-memory soak only
Live SPA transfer-status against mainnet (historical listing past 30 rows) NOT MET — unit-tested; needs frontend+canceler deploy with the wasm
Old SPA requesting 50 / len < 50 EOF Fails closed until this frontend ships — will keep listing only the first 30 Terra history rows

Deploy / ops

Ship frontend + canceler together with the wasm. Keep ContinueActiveIndexMigrate as admin fallback. Repeat wasm migrate until active_index_complete=true.

Nice-to-have (not merge blockers)

  • Deduplicate advance_withdraw_list_page into multichain-rs (copied in operator and canceler).
  • Treat JSON null next_start_after as EOF in hashMonitor.ts (may make one extra empty LCD call).
  • Clamp contract query limit to 1..=30 (Some(0) is a footgun).
  • Operator max pages / LCD livelock remains #138 / follow-up #140.
## !163 merged — remaining #139 gaps (2026-09-02) Merged [!163](https://gitlab.com/PlasticDigits/cl8y-bridge-monorepo/-/merge_requests/23) into `main` (no automerge, no CI wait). Git auto-merged `docs/FRONTEND_BRIDGE_INVARIANTS.md` with !162 (different hunks, no conflict). Local sanity: frontend `hashMonitor.test.ts` 4/4. Prior review was ACCEPT. **Do not close this issue.** Live migrate `gas_used` and post-deploy soak are still open. ### What !163 closed in code - Pagination trap: contract cap is **30**; clients that requested **50** and treated `len < 50` as EOF only saw the first page. Operator, canceler, and hash monitor now clamp to 30 and follow additive `next_start_after`. - Hash monitor stays on `pending_withdrawals` / `pending_withdraw` (**INV-FE-TC-AW1**); it does **not** switch to `active_withdrawals`. - LCD-sized migrate batch evidence (106 columbus-5 rows: 3×50 or 2×100) is documented. Same-`code_id` wasm migrate is allowed on wasmd v0.61.8 / CosmWasm 1.5. Lifecycle / replay / RBAC / fail-closed incomplete index from !158 is unchanged. ### Acceptance criteria still open | Item | Status | |------|--------| | On-chain v2.1 `wasm migrate` `gas_used` (batch 50 / cap 100) on production-sized `PENDING_WITHDRAWS` | **NOT MET** — contract still v2.0 `code_id` 10971 | | Operator/canceler soak against **deployed** v2.1 with substantial terminal history | **NOT MET** — in-memory soak only | | Live SPA transfer-status against mainnet (historical listing past 30 rows) | **NOT MET** — unit-tested; needs frontend+canceler deploy with the wasm | | Old SPA requesting 50 / `len < 50` EOF | **Fails closed until this frontend ships** — will keep listing only the first 30 Terra history rows | ### Deploy / ops Ship **frontend + canceler** together with the wasm. Keep `ContinueActiveIndexMigrate` as admin fallback. Repeat wasm migrate until `active_index_complete=true`. ### Nice-to-have (not merge blockers) - Deduplicate `advance_withdraw_list_page` into `multichain-rs` (copied in operator and canceler). - Treat JSON `null` `next_start_after` as EOF in `hashMonitor.ts` (may make one extra empty LCD call). - Clamp contract query `limit` to `1..=30` (`Some(0)` is a footgun). - Operator max pages / LCD livelock remains #138 / follow-up #140.
PlasticDigits (Migrated from gitlab.com) reopened this issue 2026-09-02 01:41:19 +00:00
PlasticDigits commented 2026-09-02 01:41:20 +00:00 (Migrated from gitlab.com)

Reopened after !163 auto-close

GitLab closed this issue when !163 merged (5294018). That was premature. Keep open until live v2.1 work is recorded.

Follow-up sanity after merge (worktree tests, not CI):

  • CosmWasm test_active_index_scale + test_withdraw_flow: 35/35
  • Operator terra_list: 6/6 (oversized-request EOF trap, cursor repair, active soak, terminal-history growth)
  • Frontend hashMonitor.test.ts: 4/4 (already noted)

Still required to close:

  1. On-chain v2.1 wasm migrate gas_used (batch 50 / cap 100) — contract still v2.0 code_id 10971
  2. Operator/canceler soak against deployed v2.1 with substantial terminal history
  3. Live SPA transfer-status past 30 rows — ship frontend + canceler with the wasm
## Reopened after !163 auto-close GitLab closed this issue when !163 merged (`5294018`). That was premature. **Keep open** until live v2.1 work is recorded. Follow-up sanity after merge (worktree tests, not CI): - CosmWasm `test_active_index_scale` + `test_withdraw_flow`: **35/35** - Operator `terra_list`: **6/6** (oversized-request EOF trap, cursor repair, active soak, terminal-history growth) - Frontend `hashMonitor.test.ts`: **4/4** (already noted) Still required to close: 1. On-chain v2.1 `wasm migrate` `gas_used` (batch 50 / cap 100) — contract still v2.0 `code_id` 10971 2. Operator/canceler soak against **deployed** v2.1 with substantial terminal history 3. Live SPA transfer-status past 30 rows — ship frontend + canceler with the wasm

Live v2.1 migrate recorded (columbus-5)

Wasm code_id 11648 stored by cl8y2_admin (sha256 d553d89a85ffb927aec009fb862e12ebc3513d82f1869044489b1270b5d01005, matches local packages/contracts-terraclassic/artifacts/bridge.wasm).

Bridge terra18m02l2f43c2dagqnz3kfccpgz9pzzz5hk9l5mh5wvr6dcvv47zfqdfs7la migrated 10971 → 11648. Same-code_id migrate worked (3 batches of 50). Admin still terra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8l. Bridge was not paused.

Pass height tx gas_wanted (est) gas_used scanned indexed complete
1 30298039 71FE2530229355901CE7CBE084DB7B0782090DFD1E1EE60D286B00318F999B6D 596742 395605 50 6 false
2 30298042 37AEA48B7E9E8384452A2BCE41E54B6889BEF0BCF818E0F46222165866827ED7 570750 379367 100 11 false
3 30298045 F9C7D3A95AEA9E1753C797B9F52487929D704531BF8316E22CEA733C55D90FC4 258392 184264 107 11 true

Post-migrate {"active_withdraw_index":{}}: migration_complete=true, active_count=11, migration_scanned=107, migration_indexed=11. {"active_withdrawals":{"limit":30}} returns 11 rows, next_start_after=null, inconsistent_skipped=0. All 11 are approved / not executed / not cancelled.

Keep open for operator/canceler soak: canceler should now log query_key="active_withdrawals" instead of the LCD unknown-variant fallback to pending_withdrawals.

## Live v2.1 migrate recorded (columbus-5) Wasm **code_id 11648** stored by `cl8y2_admin` (sha256 `d553d89a85ffb927aec009fb862e12ebc3513d82f1869044489b1270b5d01005`, matches local `packages/contracts-terraclassic/artifacts/bridge.wasm`). Bridge `terra18m02l2f43c2dagqnz3kfccpgz9pzzz5hk9l5mh5wvr6dcvv47zfqdfs7la` migrated 10971 → 11648. Same-`code_id` migrate worked (3 batches of 50). Admin still `terra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8l`. Bridge was not paused. | Pass | height | tx | gas_wanted (est) | **gas_used** | scanned | indexed | complete | |------|--------|----|------------------|--------------|---------|---------|----------| | 1 | 30298039 | `71FE2530229355901CE7CBE084DB7B0782090DFD1E1EE60D286B00318F999B6D` | 596742 | **395605** | 50 | 6 | false | | 2 | 30298042 | `37AEA48B7E9E8384452A2BCE41E54B6889BEF0BCF818E0F46222165866827ED7` | 570750 | **379367** | 100 | 11 | false | | 3 | 30298045 | `F9C7D3A95AEA9E1753C797B9F52487929D704531BF8316E22CEA733C55D90FC4` | 258392 | **184264** | 107 | 11 | **true** | Post-migrate `{"active_withdraw_index":{}}`: `migration_complete=true`, `active_count=11`, `migration_scanned=107`, `migration_indexed=11`. `{"active_withdrawals":{"limit":30}}` returns 11 rows, `next_start_after=null`, `inconsistent_skipped=0`. All 11 are approved / not executed / not cancelled. Keep **open** for operator/canceler soak: canceler should now log `query_key="active_withdrawals"` instead of the LCD unknown-variant fallback to `pending_withdrawals`.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-bridge-monorepo#139
No description provided.