Skip to content

feat: add asset service rate limiting, peer authentication and request visibility#643

Merged
icellan merged 14 commits into
mainfrom
feature/asset-rate-limiting-peer-auth
May 27, 2026
Merged

feat: add asset service rate limiting, peer authentication and request visibility#643
icellan merged 14 commits into
mainfrom
feature/asset-rate-limiting-peer-auth

Conversation

@icellan

@icellan icellan commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses #4521 — adds visibility and access control for the asset service (DataHubURL) HTTP endpoints.

  • Three-tier per-IP rate limiting on all asset service endpoints, with a stricter tier for data-heavy endpoints (subtree_data, blocks, subtrees, batch txs) that can trigger expensive UTXO store lookups
  • Ed25519 peer request signing — outgoing HTTP requests are automatically signed with the node's P2P key; the asset service verifies signatures, derives peer ID from the public key, and checks the cached peer registry to determine the caller's tier
  • Always-on structured access logging with Prometheus metrics (request duration, response size, rate limited count, in-flight gauge) replacing the previous debug-only logger
  • Proper real IP extraction behind reverse proxies via configurable trusted proxy CIDRs

Rate Limiting Tiers

Tier Determination Rate Limit
Mining peer BlocksReceived > 0 AND ReputationScore >= threshold Fully exempt
Non-mining peer Valid Ed25519 signature + in peer registry base_rate × multiplier (default 5x)
Unverified client No signature or unknown key Base rate (default 1024 req/s global, 10 req/s heavy)

Request Signing Protocol

All outgoing peer HTTP requests (block validation, subtree validation, catchup) are automatically signed:

  • Headers: X-Peer-PubKey (hex Ed25519 public key), X-Peer-Timestamp (unix seconds), X-Peer-Signature (hex signature of timestamp:METHOD:/path)
  • Verification: Signature check → peer ID derivation → peer registry lookup → tier assignment
  • Replay protection: 60-second timestamp window
  • Backwards compatible: Missing/invalid signatures are silently treated as unverified (fail open)

New Settings

Setting Default Purpose
asset_httpRateLimit 1024 Global per-IP req/s (0 = disabled)
asset_httpHeavyRateLimit 10 Per-IP req/s on data-heavy endpoints (0 = disabled)
asset_httpPeerRateMultiplier 5 Rate multiplier for non-mining peers
asset_httpBodyLimit 100MB Max request body size
asset_trustedProxyCIDRs (empty) Pipe-separated CIDRs for X-Forwarded-For trust
asset_peerMinerReputationThreshold 50.0 Min reputation score for mining peer classification

Heavy-Rate-Limited Endpoints

These endpoints get the stricter asset_httpHeavyRateLimit because they serve large payloads or trigger expensive backend operations:

  • GET /subtree_data/:hash — can trigger on-demand UTXO store BatchDecorate() lookups
  • POST /subtree/:hash/txs — batch transaction fetch (32MB buffer, 1024 goroutines)
  • GET /blocks/:hash — up to 1000 blocks per request
  • GET /block/:hash — full block data
  • GET /subtree/:hash — large subtree data
  • GET /block_legacy/:hash, GET /rest/block/:hash.bin — legacy block formats

Peer Tier Cache

The asset service caches peer IDs and their tiers locally (refreshed every 30 seconds via GetPeerRegistry() gRPC call). This means:

  • Zero gRPC calls per request — verification is entirely local (Ed25519 signature check + map lookup)
  • New peers take up to 30 seconds to be recognized
  • If P2P service is unreachable, stale cache continues serving; if never populated, all requests are rate-limited (fail open)

Middleware Stack Order

1. Recover              — panic recovery
2. BanList              — reject banned IPs
3. CORS                 — cross-origin
4. Gzip                 — compression
5. SecurityHeaders      — X-Frame-Options, HSTS, etc.
6. PeerAuth             — verify Ed25519 signatures, set peer_tier
7. AccessLog            — always-on logging + Prometheus metrics
8. TieredRateLimit      — per-IP rate limit (tier-aware)
9. BodyLimit            — request body size cap
10. [per-route] HeavyRL — stricter limit on data-heavy endpoints

Files Changed

  • New: util/http_signer.go, services/asset/httpimpl/peer_auth_middleware.go, services/asset/httpimpl/rate_limiter.go + tests
  • Modified: settings/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 (RemoteAddrRealIP())

