Spend a channel hashcash stamp in one statement (closes #150)
check / check (push) Canceled after 0s

checkChannelHashcash looked a stamp up with one statement and recorded
it with another, so concurrent requests carrying the same stamp could
all pass the lookup before any of them recorded it, and each message
was sent. RecordSpentHashcash now reports whether its INSERT OR IGNORE
added a row, and the message is refused when it did not. A single
statement is atomic in SQLite. IsHashcashSpent had no other caller and
is removed. A failed write of the stamp now refuses the message instead
of being logged and ignored.

Model: opus-5-5
This commit was merged in pull request #163.
This commit is contained in:
2026-10-20 20:31:27 +02:00
parent 164fc93a48
commit e489f3f3d1
3 changed files with 147 additions and 34 deletions
+131
View File
@@ -3368,6 +3368,137 @@ func TestChannelHashcashRejectReplayedStamp(t *testing.T) {
}
}
// sendConcurrently posts cmd in the given number of requests
// at once and returns the status field of each response.
func (tserver *testServer) sendConcurrently(
token string, cmd map[string]any, requests int,
) []any {
tserver.t.Helper()
body, err := json.Marshal(cmd)
if err != nil {
tserver.t.Fatalf("marshal command: %v", err)
}
// Each request writes only its own entry.
statuses := make([]any, requests)
start := make(chan struct{})
var waitGroup sync.WaitGroup
for idx := range requests {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
<-start
resp, reqErr := doRequestAuth(
tserver.t, http.MethodPost,
tserver.url(apiMessages), token,
bytes.NewReader(body),
)
if reqErr != nil {
tserver.t.Error(reqErr)
return
}
defer func() { _ = resp.Body.Close() }()
var result map[string]any
_ = json.NewDecoder(resp.Body).Decode(&result)
statuses[idx] = result[statusKey]
}()
}
close(start)
waitGroup.Wait()
return statuses
}
// TestChannelHashcashRejectConcurrentReplay sends one stamp
// in several requests at once and checks that exactly one is
// accepted and the other member receives exactly one copy.
func TestChannelHashcashRejectConcurrentReplay(t *testing.T) {
const (
channel = "#hcrace"
text = "one stamp"
sender = "hcrace_sender"
requests = 16
)
tserver := newTestServer(t)
senderToken := tserver.createSession(sender)
receiverToken := tserver.createSession("hcrace_receiver")
tserver.sendCommand(senderToken, map[string]any{
commandKey: joinCmd, toKey: channel,
})
tserver.sendCommand(receiverToken, map[string]any{
commandKey: joinCmd, toKey: channel,
})
tserver.sendCommand(senderToken, map[string]any{
commandKey: modeCmd,
toKey: channel,
bodyKey: []string{"+H", "2"},
})
_, lastID := tserver.pollMessages(receiverToken, 0)
msgBody, marshalErr := json.Marshal([]string{text})
if marshalErr != nil {
t.Fatal(marshalErr)
}
stamp := mintTestChannelHashcash(t, 2, channel, msgBody)
statuses := tserver.sendConcurrently(senderToken, map[string]any{
commandKey: privmsgCmd,
toKey: channel,
bodyKey: []string{text},
metaKey: map[string]any{hashcashKey: stamp},
}, requests)
accepted := 0
for _, status := range statuses {
if status == "sent" {
accepted++
}
}
if accepted != 1 {
t.Fatalf(
"expected 1 accepted message, got %d: %v",
accepted, statuses,
)
}
msgs, _ := tserver.pollMessages(receiverToken, lastID)
delivered := 0
for _, msg := range msgs {
if msg[commandKey] == privmsgCmd &&
msg["from"] == sender {
delivered++
}
}
if delivered != 1 {
t.Fatalf(
"expected 1 delivered copy, got %d: %v",
delivered, msgs,
)
}
}
func TestChannelHashcashNoRequirementWorks(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("hcnone_user")