Spend a hashcash stamp only when the request succeeds (closes #158)
check / check (push) Canceled after 0s

A session request refused with 400 or 409, or a +H channel message
refused for membership, a ban or +m, used to spend its stamp, so a
retry with the same stamp was refused as already used.

hashcash.Validator.Validate is split into Check, which spends
nothing, and Claim, which records the stamp; Release undoes a claim.
Session creation checks the stamp first, claims it just before
CreateSession and releases it if CreateSession fails.
SendChannelMessage checks the stamp after the membership, ban and +m
checks.

Model: opus-5-5
This commit is contained in:
2026-10-21 01:39:03 +00:00
parent e7e408c93e
commit 1436d4e409
8 changed files with 393 additions and 51 deletions
+17 -16
View File
@@ -387,13 +387,14 @@ func excludeSession(
return out
}
// SendChannelMessage validates the hashcash stamp of a
// PRIVMSG to a +H channel, membership and moderation, then
// fans out a message to all channel members except the
// sender. Returns the database row ID, message UUID, and
// any error. The dbID lets callers enqueue the same
// message to the sender when echo is needed (HTTP
// transport).
// SendChannelMessage checks membership, bans and
// moderation, then the hashcash stamp of a PRIVMSG to a +H
// channel, then fans out a message to all channel members
// except the sender. The stamp is checked after the other
// checks so that a message they refuse does not spend it.
// Returns the database row ID, message UUID, and any error.
// The dbID lets callers enqueue the same message to the
// sender when echo is needed (HTTP transport).
func (s *Service) SendChannelMessage(
ctx context.Context,
sessionID int64,
@@ -409,15 +410,6 @@ func (s *Service) SendChannelMessage(
return 0, "", noSuchChannel
}
if command == irc.CmdPrivmsg {
err = s.checkChannelHashcash(
ctx, chID, channel, body, meta,
)
if err != nil {
return 0, "", err
}
}
isMember, _ := s.db.IsChannelMember(
ctx, chID, sessionID,
)
@@ -459,6 +451,15 @@ func (s *Service) SendChannelMessage(
}
}
if command == irc.CmdPrivmsg {
err = s.checkChannelHashcash(
ctx, chID, channel, body, meta,
)
if err != nil {
return 0, "", err
}
}
memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID)
recipients := excludeSession(memberIDs, sessionID)