Spend a channel hashcash stamp in one statement (closes #150)
check / check (push) Waiting to run
check / check (push) Waiting to run
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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user