Skip to content

1581 add upper cap limits to chosen websocket endpoints to increase resilliency#1582

Merged
cranycrane merged 9 commits into
masterfrom
1581-add-upper-cap-limits-to-chosen-websocket-endpoints-to-increase-resilliency
Jul 10, 2026
Merged

1581 add upper cap limits to chosen websocket endpoints to increase resilliency#1582
cranycrane merged 9 commits into
masterfrom
1581-add-upper-cap-limits-to-chosen-websocket-endpoints-to-increase-resilliency

Conversation

@cranycrane

@cranycrane cranycrane commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds upper-cap / bound-checking limits to several server endpoints to improve resilience against unbounded memory growth and abusive request patterns (closes #1581). Each cap is covered by unit tests.

Changes

WebSocket — cap in-flight getMempoolFilters responses
Per-connection semaphore (maxWebsocketMempoolFiltersResponses = 4) bounds each request from acceptance until its response is written (or drained on close), capping the peak memory held in large filter responses; excess requests are rejected with a mempool_filters_limit error. Adds a release/finalize hook on WsRes so the slot is freed exactly once across write, drain-on-close, and overflow paths.

Fiat rates — bound and normalize currency selectors
Replaces removeEmpty with normalizeFiatCurrencies (trim, lowercase, dedupe, bound) applied once at the entry points (GetCurrentFiatRates, GetFiatRatesForTimestamps, GetBalanceHistory, GetXpubBalanceHistory) before any per-result maps are allocated; downstream helpers rely on the pre-normalized slice. New limits MaxFiatRatesCurrencies = 128 (applied to the raw selector list length) and MaxFiatRatesCurrencyCodeLength = 16 return a client-facing error when exceeded.

EVM — harden ERC-1155 TransferBatch log parsing
Rewrites offset/length handling with explicit bounds and overflow checks to avoid possible OOM / out-of-range slicing from malformed log data, validating total data length before allocating the result slice.

Behavior changes

  • A currency selector longer than 16 characters is now rejected with a public APIError (HTTP 400 on REST). The REST tickers endpoint never splits ?currency= on commas — it passes the whole value as a single selector — so a comma-joined value is only rejected once it exceeds 16 chars. ?currency=usd,eur,gbp,jpy (15 chars) therefore still returns 200 with {"usd,eur,gbp,jpy": -1} exactly as before; only a longer value such as ?currency=usd,eur,gbp,cad,jpy (19 chars) now returns 400 "currency code too long, max 16".
  • The currency-count cap (128) applies to the raw selector list length before deduplication, so the validation work itself is bounded by the cap; a longer list is rejected even if it would deduplicate to fewer entries. This cap only bites on the array-accepting WS methods (getCurrentFiatRates, getFiatRatesForTimestamps) and the balance-history paths; the single-param REST tickers endpoint always passes 0 or 1 selector, so it can never trip the count cap.
  • More than 4 getMempoolFilters requests in flight on one websocket connection (accepted but not yet written back) are rejected with a mempool_filters_limit error.

Tests

  • server/websocket_test.go — mempool filters slot capping, including release after a successful write (via outputLoop over a live connection) and drain-on-close
  • api/fiat_rates_api_test.go, api/fiat_balance_history_test.go — currency normalization/limits
  • bchain/coins/eth/contract_test.go — malformed ERC-1155 batch logs

@cranycrane cranycrane requested a review from pragmaxim June 22, 2026 15:22
@cranycrane cranycrane self-assigned this Jun 22, 2026

@pragmaxim pragmaxim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review of the cap-limit changes. The core logic is sound — the ERC-1155 TransferBatch bounds/overflow hardening in particular holds up well (candidate concerns about countIds misuse and erc1155BatchOffsetHex rejecting valid <0x40 offsets were checked and refuted). Findings below are behavior-change + cleanup, no crashes.

Inline comments cover the actionable items. One extra very-minor nit not inline (the function isn't in the diff): getFiatRatesResult (api/fiat_rates_api.go:81 and :104) still calls strings.ToLower on each selector, but the slice it receives is already lowercased by normalizeFiatCurrencies — redundant, bounded work. (Note line 72 is not redundant; it iterates raw ticker-map keys.)

Comment thread server/websocket.go
Comment thread api/fiat_rates_api.go
Comment thread api/fiat_rates_api.go
Comment thread api/fiat_balance_history.go Outdated
Comment thread bchain/coins/eth/contract.go Outdated
cranycrane and others added 7 commits July 6, 2026 12:43
The MaxFiatRatesCurrencies check fired only when a unique normalized code
was appended, so an all-duplicate list bypassed the cap while still costing
a full trim/lowercase/dedup pass per element. Reject over-long lists up
front so the normalization work itself is bounded by the cap; this also
makes the in-loop unique-count check unreachable, so drop it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setFiatRateToBalanceHistories re-normalized a slice that both of its
callers (GetBalanceHistory, GetXpubBalanceHistory) had already passed
through normalizeFiatCurrencies, and getFiatRatesResult re-lowercased
selectors already lowercased by its callers. Drop the redundant passes and
document the pre-normalized precondition instead. Normalization was the
only error path of setFiatRateToBalanceHistories, so it no longer returns
an error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The endValues <= len(data) check already bounds countValues to
len(data)/evmWordHex, far below maxInt, so the truncation guard on the
int conversion could never fire (the load-bearing overflow check lives in
erc1155BatchArrayEnd).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The slot is deliberately held from before the handler computes the
response until it is written to the websocket (or drained on close): the
cap exists to bound peak memory in computed-but-unwritten filter
responses, so it covers compute, queueing, and write together rather than
compute concurrency alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cranycrane cranycrane force-pushed the 1581-add-upper-cap-limits-to-chosen-websocket-endpoints-to-increase-resilliency branch from 32df92c to 66bee88 Compare July 8, 2026 08:05

@pragmaxim pragmaxim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two nit-level comments below. The rest of the review found no correctness or security issues — the websocket slot lifecycle and the ERC-1155 bounds math both hold up, all tests pass with -tags unittest, and go vet is clean.

Comment thread api/fiat_rates_api.go
Comment thread server/websocket.go
…rite

Adds TestWebsocketOutputLoopReleasesMempoolFilterSlotAfterWrite, which drives
a release-bearing WsRes through outputLoop over a live websocket connection and
asserts the mempool-filters slot is freed after WriteJSON. This pins the
primary release path (outputLoop's c.finalize(m)) that the existing semaphore
and drain-on-close tests do not exercise; without it, removing that call would
leak a slot on every successful response and go undetected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cranycrane cranycrane merged commit 302d08a into master Jul 10, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add upper-cap limits to chosen Websocket endpoints to increase resilliency

2 participants