Add Blossom bandwidth limiting and tune rate limiters (v0.49.0)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add token-bucket bandwidth rate limiting for Blossom uploads - ORLY_BLOSSOM_RATE_LIMIT enables limiting (default: false) - ORLY_BLOSSOM_DAILY_LIMIT_MB sets daily limit (default: 10MB) - ORLY_BLOSSOM_BURST_LIMIT_MB sets burst cap (default: 50MB) - Followed users, admins, owners are exempt (unlimited) - Change emergency mode throttling from exponential to linear scaling - Old: 16x multiplier at emergency threshold entry - New: 1x at threshold, +1x per 20% excess pressure - Reduce follows ACL throttle increment from 200ms to 25ms per event - Update dependencies Files modified: - app/blossom.go: Pass rate limit config to blossom server - app/config/config.go: Add Blossom rate limit config options - pkg/blossom/ratelimit.go: New bandwidth limiter implementation - pkg/blossom/server.go: Add rate limiter integration - pkg/blossom/handlers.go: Check rate limits on upload/mirror/media - pkg/ratelimit/limiter.go: Linear emergency throttling - pkg/acl/follows.go: Reduce default throttle increment Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,12 @@ func initializeBlossomServer(
|
||||
blossomCfg := &blossom.Config{
|
||||
BaseURL: "", // Will be set dynamically per request
|
||||
MaxBlobSize: 100 * 1024 * 1024, // 100MB default
|
||||
AllowedMimeTypes: nil, // Allow all MIME types by default
|
||||
AllowedMimeTypes: nil, // Allow all MIME types by default
|
||||
RequireAuth: cfg.AuthRequired || cfg.AuthToWrite,
|
||||
// Rate limiting for non-followed users
|
||||
RateLimitEnabled: cfg.BlossomRateLimitEnabled,
|
||||
DailyLimitMB: cfg.BlossomDailyLimitMB,
|
||||
BurstLimitMB: cfg.BlossomBurstLimitMB,
|
||||
}
|
||||
|
||||
// Create blossom server with relay's ACL registry
|
||||
@@ -31,7 +35,12 @@ func initializeBlossomServer(
|
||||
// We'll need to modify the handler to inject the baseURL per request
|
||||
// For now, we'll use a middleware approach
|
||||
|
||||
log.I.F("blossom server initialized with ACL mode: %s", cfg.ACLMode)
|
||||
if cfg.BlossomRateLimitEnabled {
|
||||
log.I.F("blossom server initialized with ACL mode: %s, rate limit: %dMB/day (burst: %dMB)",
|
||||
cfg.ACLMode, cfg.BlossomDailyLimitMB, cfg.BlossomBurstLimitMB)
|
||||
} else {
|
||||
log.I.F("blossom server initialized with ACL mode: %s", cfg.ACLMode)
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -69,13 +69,18 @@ type C struct {
|
||||
|
||||
// Progressive throttle for follows ACL mode - allows non-followed users to write with increasing delay
|
||||
FollowsThrottleEnabled bool `env:"ORLY_FOLLOWS_THROTTLE" default:"false" usage:"enable progressive delay for non-followed users in follows ACL mode"`
|
||||
FollowsThrottlePerEvent time.Duration `env:"ORLY_FOLLOWS_THROTTLE_INCREMENT" default:"200ms" usage:"delay added per event for non-followed users"`
|
||||
FollowsThrottlePerEvent time.Duration `env:"ORLY_FOLLOWS_THROTTLE_INCREMENT" default:"25ms" usage:"delay added per event for non-followed users"`
|
||||
FollowsThrottleMaxDelay time.Duration `env:"ORLY_FOLLOWS_THROTTLE_MAX" default:"60s" usage:"maximum throttle delay cap"`
|
||||
|
||||
// Blossom blob storage service settings
|
||||
BlossomEnabled bool `env:"ORLY_BLOSSOM_ENABLED" default:"true" usage:"enable Blossom blob storage server (only works with Badger backend)"`
|
||||
BlossomServiceLevels string `env:"ORLY_BLOSSOM_SERVICE_LEVELS" usage:"comma-separated list of service levels in format: name:storage_mb_per_sat_per_month (e.g., basic:1,premium:10)"`
|
||||
|
||||
// Blossom upload rate limiting (for non-followed users)
|
||||
BlossomRateLimitEnabled bool `env:"ORLY_BLOSSOM_RATE_LIMIT" default:"false" usage:"enable upload rate limiting for non-followed users"`
|
||||
BlossomDailyLimitMB int64 `env:"ORLY_BLOSSOM_DAILY_LIMIT_MB" default:"10" usage:"daily upload limit in MB for non-followed users (EMA averaged)"`
|
||||
BlossomBurstLimitMB int64 `env:"ORLY_BLOSSOM_BURST_LIMIT_MB" default:"50" usage:"max burst upload in MB (bucket cap)"`
|
||||
|
||||
// Web UI and dev mode settings
|
||||
WebDisableEmbedded bool `env:"ORLY_WEB_DISABLE" default:"false" usage:"disable serving the embedded web UI; useful for hot-reload during development"`
|
||||
WebDevProxyURL string `env:"ORLY_WEB_DEV_PROXY_URL" usage:"when ORLY_WEB_DISABLE is true, reverse-proxy non-API paths to this dev server URL (e.g. http://localhost:5173)"`
|
||||
|
||||
@@ -124,6 +124,17 @@ func (s *Server) handleCashuKeysets(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleCashuInfo handles GET /cashu/info - returns mint information.
|
||||
func (s *Server) handleCashuInfo(w http.ResponseWriter, r *http.Request) {
|
||||
// CORS headers for browser-based CAT support detection
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept")
|
||||
|
||||
// Handle preflight
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if s.CashuIssuer == nil {
|
||||
http.Error(w, "Cashu tokens not enabled", http.StatusNotImplemented)
|
||||
return
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func (l *Listener) HandleEvent(msg []byte) (err error) {
|
||||
log.D.F("HandleEvent: START handling event: %s", msg)
|
||||
log.I.F("HandleEvent: START handling event: %s", string(msg[:min(200, len(msg))]))
|
||||
|
||||
// 1. Raw JSON validation (before unmarshal) - use validation service
|
||||
if result := l.eventValidator.ValidateRawJSON(msg); !result.Valid {
|
||||
@@ -231,6 +231,11 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
|
||||
|
||||
// Authorization check (policy + ACL) - use authorization service
|
||||
decision := l.eventAuthorizer.Authorize(env.E, l.authedPubkey.Load(), l.remote, env.E.Kind)
|
||||
// Debug: log ephemeral event authorization
|
||||
if env.E.Kind >= 20000 && env.E.Kind < 30000 {
|
||||
log.I.F("ephemeral auth check: kind %d, allowed=%v, reason=%s",
|
||||
env.E.Kind, decision.Allowed, decision.DenyReason)
|
||||
}
|
||||
if !decision.Allowed {
|
||||
log.D.F("HandleEvent: authorization denied: %s (requireAuth=%v)", decision.DenyReason, decision.RequireAuth)
|
||||
if decision.RequireAuth {
|
||||
@@ -256,14 +261,17 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
|
||||
log.I.F("HandleEvent: authorized with access level %s", decision.AccessLevel)
|
||||
|
||||
// Progressive throttle for follows ACL mode (delays non-followed users)
|
||||
if delay := l.getFollowsThrottleDelay(env.E); delay > 0 {
|
||||
log.D.F("HandleEvent: applying progressive throttle delay of %v for %0x from %s",
|
||||
delay, env.E.Pubkey, l.remote)
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
return l.ctx.Err()
|
||||
case <-time.After(delay):
|
||||
// Delay completed, continue processing
|
||||
// Skip throttle if a Cashu Access Token is present (authenticated via CAT)
|
||||
if l.cashuToken == nil {
|
||||
if delay := l.getFollowsThrottleDelay(env.E); delay > 0 {
|
||||
log.D.F("HandleEvent: applying progressive throttle delay of %v for %0x from %s",
|
||||
delay, env.E.Pubkey, l.remote)
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
return l.ctx.Err()
|
||||
case <-time.After(delay):
|
||||
// Delay completed, continue processing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,12 +159,26 @@ func (p *P) Deliver(ev *event.E) {
|
||||
sub Subscription
|
||||
}
|
||||
var deliveries []delivery
|
||||
// Debug: log ephemeral event delivery attempts
|
||||
isEphemeral := ev.Kind >= 20000 && ev.Kind < 30000
|
||||
if isEphemeral {
|
||||
var tagInfo string
|
||||
if ev.Tags != nil {
|
||||
tagInfo = string(ev.Tags.Marshal(nil))
|
||||
}
|
||||
log.I.F("ephemeral event kind %d, id %0x, checking %d connections for matches, tags: %s",
|
||||
ev.Kind, ev.ID[:8], len(p.Map), tagInfo)
|
||||
}
|
||||
for w, subs := range p.Map {
|
||||
for id, subscriber := range subs {
|
||||
if subscriber.Match(ev) {
|
||||
deliveries = append(
|
||||
deliveries, delivery{w: w, id: id, sub: subscriber},
|
||||
)
|
||||
} else if isEphemeral {
|
||||
// Debug: log why ephemeral events don't match
|
||||
log.I.F("ephemeral event kind %d did NOT match subscription %s (filters: %s)",
|
||||
ev.Kind, id, string(subscriber.S.Marshal(nil)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user