Security review: operator RPC hardening findings (mirrors ust1-window sweep) #115
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#115
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?
@PlasticDigits parallel security check on operator following the ust1-window 40-category sweep and matching the canceler review in #114.
Same guidance from you: launch-blocking status none at medium severity. Filing as single summary issue.
Findings
EVM-01: Sequential RPC fallback, not multi-provider cross-check — Medium
operator/src/watchers/evm.rs:715uses the same sequential fallback loop as canceler ("get_block_number succeeded on fallback RPC #{i+1}"). First responsive RPC wins, no consensus check.Recommendation: mirror ust1-window 8c9fafb — parallel first two URLs + 0.01% tolerance + tiebreaker.
EVM-02:
finality_blocksdefault is weak — Mediumoperatoralready has the reorg-protection mechanic:operator/src/watchers/evm.rs:722returnsblock.saturating_sub(self.finality_blocks). Good.But:
default_finality_blocks() = 1inoperator/src/config.rs:297. 1 block is insufficient for BSC, which has seen 6-12 block reorgs historically. opBNB similar.Recommendation: raise default to something chain-appropriate (BSC ≥ 15, opBNB ≥ 12), or add per-chain defaults based on
chain_id. Existing mechanic is correct — just needs tuning.EVM-03: No canonical bridge address allowlist — Low-Med
Same as canceler:
EVM_BRIDGE_ADDRESSaccepted as any hex string, no canonical-address check at startup.Recommendation: same as canceler — startup check against a compile-time constant or allowlist, scoped per deployment environment.
EVM-08: HTTPS validation warns but doesn't block — Low-Med
Operator uses
multichain_rs::validate_rpc_url— shared helper that acceptshttp://(tested to atmultichain-rs/src/multi_evm.rs:632-633). Same weakness as canceler.Recommendation: fix once in
multichain-rswithDEV_ALLOW_HTTP=1gate, both canceler + operator get the fix. Central crate already, so one change covers both.EVM-10:
/healthreturns unconditionalOK, no staleness alert — Mediumoperator/src/api.rs:123routes/health→health_handlerwhich returns literal"OK"./metricsendpoint exists and gathers Prometheus metrics, which is better than nothing, but no built-in staleness detection.Recommendation: same as canceler —
LivenessTrackerwithlast_successful_broadcast_ts, return non-OK on extended silence./metricscan stay exposed in parallel for Prometheus scrape.EVM-14: No
eth_chainIdverification — Low-MedOperator reads
EVM_CHAIN_IDfrom env, never verifies against RPC's actualeth_chainId.get_chain_id_from_hashexists but that's a contract-level helper, not RPC verification.Recommendation: startup
verify_all_bsc_rpc_urlsequivalent. Pattern from ust1-window 673ac74.EVM-18: ✅ PASS (no finding)
EvmConfig,TerraConfig,SolanaConfig,DatabaseConfigall have manualfmt::Debugimpls that redact secrets. Consistent pattern with canceler. Clean.Shared fix opportunities
Several findings land in
multichain-rs(shared crate used by both canceler + operator):validate_rpc_url— one change, two components fixedCould knock out 2-3 findings per component with shared-crate work rather than duplicating.
Cross-references
PlasticDigits/ust1-window#5Operator-side changes merged to
main(8efb987).EVM-01:
EvmWatcherusesevm_consensus_latest_block+EvmRpcReadPolicy(same env vars as canceler).eth_getLogsuses the consensus endpoint first, then one pass over alternates if the consensus node returns a transport error.EVM-02:
FINALITY_BLOCKSdefault is chain-aware (BSC 15, opBNB 12, else 12).EVM-03 / 08 / 14:
EVM_CANONICAL_BRIDGE_ADDRESSES, strictervalidate_rpc_url, startupeth_chainIdverification for primary +EVM_CHAINSRPCs, plus Terra LCD/RPC URL validation on load.EVM-10:
packages/operator/src/liveness.rs— watchers calltouch_activity();/healthreturns 503 when idle exceedsHEALTH_MAX_IDLE_SECS(default 8h) afterHEALTH_STARTUP_GRACE_SECS(default 5m).E2E sets
DEV_ALLOW_HTTP=1for Anvil.@Brouie please verify.
@PlasticDigits verified on
8efb987.Build + test baseline:
Findings:
EvmRpcReadPolicy, consensus reads wired intoEvmWatcher.default_finality_for_native_chain(BSC=15, opBNB=12, default=12), unit test at config.rs:637 asserts each default.EVM_CANONICAL_BRIDGE_ADDRESSESenforcement at load — same pattern as canceler. Validates primary + all multi-chain bridge addresses.validate_rpc_url. Terra and Solana URL validation also added (bonus coverage beyond my original scope).liveness.rsmodule. Atomictouch_activity()called in EVM, Terra, and Solana watchers on successful polls./healthreturns 503 with diagnostic message when stale.verify_evm_rpc_chain_idsat startup (multichain-rs), plus operator-level startup verify.impl fmt::Debugretained on DatabaseConfig, EvmConfig, TerraConfig, SolanaConfig.Closing as verified.
mentioned in issue #138