feat: add asset service rate limiting, peer authentication and request visibility#643
Conversation
|
🤖 Claude Code Review Status: Complete Summary: Findings: Strengths:
Technical Highlights:
Documentation Accuracy:
No documentation corrections needed. |
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-05-26 19:24 UTC |
There was a problem hiding this comment.
Pull request overview
Adds authentication, observability, and abuse protection to the Asset HTTP service endpoints (DataHubURL), aligning rate limits and logging with peer identity and real client IP handling behind proxies.
Changes:
- Introduces Ed25519 request signing for outgoing peer HTTP calls and verification middleware on the Asset service to classify callers into tiers.
- Adds tier-aware global and heavy-endpoint per-IP rate limiting plus new Prometheus metrics for HTTP request visibility.
- Improves client IP determination by configuring trusted-proxy CIDR handling and updates handlers to use
RealIP()for logging/tracing.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| util/http_signer.go | Adds Ed25519 request signer + package-level signer hook for outgoing HTTP requests |
| util/http_signer_test.go | Tests for request signing and signer wiring |
| util/http.go | Signs outgoing requests when a signer is configured |
| settings/settings.go | Wires new Asset HTTP security/rate-limit settings into runtime settings |
| settings/asset_settings.go | Documents new Asset HTTP settings (rate limits, body limit, trusted proxies, miner threshold) |
| services/p2p/Server.go | Sets the global HTTP request signer using the node’s P2P Ed25519 key |
| services/asset/httpimpl/peer_auth_middleware.go | Adds middleware to verify signed requests and assign peer_tier using a cached peer registry |
| services/asset/httpimpl/peer_auth_middleware_test.go | Tests peer auth behavior (valid/invalid signatures, expiry, unknown peers) |
| services/asset/httpimpl/rate_limiter.go | Adds tiered per-IP rate limiting middleware with cleanup of stale limiter entries |
| services/asset/httpimpl/rate_limiter_test.go | Tests tiered rate limiting behavior (unverified, peer multiplier, miner exemption, disabled) |
| services/asset/httpimpl/metrics.go | Adds Prometheus metrics for HTTP duration, response size, rate-limited count, and in-flight gauge |
| services/asset/httpimpl/http.go | Wires middleware stack (trusted proxy IP extraction, peer auth, access logs, tiered rate limits, body limit, heavy RL) and applies heavy RL to selected routes |
| services/asset/httpimpl/http_test.go | Updates middleware test to reflect new access log middleware name |
| services/asset/httpimpl/* handlers | Switches tracing/log messages from RemoteAddr to RealIP() |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
5a76efc to
1c59929
Compare
oskarszoon
left a comment
There was a problem hiding this comment.
Substantive design weaknesses in the Ed25519 layer plus a docs gap that would mislead operators after merge. The annotation refactor and rate-limit plumbing are sound; the specific issues below need addressing.
Critical (security)
-
C1. Signed payload omits query string, body, and host.
peer_auth_middleware.go:152signs onlytimestamp:METHOD:URL.Path. A captured signature replays across any parameterised endpoint (e.g.?from=&to=on heavy block routes, body content onPOST /subtree/:hash/txs) and across any teranode that knows the pubkey (no host binding). One MITM / log leak / tcpdump inherits the peer's full 120-second window of arbitrary GETs and same-path POSTs fleet-wide. Fix: includeRequestURI(path + raw query),Host, and aDigest: SHA-256=...of the body (RFC 9530 style). -
C2. No nonce / no replay cache.
peer_auth_middleware.go:131-139enforces only the ±60s clock-skew bound. A captured signed header set is replayable for the full ~120s span, unbounded count. Fix: bounded LRU of recently-seen(pubkey, signature)pairs with TTL of the freshness window. -
C3. No peer allowlist / pinning / revocation.
peerTierCache.refresh(peer_auth_middleware.go:83-102) blanket-classifies every entry fromp2pClient.GetPeerRegistry. Any peer that ever connected to libp2p istierPeer(5× rate). Any peer that has ever forwarded a block becomestierMiner— full rate-limit exempt. The libp2p registry isn't rate-limited onPuteither, so a peer that floods us with valid libp2p handshakes mints itself as authenticated. Fix: addasset_peerAuthAllowlistconfig; require explicit membership before grantingtierPeer/tierMiner.
High (security)
- H1. Authenticated rate-limit buckets keyed by IP (
rate_limiter.go:77-95), not by peer-ID. Two peers behind one egress IP share one elevated bucket; auth doesn't elevate the peer, it elevates the IP. Fix: switch to peer-ID keying for authenticated buckets; unverified continues IP-keyed. - H2.
unverifiedLimitersmap is unbounded. IPv6 attacker rotates source addresses (/64 has 2^64) → ~1 GB allocation in 5 minutes before cleanup runs. Fix: bound the map (LRU), normalise IPv6 to /64, run cleanup more aggressively above a high-water mark. - H3. BodyLimit middleware ordered after auth+rate-limit (
http.go:181-207). Mitigated today because auth doesn't read the body, but if body verification is added (C1 fix), BodyLimit must move ahead of auth. - H4. Wall-clock timestamp ±60s. Vulnerable to NTP attacks; cut the window (5-10s typical for well-NTP'd infra) and document the NTP requirement.
- H5.
c.Request().RequestURI(path + raw query) logged at INFO (http.go:685). No documented sensitive params today but no redaction either. Fix: logpath(route pattern, already computed) and dropuri, or add a query-string redactor.
Medium
- M2 (partial).
030a82b25added aWarnffor invalidTrustedProxyCIDRs— improvement, but the fail-open downgrade to Echo defaults (loopback + RFC 1918) on all-invalid CIDR config still stands. Fix: ifTrustedProxyCIDRsis non-empty but no entries parsed, failInit()instead of silently downgrading. - M3.
tierMineris fully exempt from rate limiting (rate_limiter.go:73). Combined with C1-C3 this is the highest-impact bypass. Fix: separateHTTPMinerRateLimit(e.g.defaultRate * 100) instead of hardreturn next(c).
Go correctness
util/http_signer.go:19is a plain package-levelvarwritten once on startup and read on every outbound HTTP call. Interface writes are not atomic — type-word and pointer can be observed torn under the race detector. Test suite passes-raceonly because no test exercises concurrent write+read; real race exists in production. Fix:atomic.Valueor mutex.rate_limiter.go:83-95pre-constructs&limiterEntry{rate.NewLimiter(...)}beforeLoadOrStorefor every distinct-IP request, including race-losers. Wasteful under port-scan / catchup bursts. Move the allocation behind a sync.Pool or use a load-then-store pattern.
Docs (merge-blocker)
docs/topics/services/assetServer.md:837-852(section 7.2.5.1) explicitly says "Per-IP rate limiting inside the asset service itself is a planned defense-in-depth follow-up but is not currently implemented." This PR implements exactly that — operators reading the docs post-merge will be misinformed. Update the section.- The 6 new settings (
HTTPRateLimit,HTTPHeavyRateLimit,HTTPPeerRateLimit,HTTPRateLimitTTL,EnablePeerAuthentication,PeerAuthFreshnessWindowor whatever the actual names are) need rows indocs/references/settings/services/asset_settings.mdusing the same table format aspropagation_settings.md.
Other
- Stray
c.Request().RemoteAddratservices/asset/httpimpl/mining_candidate_legacy_block.go:35(file added by a separate merged PR, but cheap to fix here for codebase-wide consistency with the conversion sweep this PR is doing). peerTierconstants use zero-indexediota. Add a// never renumber after mergecomment so a future addition in the middle doesn't shift existing values.- No metric for "auth attempted and failed". Operators have no visibility into whether legitimate peers are getting bounced (clock drift, key rotation) vs the auth path being unused. Add
prometheusAssetHTTPPeerAuthResult{result=ok|expired|bad_sig|unknown_key}.
Recommended fix sequence (each is a focused change): C1 (extend signed payload), C2 (in-memory replay cache), H1 (peer-ID keying for authenticated buckets), H2 (bound + IPv6 normalise + faster cleanup), C3 (allowlist), M3 (cap miner rate), then the docs + Go nits.
freemans13's March 30 approval is at a stale commit — re-approval at HEAD will be needed once these land.
Full per-finding writeups in the team review drafts.
Address security review of #643: - Signed payload now covers Host, Method, RequestURI (path + raw query), and lowercase-hex SHA-256 of the body. Previously the payload was just ts:METHOD:URL.Path, so a captured signature replayed against any parameterised endpoint or POST body, and across any teranode that knew the pubkey. Verifiers must recompute the body digest from the actual bytes and reject mismatches, so the digest header alone is not enough. - Payload is explicitly versioned (v2:) and the old v1 payload format is no longer accepted. The PR is pre-merge so there are no v1 clients to preserve compatibility for. - Freshness window tightened from ±60s to ±10s. Replay-cache work in a follow-up commit makes the window meaningful; for now the smaller window narrows the replayable interval. - Access log drops the raw RequestURI to keep query-string values out of logs. The route pattern is still logged for diagnostics. - BodyLimit middleware moved ahead of peer auth so the digest read can't be turned into a DoS surface by an oversized body. - util.SetHTTPRequestSigner switched to atomic.Value so concurrent Set/Load no longer tears the interface value. (Real race; existing tests didn't trip it because nothing was concurrent.)
451925d to
3af95e2
Compare
Address security review of #643: - Signed payload now covers Host, Method, RequestURI (path + raw query), and lowercase-hex SHA-256 of the body. Previously the payload was just ts:METHOD:URL.Path, so a captured signature replayed against any parameterised endpoint or POST body, and across any teranode that knew the pubkey. Verifiers must recompute the body digest from the actual bytes and reject mismatches, so the digest header alone is not enough. - Payload is explicitly versioned (v2:) and the old v1 payload format is no longer accepted. The PR is pre-merge so there are no v1 clients to preserve compatibility for. - Freshness window tightened from ±60s to ±10s. Replay-cache work in a follow-up commit makes the window meaningful; for now the smaller window narrows the replayable interval. - Access log drops the raw RequestURI to keep query-string values out of logs. The route pattern is still logged for diagnostics. - BodyLimit middleware moved ahead of peer auth so the digest read can't be turned into a DoS surface by an oversized body. - util.SetHTTPRequestSigner switched to atomic.Value so concurrent Set/Load no longer tears the interface value. (Real race; existing tests didn't trip it because nothing was concurrent.)
3af95e2 to
598d122
Compare
oskarszoon
left a comment
There was a problem hiding this comment.
Re-review at 1350186c7. Every Critical/High/Medium from the prior CHANGES_REQUESTED is materially closed in code (v2 signed payload, replay cache, peer-ID keying, IPv6 /64 LRU, ±10s window, atomic.Value signer, fail-closed CIDR, c.RealIP() everywhere). 988 tests pass under -race. Docs match code. Sonar refactor is behaviour-preserving.
Two new P1s found during the re-review.
P1-1 — Settings loader doesn't read asset_httpMinerRateLimit or asset_peerAuthAllowlist
settings/asset_settings.go:32,36 declares the fields with key: struct-tag defaults, but settings/settings.go:198-220 doesn't read either key. The loader is hand-rolled getInt/getString, not reflection over the key: tag — grep -r asset_httpMinerRateLimit --include='*.go' matches only the struct tag and longdesc text. Both fields stay at Go zero value forever.
Effects:
HTTPMinerRateLimit == 0→ miner tier always fully exempt. M3 belt unfastenable.PeerAuthAllowlist == ""→parsePeerAuthAllowlist("")returns empty map → every authenticated peer drops totierUnverified. C3 mechanism cannot be turned on by any operator.
Defaults happen to match the safe-by-default posture (deny-all elevation, miner-exempt), so this isn't a regression of merge-time security stance. But the configurable mitigations the PR claims to ship don't work — same failure mode as #933. Two-line fix in the loader block:
HTTPMinerRateLimit: getInt("asset_httpMinerRateLimit", 0, alternativeContext...),
PeerAuthAllowlist: getString("asset_peerAuthAllowlist", "", alternativeContext...),Plus a regression test that asserts the runtime default via settings.NewSettings() (current tests bypass the broken seam).
P1-2 — customHTTPErrorHandler still logs full RequestURI
H5 fixed accessLogMiddleware at http.go:725 but customHTTPErrorHandler at http.go:672 still logs c.Request().RequestURI. The 97fb42f commit message says "drop RequestURI to keep query-string values out of logs"; error paths are exactly where query-string leakage matters most. One-line fix: c.Path().
Worth fixing in the same PR (not blockers)
- Sonar refactor
1350186c7: claims "no behaviour change" but the tier-minerpeer_id-missing fallback rate flippeddefaultRate→minerRateatrate_limiter.go. Unreachable in normal flow AND inert with currentminerRate==0, but worth amending the commit message or reverting the fallback. authBucketfallback atrate_limiter.go:134callsunverifiedBucket(c.RealIP(), ...)without IPv6 /64 normalisation, inconsistent with the H2 fix on the default tier path at:124. Unreachable today but worth tightening.- No test for
TrustedProxyCIDRsbad-CIDR →New()error path; the fail-loudly intent is only manual-tested. - No test for
StartCleanup/cleanupSyncMapgoroutine-leak intent — exactly the kind of thing that gets silently regressed. rate_limiter.go:96has a deaddefaultRate <= 0branch — delete or comment.
Confirmations from the re-review
- C1: signer and verifier build
"v2:" + ts + ":" + host + ":" + method + ":" + request_uri + ":" + body_sha256bit-identically. No canonicalisation on either side (sharper, not looser). Body digest verified against actual buffered body. v1 rejection is structural (verifier only accepts"v2:"). - C2: key
sha256(pubkey | sig)— bypass-via-body-variation impossible because body digest is in the signed payload. TTL=15s > freshness=10s (5s clock-skew headroom). CacheSetonly after verify succeeds — invalid-sig flood doesn't pollute. - C3 mechanism (gated by P1-1): empty allowlist truly denies —
TestPeerAuthMiddleware_AllowlistEmpty_NoElevationconfirms. Hash-based map lookup, no timing leak. - H4: symmetric
math.Abs(now-ts) <= 10. Wall-clock comparison correct (monotonic incomparable to signer ts). NTP requirement documented in code. atomic.Valuesigner: nil-guard on Store, typed coercion on Load, single Load-then-use.TestSetHTTPRequestSigner_ConcurrentRaceclean under-race.- M2 fail-closed: returns
ConfigurationErrorfromNew()for all-invalid and mixed invalid CIDR config. - Sonar
verifySignedRequest/limiterFor: early-return ordering identical to inlined version, replay-before-sig-verify preserved, no closure captures. Cognitive complexity 40→<15 and 18→<15.
…t visibility (#4521) Add three-tier per-IP rate limiting, Ed25519 peer request signing, always-on access logging with Prometheus metrics, and proper real IP extraction for reverse proxy deployments on the asset service HTTP endpoints. Rate limiting tiers: - Mining peers (high reputation + blocks received): fully exempt - Non-mining peers (valid signature, in peer registry): multiplied rate (default 5x) - Unverified clients: base rate (configurable, default 1024 req/s global, 10 req/s heavy) Data-heavy endpoints (subtree_data, blocks, block, subtree, batch txs, legacy block) get a stricter per-route rate limit to protect against UTXO store abuse from on-demand subtreeData creation. Peer authentication: - Outgoing HTTP requests are automatically signed with the node's Ed25519 P2P key - Asset service verifies signatures, derives peer ID, checks cached peer registry - Peer tier cache refreshes every 30s, fails open on P2P service unavailability New settings: - asset_httpRateLimit (default 1024): global per-IP req/s - asset_httpHeavyRateLimit (default 10): per-IP req/s on heavy endpoints - asset_httpPeerRateMultiplier (default 5): rate multiplier for non-mining peers - asset_httpBodyLimit (default 100MB): max request body size - asset_trustedProxyCIDRs: pipe-separated CIDRs for X-Forwarded-For trust - asset_peerMinerReputationThreshold (default 50.0): min reputation for miner tier Additional changes: - Replace debug-only logger with always-on access log middleware - Add Prometheus metrics: request duration, response size, rate limited count, in-flight - Fix all handlers to use c.RealIP() instead of c.Request().RemoteAddr - Configure Echo IPExtractor for correct IP extraction behind reverse proxies
- Fix goroutine leak: rate limiter cleanup goroutines now accept a context and stop on cancellation, matching the peerTierCache pattern. Created via Start() in the HTTP server's Start method. - Fix double error handling: remove c.Error(err) call in accessLogMiddleware to prevent Echo invoking HTTPErrorHandler twice. - Fix panic on negative settings: clamp defaultRate <= 0 as disabled and peerMultiplier to minimum 1. - Fix misleading metric label: rename "tier" to "scope" on the rate limited counter since values are "global"/"heavy" (limiter scope, not peer tier). - Fix bucket comment: ExponentialBuckets(256, 4, 10) maxes at ~64MB not ~256MB.
- Fix accessLogMiddleware: call c.Error(err) and return nil so status/size are finalized by the error handler before metrics observe them - Fix stale comment: "counts rate-limited requests by tier" → "by scope" - Fix pre-existing copy-paste log message in GetTxMetaByTxID (was logging "GetUTXOsByTXID") - Add warning when asset_trustedProxyCIDRs is configured but all entries fail to parse
The customHTTPErrorHandler returns {"message": ...} for all error responses.
Match that schema for rate limit 429 responses.
Address security review of #643: - Signed payload now covers Host, Method, RequestURI (path + raw query), and lowercase-hex SHA-256 of the body. Previously the payload was just ts:METHOD:URL.Path, so a captured signature replayed against any parameterised endpoint or POST body, and across any teranode that knew the pubkey. Verifiers must recompute the body digest from the actual bytes and reject mismatches, so the digest header alone is not enough. - Payload is explicitly versioned (v2:) and the old v1 payload format is no longer accepted. The PR is pre-merge so there are no v1 clients to preserve compatibility for. - Freshness window tightened from ±60s to ±10s. Replay-cache work in a follow-up commit makes the window meaningful; for now the smaller window narrows the replayable interval. - Access log drops the raw RequestURI to keep query-string values out of logs. The route pattern is still logged for diagnostics. - BodyLimit middleware moved ahead of peer auth so the digest read can't be turned into a DoS surface by an oversized body. - util.SetHTTPRequestSigner switched to atomic.Value so concurrent Set/Load no longer tears the interface value. (Real race; existing tests didn't trip it because nothing was concurrent.)
The freshness window alone gave attackers ~2 × window seconds of unbounded replay per captured signature. Now every verified (pubkey, signature) pair is recorded in a bounded TTL cache (100k entries, 15s TTL) and a re-submit within the window is rejected. The cache is wrapped together with the existing peer-tier cache into a peerAuthVerifier owned by HTTP. Both lifecycle goroutines are started in HTTP.Start(ctx) and stop when ctx is cancelled.
Previously every peer in the libp2p registry was auto-trusted for rate-limit purposes — peers that had received any block were classified as miners (fully exempt), and everyone else as tierPeer (5x rate). The registry is populated by network connections, so this auto-trust effectively meant "anyone who can connect to our libp2p node gets elevated rate limits." Add a new setting asset_peerAuthAllowlist (pipe-separated libp2p peer IDs). Empty allowlist (the default) means tier elevation is denied for every authenticated peer — signatures are still verified (replay cache, body digest, freshness window) but the registry-derived tier is not applied. Operators must explicitly list peers they want to trust.
H1 - Authenticated buckets keyed by peer ID, not IP. Two authenticated peers behind one NAT no longer starve each other's bucket; a captured signature can't be used from a different IP to get a fresh bucket. H2 - Unverified buckets held in a bounded LRU (50k entries) with IPv6 addresses normalised to /64. An attacker rotating source addresses across a /64 (2^64) can no longer grow the map without limit; cleanup happens via LRU eviction rather than a 5-minute timer. M3 - New setting asset_httpMinerRateLimit caps the miner tier. Default 0 preserves the original fully-exempt behaviour; operators can set e.g. base*100 as defense in depth against an allowlisted miner key being misused. Also: load-then-store on the contended path so race-losers don't allocate a stranded rate.Limiter under burst load (was hot path under catchup / port-scan bursts).
- M2: asset_trustedProxyCIDRs fails closed. When the setting is
non-empty but no valid CIDRs are parsed, return a ConfigurationError
from New() instead of silently downgrading to "trust loopback +
RFC1918". Mixed valid/invalid input also fails — that's almost
always a typo and partial application is the worst outcome.
- mining_candidate_legacy_block.go:35: replace c.Request().RemoteAddr
with c.RealIP() to match the conversion sweep the rest of this PR
is doing.
- New prometheusAssetHTTPPeerAuthResult{result=...} counter wired
into every fail-open branch of the peer-auth verifier. Operators
now see exactly *why* signed peers are getting bounced (clock drift
vs bad sig vs replay vs not allowlisted) instead of inferring from
the absence of tier=peer in access logs.
assetServer.md §7.2.5.1 previously said per-IP rate limiting "is a planned defense-in-depth follow-up but is not currently implemented" — that text contradicts the rest of this PR. Rewrite the section to describe what's actually implemented: the three tiers, IP/peer-ID keying, IPv6 /64 normalisation, the bounded LRU, the allowlist opt-in policy, and the v2 signed-payload protocol. Retain the reverse-proxy recommendation as defense in depth. asset_settings.md gets a new "HTTP Rate Limit and Peer Authentication Settings" table covering all 8 new settings (HTTPRateLimit, HTTPHeavyRateLimit, HTTPPeerRateMultiplier, HTTPMinerRateLimit, HTTPBodyLimit, TrustedProxyCIDRs, PeerAuthAllowlist, PeerMinerReputationThreshold) with the tier semantics summarised underneath the table.
…dleware SonarCloud flagged two CRITICAL findings on the previous commits: - peer_auth_middleware.go Middleware(): complexity 40 vs allowed 15 - rate_limiter.go Middleware(): complexity 18 vs allowed 15 No behaviour change. Both Middleware bodies are now thin pipelines that delegate to helpers: - peerAuthVerifier.verifySignedRequest does all crypto/policy work and returns (peerID, result, attempted). Middleware just records the result and elevates on OK. - tieredRateLimiter.limiterFor and .authBucket factor out the per-tier bucket selection. Middleware now reads as "compute bucket, charge bucket, or pass" with no nested switch.
…st (P1-1) Re-review of #643 caught that the loader in settings/settings.go was missing getInt/getString calls for these two fields. The struct-tag declarations are docs-only; the loader is hand-rolled. Both fields stayed at Go zero forever, so the C3 allowlist gate could never be turned on and the M3 miner cap was permanently unfastenable. Defaults happen to match the safe posture (deny-all elevation, miner-exempt), so this isn't a regression of merge-time security stance — but the configurable mitigations the PR claims to ship didn't work. Adds a regression test that asserts each rate-limit / auth setting is reachable from gocore via the documented key (and would have caught this class of bug for the existing settings too).
H5 in 97fb42f only patched accessLogMiddleware; the parallel error path through customHTTPErrorHandler was still logging the raw RequestURI. Error paths are exactly where query-string values (auth tokens in URLs, search terms, identifiers) most need to stay out of logs. Switches the log line to c.Path() (the route pattern) — already sufficient for diagnostics, and what the access-log path uses. Test asserts that a request whose query string contains marker values does not appear in the error logger.
…its) Re-review of 1350186 caught two consistency gaps in the cognitive- complexity refactor: - Miner-tier fallback rate. When the authenticated middleware sets peer_tier=tierMiner but somehow doesn't set peer_id, the old inlined code fell back to defaultRate; the refactor used minerRate via authBucket(c, ..., minerRate). Unreachable in current flow and inert with minerRate=0, but still a quiet behaviour change against a "no behaviour change" commit message. - IPv6 normalisation in the same fallback. The default-tier path keys buckets by unverifiedKey(c.RealIP()) so /128s in the same /64 share a bucket; the authBucket fallback was using raw c.RealIP(), so two /128s in the same /64 got independent buckets via this seam. Restructured limiterFor so all "tier set but peer_id missing" cases land in the unverified path at defaultRate with /64 normalisation. authBucket helper is gone (its job was the inconsistent fallback). Added regression tests for both fallback paths plus the M2 TrustedProxyCIDRs fail-closed behaviour (all-invalid, mixed valid/invalid, all-valid, empty).
1350186 to
1131a35
Compare
oskarszoon
left a comment
There was a problem hiding this comment.
Approve at 1131a3550. All P1+P2 from v3 closed. 600/600 tests -race clean.
P1-1 settings loader (88c07d8fc): settings.go:203, 205 wires getInt("asset_httpMinerRateLimit", ...) + getString("asset_peerAuthAllowlist", ...). New asset_settings_test.go is table-driven, sets via gocore.Config().Set and asserts NewSettings() reads through. C3 mechanism + M3 belt now operator-fastenable.
P1-2 RequestURI leak (622f202c2): http.go:672 switched to c.Path(). Test pings ?q=SECRET_QUERY_STRING&token=DEADBEEF and asserts log contains neither value, no ? at all, and route pattern. Defensive ?-absence check catches partial-strip refactors.
P2 rate-limit fallback (1131a3550) — went further than asked. Rewrote limiterFor to remove authBucket helper; all non-elevated flows (including tierMiner/tierPeer with missing peer_id) now land in unverifiedBucket(unverifiedKey(c.RealIP()), defaultRate) — defaultRate + /64 + LRU all in one path. Two new tests pin both invariants. Also closes the v3 Sonar-refactor drift in code, not just by unreachability.
M2 fail-closed test added: TestNew_TrustedProxyCIDRsFailsClosed covers all-invalid, mixed, all-valid, empty.
Earlier C1/C2/C3/H1-H5/M1/M2/M3 commits rebased onto fresh main — content identical, spot-checked. Nothing new introduced.
|
Adopt PR bsv-blockchain#643's asset HTTP design: - Drop my per-route BodyLimit on POST /utxos in favour of the new top-level global middleware (asset_httpBodyLimit, default 100MB). - Add heavyMW()... to the three POST /utxos* routes so they are gated by the same tiered rate limiter as POST /subtree/:hash/txs. Closes the anonymous/unrated gap flagged in PR bsv-blockchain#950 review. - Drop the duplicate HTTPBodyLimit definition (settings/asset_settings.go) and the duplicate loader (settings/settings.go) — main's wins. - Update docs (asset_reference.md, GetUTXOs.go doc comment) for the new global cap, the rate limiter, and the 429 response code. Conflicts resolved: settings/asset_settings.go (kept main's HTTPBodyLimit definition wholesale).
… fix swagger doc Two PR bsv-blockchain#950 follow-ups from the latest bot runs: 1. swagger_routes.go:384 still said "default 4MB" but the post-merge cap is 100MB (the global asset_httpBodyLimit from PR bsv-blockchain#643). Aligned with asset_reference.md. 2. SonarCloud flagged the GetUTXOs handler with cognitive complexity 31 (limit 15). Extract two helpers — no behaviour change: - readUTXOsBody(c): read + 413 mapping + length-multiple validation. - fetchOneUTXO(ctx, txHash, vout): per-record store lookup with ErrNotFound / nil-resp / wrapped-error semantics. The panic-recover defer stays inline in the goroutine wrapper because it needs the closure variables. Handler now reads top-to-bottom: tracing → read body → fast-path empty → fan-out → wait → metrics → serialize. (The two MAJOR "filename does not follow snake_case" findings from Sonar are false positives — every Get*.go file under services/asset/httpimpl is CamelCase. Will flag in the PR reply.)



Summary
Addresses #4521 — adds visibility and access control for the asset service (DataHubURL) HTTP endpoints.
Rate Limiting Tiers
BlocksReceived > 0ANDReputationScore >= thresholdbase_rate × multiplier(default 5x)Request Signing Protocol
All outgoing peer HTTP requests (block validation, subtree validation, catchup) are automatically signed:
X-Peer-PubKey(hex Ed25519 public key),X-Peer-Timestamp(unix seconds),X-Peer-Signature(hex signature oftimestamp:METHOD:/path)New Settings
asset_httpRateLimitasset_httpHeavyRateLimitasset_httpPeerRateMultiplierasset_httpBodyLimitasset_trustedProxyCIDRsasset_peerMinerReputationThresholdHeavy-Rate-Limited Endpoints
These endpoints get the stricter
asset_httpHeavyRateLimitbecause they serve large payloads or trigger expensive backend operations:GET /subtree_data/:hash— can trigger on-demand UTXO storeBatchDecorate()lookupsPOST /subtree/:hash/txs— batch transaction fetch (32MB buffer, 1024 goroutines)GET /blocks/:hash— up to 1000 blocks per requestGET /block/:hash— full block dataGET /subtree/:hash— large subtree dataGET /block_legacy/:hash,GET /rest/block/:hash.bin— legacy block formatsPeer Tier Cache
The asset service caches peer IDs and their tiers locally (refreshed every 30 seconds via
GetPeerRegistry()gRPC call). This means:Middleware Stack Order
Files Changed
util/http_signer.go,services/asset/httpimpl/peer_auth_middleware.go,services/asset/httpimpl/rate_limiter.go+ testssettings/asset_settings.go,settings/settings.go,services/asset/httpimpl/http.go,services/asset/httpimpl/metrics.go,services/p2p/Server.go,util/http.go, 23 handler files (RemoteAddr→RealIP())Test plan
go build ./...— compiles cleanlymake lint— 0 issuesgo vet— cleanutil,httpimpl,settings,p2p)tier=unverified/peer/minerand real client IP/metricsendpointX-Forwarded-Forextraction with trusted proxy CIDRs