Skip to content

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

Open
icellan wants to merge 5 commits intomainfrom
feature/asset-rate-limiting-peer-auth
Open

feat: add asset service rate limiting, peer authentication and request visibility#643
icellan wants to merge 5 commits intomainfrom
feature/asset-rate-limiting-peer-auth

Conversation

@icellan
Copy link
Copy Markdown
Contributor

@icellan icellan commented Mar 30, 2026

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

…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
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

🤖 Claude Code Review

Status: Complete

This is a well-implemented security and observability feature for the Asset service. The PR adds three-tier rate limiting, Ed25519 peer authentication, and comprehensive access logging with Prometheus metrics.

Summary: No critical issues found. The implementation is solid with good test coverage and proper error handling.

Minor observations:

  • All previous review feedback has been addressed
  • Rate limiter cleanup goroutines properly stop on context cancellation
  • Peer authentication uses fail-open design (unverified on error) which is appropriate
  • Test coverage is good with 13 new tests covering key paths
  • Middleware stack ordering is correct (auth before rate limit before logging)

Architecture notes:

  • The peer tier cache refreshes every 30 seconds from P2P registry
  • Mining peers (BlocksReceived > 0 AND ReputationScore >= threshold) are fully exempt
  • Non-mining peers get 5x the base rate by default
  • Heavy endpoints (subtree_data, blocks, batch txs) have stricter limits
  • Request signing is automatically applied by P2P service via global signer