Test plan

  • go build ./... — compiles cleanly
  • make lint — 0 issues
  • go vet — clean
  • All existing tests pass (util, httpimpl, settings, p2p)
  • 13 new tests pass (3 signer + 6 peer auth + 4 rate limiter)
  • Manual: verify unsigned requests hit 429 at configured rates
  • Manual: verify signed mining peer requests bypass rate limits
  • Manual: verify signed non-mining peer requests get 5x rate
  • Manual: verify access log shows tier=unverified/peer/miner and real client IP
  • Manual: verify Prometheus metrics at /metrics endpoint
  • Manual: verify X-Forwarded-For extraction with trusted proxy CIDRs

@github-actions

github-actions Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Summary:
This PR implements comprehensive rate limiting and peer authentication for the asset service HTTP endpoints. The implementation is well-structured with proper defense-in-depth mechanisms.

Findings:
No critical issues found. All previous review comments have been addressed and resolved.

Strengths:

  • Security-first design: Empty allowlist defaults prevent accidental trust escalation
  • Defense in depth: HTTPMinerRateLimit caps even trusted miners, preventing compromise impact
  • Proper resource management: Bounded LRU for unverified IPs, context-based goroutine cleanup
  • Comprehensive testing: 13 new tests covering auth verification, rate limiting tiers, and replay protection
  • Accurate documentation: Both inline docs and external documentation correctly describe the implementation

Technical Highlights:

  • Ed25519 signature verification with replay cache (15s TTL, 100k capacity)
  • Three-tier rate limiting: unverified/peer/miner with independent buckets
  • IPv6 /64 normalization prevents bucket explosion attacks
  • Body digest verification prevents signature replay across different payloads
  • Proper middleware ordering: body limit before auth (DoS prevention)

Documentation Accuracy:
The documentation in assetServer.md and asset_settings.md accurately describes the implementation. Key behaviors verified:

  • ✅ Freshness window is ±10s (code line 39, docs match)
  • ✅ Replay cache TTL is 15s (code line 44, docs match)
  • ✅ Default allowlist is empty = no elevation (code line 36, docs explicitly state this)
  • ✅ Miner exemption requires HTTPMinerRateLimit=0 (code line 124, docs match)

No documentation corrections needed.

Comment thread services/asset/httpimpl/rate_limiter.go
@github-actions

