bug(terraclassic): stop pending-withdrawal polling from scaling with terminal history #139
Labels
No labels
agent:implement
agent:ready
backend
bug
cannot-reproduce
confirmed
desktop
docs
documentation
duplicate
enhancement
feature
frontend
good first issue
help wanted
high-risk
in-review
invalid
mobile
needs-triage
P0-critical
P1-high
P2-medium
P3-low
qa
QA
question
ready
report
responsive
security
security-escalate
smart-contract
solana
tablet
test-pass
ux
wallet-issue
wallet:keplr
wallet:metamask
wallet:station
wallet:walletconnect
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-bridge-monorepo#139
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Terra Classic's
PendingWithdrawalsquery paginates the completePENDING_WITHDRAWShistory, 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.rsstores every submitted withdrawal inPENDING_WITHDRAWSkeyed by the 32-byte cross-chain hash.execute_withdraw_submitrejects an existing hash usingPENDING_WITHDRAWSand also checksWITHDRAW_NONCE_USEDfor already-approved(src_chain, nonce)pairs.PENDING_WITHDRAWS.WithdrawUncancel, so indiscriminate deletion on cancel would break current semantics.Queries
packages/contracts-terraclassic/bridge/src/query.rs::query_pending_withdrawreads the canonical record by hash and returns status/details used by clients.query_pending_withdrawalsperforms an ascending range over the entirePENDING_WITHDRAWSmap and returns entries regardless of status.QueryMsg::PendingWithdrawalsand 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_approvestarts at the first page every cycle and follows hash cursors until the returned page has fewer than the maximum page size.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
PendingWithdrawfalsely report nonexistence, lose audit/status data, or break cancelled-withdrawal uncancel behavior.Constraints and guardrails
(src_chain, nonce)must not become resubmittable because active state was removed.WithdrawUncancelis supported.PendingWithdrawalsif deployed clients expect all statuses. Prefer a versionedActiveWithdrawalsquery/index or an explicit status-filter API with a documented rollout.Relevant files
packages/contracts-terraclassic/bridge/src/state.rspackages/contracts-terraclassic/bridge/src/execute/withdraw.rspackages/contracts-terraclassic/bridge/src/query.rspackages/contracts-terraclassic/bridge/src/msg.rspackages/contracts-terraclassic/bridge/src/contract.rs, migration modules/scriptspackages/contracts-terraclassic/bridge/tests/test_withdraw_flow.rs,test_hash_parity.rspackages/operator/src/writers/terra.rspackages/canceler/,packages/frontend/,packages/multichain-rs/src/terra/docs/deployment-terraclassic-upgrade.md,docs/contracts-terraclassic.mdRecommended direction
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.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:ActiveWithdrawalsquery (or explicit status-specific queries) that ranges the active index directly. Do not implement active filtering by scanning the entire historical map.Acceptance criteria
(src_chain, nonce)submissions remain rejected after archival/index removal and after migration.Test plan: lifecycle, migration, and client paths
(src_chain, nonce)after index removal/migration; both remain rejected.Test plan: attack, hack, and abuse vectors
Verification criteria
mentioned in issue #138
changed the description
marked as related to #138
mentioned in commit
2fdb194052mentioned in merge request !158
mentioned in merge request !160
mentioned in commit
d5aeda493dmentioned in commit
9e820004dcMerge 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)
PENDING_WITHDRAWS(batch 50 / cap 100 — needs a real LCD number)pending_withdraw/pending_withdrawals(INV-FE-TC-AW1 — do not point the monitor atactive_withdrawals)code_idwasm migrate vs admin continue pathmentioned in issue #140
mentioned in commit
21d03a523cmentioned in merge request !163
Follow-up MR: !163 (
fix/gl-139-migrate-gas-evidence). Does not close this issue.Remaining notes from the reopen comment
PENDING_WITHDRAWS(batch 50 / cap 100 — needs a real LCDgas_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_wanted500k).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 < 50EOF dropped history after the first 30 rows (contract cap).code_idwasm migrate vs admin continue: LCD 2026-09-01,terrad4.0.1 / wasmd v0.61.8 does not reject samecode_id; CosmWasm 1.5ContractMigrateVersionis nil somigrateis invoked. Repeat wasm migrate untilactive_index_complete=true. KeepContinueActiveIndexMigrateif a future chain upgrade rejects same-code migrate.See INV-TC-AW3 / INV-TC-AW5 and
skills/agent-terraclassic-active-withdrawals.md.mentioned in commit
5294018bf3!163 merged — remaining #139 gaps (2026-09-02)
Merged !163 into
main(no automerge, no CI wait). Git auto-mergeddocs/FRONTEND_BRIDGE_INVARIANTS.mdwith !162 (different hunks, no conflict). Local sanity: frontendhashMonitor.test.ts4/4. Prior review was ACCEPT.Do not close this issue. Live migrate
gas_usedand post-deploy soak are still open.What !163 closed in code
len < 50as EOF only saw the first page. Operator, canceler, and hash monitor now clamp to 30 and follow additivenext_start_after.pending_withdrawals/pending_withdraw(INV-FE-TC-AW1); it does not switch toactive_withdrawals.code_idwasm 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
wasm migrategas_used(batch 50 / cap 100) on production-sizedPENDING_WITHDRAWScode_id10971len < 50EOFDeploy / ops
Ship frontend + canceler together with the wasm. Keep
ContinueActiveIndexMigrateas admin fallback. Repeat wasm migrate untilactive_index_complete=true.Nice-to-have (not merge blockers)
advance_withdraw_list_pageintomultichain-rs(copied in operator and canceler).nullnext_start_afteras EOF inhashMonitor.ts(may make one extra empty LCD call).limitto1..=30(Some(0)is a footgun).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):
test_active_index_scale+test_withdraw_flow: 35/35terra_list: 6/6 (oversized-request EOF trap, cursor repair, active soak, terminal-history growth)hashMonitor.test.ts: 4/4 (already noted)Still required to close:
wasm migrategas_used(batch 50 / cap 100) — contract still v2.0code_id10971Live v2.1 migrate recorded (columbus-5)
Wasm code_id 11648 stored by
cl8y2_admin(sha256d553d89a85ffb927aec009fb862e12ebc3513d82f1869044489b1270b5d01005, matches localpackages/contracts-terraclassic/artifacts/bridge.wasm).Bridge
terra18m02l2f43c2dagqnz3kfccpgz9pzzz5hk9l5mh5wvr6dcvv47zfqdfs7lamigrated 10971 → 11648. Same-code_idmigrate worked (3 batches of 50). Admin stillterra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8l. Bridge was not paused.71FE2530229355901CE7CBE084DB7B0782090DFD1E1EE60D286B00318F999B6D37AEA48B7E9E8384452A2BCE41E54B6889BEF0BCF818E0F46222165866827ED7F9C7D3A95AEA9E1753C797B9F52487929D704531BF8316E22CEA733C55D90FC4Post-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 topending_withdrawals.