The implementation follows project conventions and integrates cleanly with existing code.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-643 (8b8dc19)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 151
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.653µ 1.397µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.49n 61.54n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.50n 61.79n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.73n 61.66n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.75n 32.11n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 52.50n 52.26n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 113.0n 109.6n ~ 0.100
MiningCandidate_Stringify_Short-4 267.1n 264.8n ~ 0.500
MiningCandidate_Stringify_Long-4 1.862µ 1.857µ ~ 0.700
MiningSolution_Stringify-4 949.1n 931.9n ~ 0.100
BlockInfo_MarshalJSON-4 1.789µ 1.765µ ~ 0.100
NewFromBytes-4 126.3n 126.9n ~ 0.400
Mine_EasyDifficulty-4 62.88µ 62.89µ ~ 0.700
Mine_WithAddress-4 4.951µ 4.893µ ~ 0.700
BlockAssembler_AddTx-4 0.02889n 0.03048n ~ 1.000
AddNode-4 10.98 11.28 ~ 1.000
AddNodeWithMap-4 11.12 11.22 ~ 1.000
DirectSubtreeAdd/4_per_subtree-4 62.57n 62.58n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 30.11n 29.81n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 28.74n 28.92n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 27.78n 27.76n ~ 0.800
DirectSubtreeAdd/2048_per_subtree-4 27.54n 27.49n ~ 0.700
SubtreeProcessorAdd/4_per_subtree-4 293.0n 293.0n ~ 0.800
SubtreeProcessorAdd/64_per_subtree-4 293.0n 293.8n ~ 1.000
SubtreeProcessorAdd/256_per_subtree-4 292.5n 294.5n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 293.6n 295.2n ~ 0.400
SubtreeProcessorAdd/2048_per_subtree-4 292.5n 293.4n ~ 0.600
SubtreeProcessorRotate/4_per_subtree-4 300.6n 304.2n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 298.0n 303.4n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 298.6n 300.0n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 300.3n 298.4n ~ 0.400
SubtreeNodeAddOnly/4_per_subtree-4 64.44n 64.52n ~ 1.000
SubtreeNodeAddOnly/64_per_subtree-4 39.30n 38.10n ~ 0.100
SubtreeNodeAddOnly/256_per_subtree-4 37.72n 37.58n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 36.69n 36.07n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 141.1n 143.4n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 595.6n 599.2n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 2.074µ 2.075µ ~ 1.000
SubtreeCreationOnly/1024_per_subtree-4 7.527µ 7.538µ ~ 1.000
SubtreeCreationOnly/2048_per_subtree-4 14.02µ 14.30µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 298.6n 296.3n ~ 0.200
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 297.6n 296.2n ~ 0.400
ParallelGetAndSetIfNotExists/1k_nodes-4 926.2µ 911.2µ ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 1.834m 1.802m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.900m 8.004m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 15.51m 15.69m ~ 0.200
SequentialGetAndSetIfNotExists/1k_nodes-4 743.4µ 752.6µ ~ 0.200
SequentialGetAndSetIfNotExists/10k_nodes-4 2.899m 2.942m ~ 0.200
SequentialGetAndSetIfNotExists/50k_nodes-4 10.69m 10.86m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 20.39m 20.77m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 958.2µ 949.8µ ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.556m 4.585m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 18.25m 18.46m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 799.6µ 790.0µ ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 5.863m 5.908m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 39.74m 39.98m ~ 0.200
DiskTxMap_SetIfNotExists-4 3.883µ 3.895µ ~ 0.700
DiskTxMap_SetIfNotExists_Parallel-4 3.763µ 3.488µ ~ 0.100
DiskTxMap_ExistenceOnly-4 500.5n 339.6n ~ 0.400
Queue-4 201.6n 193.3n ~ 0.100
AtomicPointer-4 4.554n 4.246n ~ 0.200
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 883.6µ 893.7µ ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/10K-4 902.7µ 891.1µ ~ 0.200
ReorgOptimizations/AllMarkFalse/Old/10K-4 116.3µ 123.5µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 62.66µ 62.67µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 68.62µ 67.34µ ~ 1.000
ReorgOptimizations/HashSlicePool/New/10K-4 11.92µ 12.01µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/10K-4 5.690µ 5.345µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.936µ 1.829µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 12.17m 10.97m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 11.56m 10.72m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.178m 1.164m ~ 0.400
ReorgOptimizations/AllMarkFalse/New/100K-4 687.9µ 688.0µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 738.0µ 706.9µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/100K-4 344.9µ 327.0µ ~ 0.400
ReorgOptimizations/NodeFlags/Old/100K-4 60.68µ 57.45µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 20.92µ 20.37µ ~ 0.400
TxMapSetIfNotExists-4 51.77n 51.92n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 38.04n 38.06n ~ 1.000
ChannelSendReceive-4 609.5n 623.0n ~ 0.200
CalcBlockWork-4 501.2n 506.7n ~ 0.700
CalculateWork-4 685.1n 687.0n ~ 0.400
BuildBlockLocatorString_Helpers/Size_10-4 1.341µ 1.347µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 15.94µ 15.79µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_1000-4 125.6µ 126.3µ ~ 0.100
CatchupWithHeaderCache-4 104.1m 103.9m ~ 0.700
_BufferPoolAllocation/16KB-4 3.313µ 3.231µ ~ 1.000
_BufferPoolAllocation/32KB-4 7.187µ 7.899µ ~ 0.400
_BufferPoolAllocation/64KB-4 14.37µ 14.85µ ~ 1.000
_BufferPoolAllocation/128KB-4 28.29µ 28.42µ ~ 1.000
_BufferPoolAllocation/512KB-4 105.6µ 103.4µ ~ 0.700
_BufferPoolConcurrent/32KB-4 19.00µ 17.33µ ~ 0.100
_BufferPoolConcurrent/64KB-4 30.51µ 26.69µ ~ 0.100
_BufferPoolConcurrent/512KB-4 140.5µ 138.7µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/16KB-4 612.4µ 639.8µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 604.6µ 644.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 603.6µ 662.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 604.8µ 650.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 618.9µ 663.6µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 34.95m 34.95m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/32KB-4 35.52m 34.79m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 35.58m 34.89m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/128KB-4 35.22m 34.79m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.26m 34.72m ~ 0.200
_PooledVsNonPooled/Pooled-4 735.9n 731.1n ~ 0.100
_PooledVsNonPooled/NonPooled-4 7.009µ 7.134µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 6.744µ 6.541µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 11.008µ 9.350µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.631µ 8.810µ ~ 0.100
_prepareTxsPerLevel-4 408.3m 406.9m ~ 0.700
_prepareTxsPerLevelOrdered-4 3.785m 3.508m ~ 0.700
_prepareTxsPerLevel_Comparison/Original-4 411.1m 416.7m ~ 0.200
_prepareTxsPerLevel_Comparison/Optimized-4 3.525m 3.583m ~ 0.700
SubtreeProcessor/100_tx_64_per_subtree-4 79.14m 80.27m ~ 0.700
SubtreeProcessor/500_tx_64_per_subtree-4 383.7m 382.7m ~ 0.400
SubtreeProcessor/500_tx_256_per_subtree-4 398.1m 397.5m ~ 0.700
SubtreeProcessor/1k_tx_64_per_subtree-4 767.9m 757.7m ~ 0.100
SubtreeProcessor/1k_tx_256_per_subtree-4 777.7m 770.6m ~ 0.100
StreamingProcessorPhases/FilterValidated/100_tx-4 2.682m 2.645m ~ 0.100
StreamingProcessorPhases/ClassifyProcess/100_tx-4 234.8µ 233.2µ ~ 0.700
StreamingProcessorPhases/FilterValidated/500_tx-4 13.13m 13.08m ~ 0.700
StreamingProcessorPhases/ClassifyProcess/500_tx-4 586.3µ 585.3µ ~ 0.700
StreamingProcessorPhases/FilterValidated/1k_tx-4 26.32m 26.06m ~ 0.100
StreamingProcessorPhases/ClassifyProcess/1k_tx-4 1.040m 1.036m ~ 0.200
SubtreeSizes/10k_tx_4_per_subtree-4 1.311m 1.274m ~ 0.100
SubtreeSizes/10k_tx_16_per_subtree-4 321.2µ 315.9µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 75.15µ 73.25µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 18.80µ 18.18µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.335µ 9.153µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 4.672µ 4.538µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.313µ 2.246µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 73.54µ 73.33µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 18.68µ 18.48µ ~ 0.200
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.601µ 4.581µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 406.4µ 385.3µ ~ 0.200
BlockSizeScaling/50k_tx_256_per_subtree-4 94.08µ 91.56µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.02µ 22.60µ ~ 0.100
SubtreeAllocations/small_subtrees_exists_check-4 158.2µ 153.7µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 164.3µ 160.2µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 326.1µ 320.4µ ~ 0.100
SubtreeAllocations/medium_subtrees_exists_check-4 9.370µ 9.133µ ~ 0.100
SubtreeAllocations/medium_subtrees_data_fetch-4 9.546µ 9.396µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 18.79µ 18.36µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.247µ 2.184µ ~ 0.100
SubtreeAllocations/large_subtrees_data_fetch-4 2.338µ 2.313µ ~ 0.400
SubtreeAllocations/large_subtrees_full_validation-4 4.702µ 4.587µ ~ 0.100
GetUtxoHashes-4 257.7n 266.6n ~ 0.100
GetUtxoHashes_ManyOutputs-4 42.32µ 42.52µ ~ 0.100
_NewMetaDataFromBytes-4 236.5n 237.3n ~ 0.400
_Bytes-4 622.0n 624.3n ~ 1.000
_MetaBytes-4 568.4n 568.0n ~ 1.000