github-actions Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-643 (04f6c3e)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.677µ 1.632µ ~ 0.700
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 71.02n 70.99n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 71.05n 71.46n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 70.99n 71.07n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 32.64n 32.89n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 54.72n 54.35n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 161.2n 139.4n ~ 0.200
MiningCandidate_Stringify_Short-4 226.4n 220.1n ~ 0.100
MiningCandidate_Stringify_Long-4 1.659µ 1.636µ ~ 0.200
MiningSolution_Stringify-4 851.4n 849.0n ~ 0.700
BlockInfo_MarshalJSON-4 1.783µ 1.769µ ~ 0.200
NewFromBytes-4 136.4n 122.6n ~ 0.100
AddTxBatchColumnar_Validation-4 2.390µ 2.515µ ~ 0.100
OffsetValidationLoop-4 719.3n 544.9n ~ 0.100
Mine_EasyDifficulty-4 60.46µ 60.46µ ~ 0.700
Mine_WithAddress-4 6.866µ 6.802µ ~ 0.700
BlockAssembler_AddTx-4 0.02975n 0.02891n ~ 1.000
AddNode-4 11.73 11.67 ~ 0.700
AddNodeWithMap-4 12.51 11.91 ~ 0.200
DirectSubtreeAdd/4_per_subtree-4 57.35n 58.62n ~ 0.400
DirectSubtreeAdd/64_per_subtree-4 30.05n 31.71n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 28.94n 30.53n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 27.90n 29.33n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 27.63n 28.99n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 296.0n 285.4n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 286.9n 280.9n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 287.0n 283.3n ~ 0.700
SubtreeProcessorAdd/1024_per_subtree-4 284.2n 271.8n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 277.2n 271.8n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 285.1n 276.6n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 295.1n 273.9n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 291.5n 273.0n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 279.2n 273.5n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 54.40n 54.11n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 34.50n 34.18n ~ 0.100
SubtreeNodeAddOnly/256_per_subtree-4 33.38n 33.17n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 32.53n 32.67n ~ 0.400
SubtreeCreationOnly/4_per_subtree-4 114.0n 115.6n ~ 0.200
SubtreeCreationOnly/64_per_subtree-4 401.4n 398.4n ~ 0.400
SubtreeCreationOnly/256_per_subtree-4 1.356µ 1.344µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 4.454µ 4.454µ ~ 0.800
SubtreeCreationOnly/2048_per_subtree-4 8.168µ 8.178µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 277.0n 278.3n ~ 1.000
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 278.4n 281.8n ~ 0.400
ParallelGetAndSetIfNotExists/1k_nodes-4 2.191m 2.211m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 5.243m 5.372m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.115m 7.230m ~ 0.700
ParallelGetAndSetIfNotExists/100k_nodes-4 9.742m 10.005m ~ 0.400
SequentialGetAndSetIfNotExists/1k_nodes-4 1.930m 1.960m ~ 0.200
SequentialGetAndSetIfNotExists/10k_nodes-4 4.418m 4.409m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 12.73m 12.50m ~ 0.200
SequentialGetAndSetIfNotExists/100k_nodes-4 23.18m 23.21m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.249m 2.272m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.191m 8.326m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.33m 13.79m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 2.403m 1.974m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 7.955m 7.751m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 40.42m 40.64m ~ 1.000
DiskTxMap_SetIfNotExists-4 3.665µ 3.368µ ~ 0.400
DiskTxMap_SetIfNotExists_Parallel-4 3.044µ 3.294µ ~ 0.400
DiskTxMap_ExistenceOnly-4 328.0n 306.8n ~ 0.200
Queue-4 149.0n 150.3n ~ 0.200
AtomicPointer-4 2.821n 2.503n ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 643.0µ 630.0µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/10K-4 579.2µ 591.6µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 81.30µ 80.89µ ~ 0.200
ReorgOptimizations/AllMarkFalse/New/10K-4 50.08µ 49.90µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 45.82µ 42.35µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 8.615µ 8.518µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 3.875µ 3.388µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.759µ 1.194µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 7.109m 8.427m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 8.161m 8.545m ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/100K-4 844.8µ 839.3µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/100K-4 547.3µ 548.4µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/100K-4 471.7µ 485.8µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/100K-4 202.6µ 205.0µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/100K-4 31.54µ 34.57µ ~ 0.200
ReorgOptimizations/NodeFlags/New/100K-4 10.57µ 11.93µ ~ 0.100
TxMapSetIfNotExists-4 38.24n 38.30n ~ 0.700
TxMapSetIfNotExistsDuplicate-4 32.03n 31.78n ~ 0.100
ChannelSendReceive-4 432.0n 452.5n ~ 0.100
CalcBlockWork-4 467.8n 474.5n ~ 0.100
CalculateWork-4 659.8n 639.1n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.037µ 1.191µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 9.919µ 9.993µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 123.32µ 98.50µ ~ 0.700
CatchupWithHeaderCache-4 103.9m 103.7m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.368m 1.336m ~ 1.000
SubtreeSizes/10k_tx_16_per_subtree-4 315.6µ 315.6µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 75.12µ 77.04µ ~ 0.200
SubtreeSizes/10k_tx_256_per_subtree-4 19.19µ 19.15µ ~ 0.700
SubtreeSizes/10k_tx_512_per_subtree-4 9.431µ 9.529µ ~ 0.400
SubtreeSizes/10k_tx_1024_per_subtree-4 4.641µ 4.650µ ~ 1.000
SubtreeSizes/10k_tx_2k_per_subtree-4 2.318µ 2.326µ ~ 0.500
BlockSizeScaling/10k_tx_64_per_subtree-4 73.09µ 74.50µ ~ 0.100
BlockSizeScaling/10k_tx_256_per_subtree-4 18.59µ 18.74µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.630µ 4.653µ ~ 0.200
BlockSizeScaling/50k_tx_64_per_subtree-4 384.7µ 388.0µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 92.56µ 93.88µ ~ 0.200
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.75µ 22.98µ ~ 0.100
SubtreeAllocations/small_subtrees_exists_check-4 155.0µ 153.1µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 158.5µ 163.9µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 317.0µ 321.4µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 8.999µ 9.067µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.408µ 9.655µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 18.66µ 18.86µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.160µ 2.171µ ~ 0.500
SubtreeAllocations/large_subtrees_data_fetch-4 2.276µ 2.333µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 4.665µ 4.659µ ~ 0.700
_BufferPoolAllocation/16KB-4 4.047µ 3.915µ ~ 0.100
_BufferPoolAllocation/32KB-4 8.409µ 8.288µ ~ 0.700
_BufferPoolAllocation/64KB-4 22.04µ 18.51µ ~ 0.400
_BufferPoolAllocation/128KB-4 38.48µ 28.87µ ~ 0.100
_BufferPoolAllocation/512KB-4 121.6µ 127.0µ ~ 0.700
_BufferPoolConcurrent/32KB-4 20.24µ 19.07µ ~ 0.100
_BufferPoolConcurrent/64KB-4 31.08µ 30.55µ ~ 0.400
_BufferPoolConcurrent/512KB-4 157.3µ 152.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 644.5µ 681.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 633.9µ 691.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 633.8µ 693.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 631.6µ 686.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 606.0µ 634.2µ ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.09m 37.43m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.10m 37.02m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.25m 36.90m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/128KB-4 37.29m 36.82m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 37.08m 37.05m ~ 0.700
_PooledVsNonPooled/Pooled-4 833.0n 837.9n ~ 0.400
_PooledVsNonPooled/NonPooled-4 7.914µ 7.938µ ~ 1.000
_MemoryFootprint/Current_512KB_32concurrent-4 7.540µ 7.927µ ~ 0.400
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.773µ 10.433µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.557µ 10.346µ ~ 0.400
_prepareTxsPerLevel-4 385.0m 384.8m ~ 1.000
_prepareTxsPerLevelOrdered-4 4.209m 3.776m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 394.3m 382.0m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 4.229m 3.921m ~ 0.200
StoreBlock_Sequential/BelowCSVHeight-4 323.4µ 315.9µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 312.3µ 317.8µ ~ 0.400
GetUtxoHashes-4 260.7n 264.8n ~ 1.000
GetUtxoHashes_ManyOutputs-4 42.92µ 42.75µ ~ 0.700
_NewMetaDataFromBytes-4 228.9n 228.6n ~ 0.500
_Bytes-4 400.5n 405.1n ~ 0.100
_MetaBytes-4 138.9n 137.5n ~ 0.400

