feat: per-channel hashcash proof-of-work for PRIVMSG anti-spam
All checks were successful
check / check (push) Successful in 1m11s
All checks were successful
check / check (push) Successful in 1m11s
Add per-channel hashcash requirement via MODE +H <bits>. When set, PRIVMSG to the channel must include a valid hashcash stamp in the meta.hashcash field bound to the channel name and message body hash. Server validates stamp format, difficulty, date freshness, channel binding, body hash binding, and proof-of-work. Spent stamps are persisted to SQLite with 1-year TTL for replay prevention. Stamp format: 1:bits:YYMMDD:channel:bodyhash:counter Changes: - Schema: add hashcash_bits column to channels, spent_hashcash table - DB: queries for get/set channel hashcash bits, spent token CRUD - Hashcash: ChannelValidator, BodyHash, StampHash, MintChannelStamp - Handlers: validate hashcash on PRIVMSG, MODE +H/-H support - Pass meta through fanOut chain to store in messages - Prune spent hashcash tokens in cleanup loop (1-year TTL) - Client: MintChannelHashcash helper for CLI - Tests: 12 new channel_test.go + 10 new api_test.go integration tests - README: document +H mode, stamp format, and usage
This commit is contained in:
@@ -36,15 +36,21 @@ type Params struct {
|
||||
|
||||
const defaultIdleTimeout = 30 * 24 * time.Hour
|
||||
|
||||
// spentHashcashTTL is how long spent hashcash tokens are
|
||||
// retained for replay prevention. Per issue requirements,
|
||||
// this is 1 year.
|
||||
const spentHashcashTTL = 365 * 24 * time.Hour
|
||||
|
||||
// Handlers manages HTTP request handling.
|
||||
type Handlers struct {
|
||||
params *Params
|
||||
log *slog.Logger
|
||||
hc *healthcheck.Healthcheck
|
||||
broker *broker.Broker
|
||||
hashcashVal *hashcash.Validator
|
||||
stats *stats.Tracker
|
||||
cancelCleanup context.CancelFunc
|
||||
params *Params
|
||||
log *slog.Logger
|
||||
hc *healthcheck.Healthcheck
|
||||
broker *broker.Broker
|
||||
hashcashVal *hashcash.Validator
|
||||
channelHashcash *hashcash.ChannelValidator
|
||||
stats *stats.Tracker
|
||||
cancelCleanup context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a new Handlers instance.
|
||||
@@ -58,12 +64,13 @@ func New(
|
||||
}
|
||||
|
||||
hdlr := &Handlers{ //nolint:exhaustruct // cancelCleanup set in startCleanup
|
||||
params: ¶ms,
|
||||
log: params.Logger.Get(),
|
||||
hc: params.Healthcheck,
|
||||
broker: broker.New(),
|
||||
hashcashVal: hashcash.NewValidator(resource),
|
||||
stats: params.Stats,
|
||||
params: ¶ms,
|
||||
log: params.Logger.Get(),
|
||||
hc: params.Healthcheck,
|
||||
broker: broker.New(),
|
||||
hashcashVal: hashcash.NewValidator(resource),
|
||||
channelHashcash: hashcash.NewChannelValidator(),
|
||||
stats: params.Stats,
|
||||
}
|
||||
|
||||
lifecycle.Append(fx.Hook{
|
||||
@@ -285,4 +292,20 @@ func (hdlr *Handlers) pruneQueuesAndMessages(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Prune spent hashcash tokens older than 1 year.
|
||||
hashcashCutoff := time.Now().Add(-spentHashcashTTL)
|
||||
|
||||
pruned, err := hdlr.params.Database.
|
||||
PruneSpentHashcash(ctx, hashcashCutoff)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"spent hashcash pruning failed", "error", err,
|
||||
)
|
||||
} else if pruned > 0 {
|
||||
hdlr.log.Info(
|
||||
"pruned spent hashcash tokens",
|
||||
"deleted", pruned,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user