Threshold: >10% with p < 0.05 | Generated: 2026-03-31 17:00 UTC

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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.

- 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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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.

icellan added 2 commits March 30, 2026 18:50
Reformat struct field alignment to satisfy golangci-lint's embedded gci
formatter. Pre-existing formatting issue surfaced because the file was
modified in this branch.
- 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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 on lines +236 to +239
// Sign the request if a signer is configured (silently skip on error)
if httpRequestSigner != nil {
_ = httpRequestSigner.SignRequest(req)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

executeHTTPRequest now signs all outgoing HTTP requests when a global signer is configured. Since util.DoHTTPRequest/DoHTTPRequestBodyReader are also used for non-peer HTTP calls (e.g., local health checks and test harness endpoints), this will attach X-Peer-* headers (and a signature) to requests that aren’t peer-to-peer asset-service calls, potentially leaking node identity and creating unexpected coupling. Consider making signing opt-in per request (or only for known peer/DataHub targets), or providing a way for callers to explicitly disable signing for non-peer URLs.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The signer is only set when util.SetHTTPRequestSigner() is called during P2P service startup. Test processes and non-peer callers (test_daemon.go, test_containers.go, helper.go) don't start the P2P service, so the signer is nil and no headers are added. The only production callers are blockvalidation, subtreevalidation, and legacy peer_server — all legitimate peer-to-peer paths. Adding per-request opt-in would require changing 13 call sites for no practical benefit.


tier := cache.GetTier(peerID)
c.Set("peer_tier", tier)
logger.Debugf("[PeerAuth] authenticated peer %s as %s", peerID, tier)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The format string uses tier (type peerTier) with %s. Since peerTier is not a string type, this will log as %!s(httpimpl.peerTier=...) rather than the intended peer/miner/unverified. Use tier.String() or %v here (and similarly anywhere else peerTier is formatted as %s).

Suggested change
logger.Debugf("[PeerAuth] authenticated peer %s as %s", peerID, tier)
logger.Debugf("[PeerAuth] authenticated peer %s as %v", peerID, tier)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

peerTier implements fmt.Stringer (line 27 of this file), so %s works correctly — fmt.Sprintf calls String() on any type implementing the Stringer interface. Verified: fmt.Printf("%s", tierMiner) outputs "miner".

prometheusAssetHTTPResponseSize.WithLabelValues(method, path, statusStr).Observe(float64(size))

return err
logger.Infof("[Asset_http] %s %s client_ip=%s status=%d duration=%v size=%d tier=%s uri=%s",
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The access log uses tier (type peerTier) with %s, which will produce %!s(httpimpl.peerTier=...) in logs. Use tier.String() or change the format verb to %v so the intended tier name is logged.

Suggested change
logger.Infof("[Asset_http] %s %s client_ip=%s status=%d duration=%v size=%d tier=%s uri=%s",
logger.Infof("[Asset_http] %s %s client_ip=%s status=%d duration=%v size=%d tier=%v uri=%s",

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — peerTier has a String() method so %s works correctly via the fmt.Stringer interface. No change needed.


if !limiter.Allow() {
prometheusAssetHTTPRateLimited.WithLabelValues(rl.tierLabel).Inc()
return c.JSON(http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"})
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Rate-limited responses return JSON { "error": "rate limit exceeded" }, but the service’s customHTTPErrorHandler (and handler docs) use a { "message": ... } schema. For API consistency, consider returning the same top-level field (message) (or routing through echo.HTTPError/customHTTPErrorHandler) so clients don’t need special-case parsing for 429s.

Suggested change
return c.JSON(http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"})
return c.JSON(http.StatusTooManyRequests, map[string]string{"message": "rate limit exceeded"})

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Changed to {"message": "rate limit exceeded"} to match the customHTTPErrorHandler schema.

The customHTTPErrorHandler returns {"message": ...} for all error responses.
Match that schema for rate limit 429 responses.
@sonarqubecloud
Copy link
Copy Markdown

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.

3 participants