Threshold: >10% with p < 0.05 | Generated: 2026-05-26 19:24 UTC

Copilot AI 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.

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.

Comment thread services/asset/httpimpl/http.go
Comment thread services/asset/httpimpl/rate_limiter.go Outdated
Comment thread services/asset/httpimpl/rate_limiter.go Outdated
Comment thread services/asset/httpimpl/rate_limiter.go Outdated
Comment thread services/asset/httpimpl/metrics.go Outdated

Copilot AI 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.

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.

Comment thread services/asset/httpimpl/http.go
Comment thread services/asset/httpimpl/metrics.go Outdated
Comment thread services/asset/httpimpl/GetTxMetaByTXID.go Outdated
Comment thread services/asset/httpimpl/http.go

Copilot AI 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.

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.

Comment thread util/http.go
Comment thread services/asset/httpimpl/peer_auth_middleware.go Outdated
Comment thread services/asset/httpimpl/http.go Outdated
Comment thread services/asset/httpimpl/rate_limiter.go Outdated
@sonarqubecloud

Copy link
Copy Markdown

oskarszoon

This comment was marked as outdated.

@icellan icellan force-pushed the feature/asset-rate-limiting-peer-auth branch from 5a76efc to 1c59929 Compare May 18, 2026 12:01
@icellan icellan requested a review from oskarszoon May 18, 2026 15:36

