bug(operator): prevent EVM writer RPC/cursor livelock and stale-withdrawal retry amplification #138
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-bridge-monorepo#138
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
The EVM writer can enter a persistent polling livelock when an RPC endpoint answers
eth_blockNumberbut rejects or rate-limitseth_getLogs. If the first lookback chunk fails, the writer keepslast_polled_block == 0, repeats the entire first-poll lookback on every writer cycle, and re-enumerates/re-verifies the same unapproved withdrawals. This creates avoidable CPU, network, RPC, and log load and provides a resource-amplification path for stale or attacker-created pending withdrawals.This issue bundles the tightly related operator-side work: method-level RPC fallback, cursor-safe retry/backoff, bounded negative-result retry scheduling, polling configuration, observability, and log-volume reduction.
Prior context: #115 covered broader operator RPC hardening, but not this residual method-level fallback/cursor livelock.
Companion contract/state work for terminal Terra withdrawal history is tracked in #139.
Current codebase
Writer scheduling
packages/operator/src/writers/mod.rs::WriterManager::runuses a hard-coded five-second interval.POLL_INTERVAL_MSconfigures other loops but does not configure this writer-manager interval.EVM event polling and cursor behavior
packages/operator/src/writers/evm.rs::poll_and_approvechooses one provider by tryingget_block_number()against the configured RPC URLs.eth_blockNumber, the writer uses that same provider for everyWithdrawSubmiteth_getLogschunk. A method-specific failure does not try the remaining RPC URLs.last_polled_blockadvances only tolast_successful_block. On an initial first-chunk failure, both remain zero.last_polled_block == 0, every later cycle logs another first poll and computes a freshcurrent_block - EVM_POLL_LOOKBACK_BLOCKSrange. A provider that consistently accepts block-number requests but rejects log queries therefore causes an indefinite retry loop.get_logs_with_fallback; the writer has separate, weaker behavior.Enumeration retry amplification
EvmWriter::process_pendingrunsenumerate_and_approve()before event polling because contract enumeration is the primary safety path.enumerate_and_approve()callsgetPendingWithdrawHashes(), thengetPendingWithdraw()and source-chain verification for every unapproved hash not present inapproved_hashes.new_to_processcurrently counts every still-unapproved hash attempted in that cycle; it does not mean the hash is newly discovered.Logging and observability
packages/operator/src/main.rs::init_loggingdefaults toinfo,cl8y_operator=debugwhenRUST_LOGis absent.Why a new implementation is needed
RPC endpoints commonly apply different limits to
eth_blockNumber,eth_call, andeth_getLogs. Selecting an endpoint with one method is not proof that another method will work. The current behavior can consume a significant fraction of a core indefinitely, inflate RPC/network/log costs, and obscure real bridge activity.The enumeration path also turns every unapproved destination-chain entry into recurring source-chain work. Because withdrawal submission is externally reachable, an attacker willing to pay transaction gas can create persistent work amplification. A degraded RPC plus a growing pending set can delay legitimate approvals and other chains handled by the same writer manager.
This is primarily an availability/resource-exhaustion issue. The fix must retain the bridge's fail-closed approval behavior and must never skip an event range merely to restore progress.
Constraints and guardrails
eth_getLogsrange. Partial success may advance only through the last contiguous successful chunk.Relevant files
packages/operator/src/writers/mod.rspackages/operator/src/writers/evm.rspackages/operator/src/watchers/evm.rspackages/operator/src/rpc_fallback.rs,packages/operator/src/writers/retry.rs,packages/multichain-rs/src/packages/operator/src/config.rs,packages/operator/.env.examplepackages/operator/src/main.rspackages/operator/src/metrics.rs,packages/operator/src/api.rs,packages/operator/src/liveness.rspackages/operator/src/writers/evm.rsand operator integration/E2E suitesRecommended direction
eth_getLogschunk, try the next validated endpoint on transient transport, HTTP, rate-limit, and retryable JSON-RPC errors (including provider-specific limit errors).eth_blockNumberand rebuilding the same first-poll range at the normal cycle rate.attempted_unapproved,newly_discovered,negative_retry_suppressed) instead of describing every attempt as new.info, repeated negatives/routing at sampleddebug/trace, and default production logging atinfo. Add structured metrics for diagnosis.Acceptance criteria
eth_blockNumberbut returns a retryable error foreth_getLogs, the same chunk is attempted against a configured fallback and the cursor advances after success.WithdrawSubmitevent is skipped.Test plan: functional and failure paths
Test plan: attack, hack, and abuse vectors
eth_getLogsVerification criteria
eth_blockNumberand rejectseth_getLogs; the fallback succeeds and the writer reaches head without repeating first poll.changed the description
mentioned in issue #139
marked as related to #139
mentioned in merge request !158
mentioned in commit
fd4edddbd9mentioned in merge request !160
mentioned in commit
0d58eacd72mentioned in commit
6b40cc5819Merge status (!160 landed on
main; this issue auto-closed on Closes)Code AC is met: method-level
eth_getLogsfallback, sticky cursor, jittered backoff, isolated writer loops, bounded negative retry, INV-OP-W9 sanitizer (scheme://host+sanitize_rpc_error).Remaining infra soaks (not in this issue after close)
Tracked in a new follow-up issue (linked): operator restart vs Anvil, real-provider fallback soak, regression E2E Terra→EVM / EVM→EVM / Solana→EVM.
Unapproved on-chain set growth remains #139 / contract-side, not retry amplification.
mentioned in issue #140
marked as related to #140
mentioned in merge request !163