@oskarszoon oskarszoon 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.

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:152 signs only timestamp:METHOD:URL.Path. A captured signature replays across any parameterised endpoint (e.g. ?from=&to= on heavy block routes, body content on POST /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: include RequestURI (path + raw query), Host, and a Digest: SHA-256=... of the body (RFC 9530 style).

  • C2. No nonce / no replay cache. peer_auth_middleware.go:131-139 enforces 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 from p2pClient.GetPeerRegistry. Any peer that ever connected to libp2p is tierPeer (5× rate). Any peer that has ever forwarded a block becomes tierMiner — full rate-limit exempt. The libp2p registry isn't rate-limited on Put either, so a peer that floods us with valid libp2p handshakes mints itself as authenticated. Fix: add asset_peerAuthAllowlist config; require explicit membership before granting tierPeer / 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. unverifiedLimiters map 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: log path (route pattern, already computed) and drop uri, or add a query-string redactor.

Medium

  • M2 (partial). 030a82b25 added a Warnf for invalid TrustedProxyCIDRs — improvement, but the fail-open downgrade to Echo defaults (loopback + RFC 1918) on all-invalid CIDR config still stands. Fix: if TrustedProxyCIDRs is non-empty but no entries parsed, fail Init() instead of silently downgrading.
  • M3. tierMiner is fully exempt from rate limiting (rate_limiter.go:73). Combined with C1-C3 this is the highest-impact bypass. Fix: separate HTTPMinerRateLimit (e.g. defaultRate * 100) instead of hard return next(c).

Go correctness

  • util/http_signer.go:19 is a plain package-level var written 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 -race only because no test exercises concurrent write+read; real race exists in production. Fix: atomic.Value or mutex.
  • rate_limiter.go:83-95 pre-constructs &limiterEntry{rate.NewLimiter(...)} before LoadOrStore for 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, PeerAuthFreshnessWindow or whatever the actual names are) need rows in docs/references/settings/services/asset_settings.md using the same table format as propagation_settings.md.

Other

  • Stray c.Request().RemoteAddr at services/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).
  • peerTier constants use zero-indexed iota. Add a // never renumber after merge comment 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.

icellan added a commit that referenced this pull request May 19, 2026
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.)
@icellan icellan force-pushed the feature/asset-rate-limiting-peer-auth branch from 451925d to 3af95e2 Compare May 19, 2026 15:45
icellan added a commit that referenced this pull request May 19, 2026
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.)
@icellan icellan force-pushed the feature/asset-rate-limiting-peer-auth branch from 3af95e2 to 598d122 Compare May 19, 2026 15:52
@icellan icellan requested a review from oskarszoon May 26, 2026 14:38

@oskarszoon oskarszoon 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.

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 to tierUnverified. 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-miner peer_id-missing fallback rate flipped defaultRateminerRate at rate_limiter.go. Unreachable in normal flow AND inert with current minerRate==0, but worth amending the commit message or reverting the fallback.
  • authBucket fallback at rate_limiter.go:134 calls unverifiedBucket(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 TrustedProxyCIDRs bad-CIDR → New() error path; the fail-loudly intent is only manual-tested.
  • No test for StartCleanup / cleanupSyncMap goroutine-leak intent — exactly the kind of thing that gets silently regressed.
  • rate_limiter.go:96 has a dead defaultRate <= 0 branch — delete or comment.

Confirmations from the re-review

  • C1: signer and verifier build "v2:" + ts + ":" + host + ":" + method + ":" + request_uri + ":" + body_sha256 bit-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). Cache Set only after verify succeeds — invalid-sig flood doesn't pollute.
  • C3 mechanism (gated by P1-1): empty allowlist truly denies — TestPeerAuthMiddleware_AllowlistEmpty_NoElevation confirms. 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.Value signer: nil-guard on Store, typed coercion on Load, single Load-then-use. TestSetHTTPRequestSigner_ConcurrentRace clean under -race.
  • M2 fail-closed: returns ConfigurationError from New() 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.

icellan added 4 commits May 26, 2026 21:08
…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.
icellan added 10 commits May 26, 2026 21:08
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).

@oskarszoon oskarszoon 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.

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.

@sonarqubecloud

Copy link
Copy Markdown

@icellan icellan merged commit 13b438b into main May 27, 2026
32 of 33 checks passed
@icellan icellan deleted the feature/asset-rate-limiting-peer-auth branch May 27, 2026 15:12
icellan added a commit to icellan/teranode that referenced this pull request May 27, 2026
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).
icellan added a commit to icellan/teranode that referenced this pull request May 27, 2026
… 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.)
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.

4 participants