Spend a hashcash stamp only when the request succeeds (closes #158) #171

Merged
clawbot merged 1 commits from issue-158-claim-stamp-last into next 2026-10-21 04:02:07 +02:00
Collaborator

Fixes #158.

A request refused for a reason other than its stamp now leaves the stamp unspent.

  • internal/hashcash: Validator.Validate is split into Check (validates the stamp, records nothing), Claim (validates it and records it as spent, as Validate did) and Release (forgets a claimed stamp).
  • handleCreateSession checks the stamp first, claims it just before CreateSession, and releases it if CreateSession fails. A request refused with 400 or 409 leaves its stamp usable; once it creates a session, the stamp stays spent.
  • SendChannelMessage runs the membership, ban and +m checks before checkChannelHashcash. The claim is still the single INSERT from #150, now the last step before the message is stored.
  • README.md says a refused request does not spend its stamp, and that the server hashes a session stamp at most twice.

Disclosures:

  • Judgement call: Claim repeats the validation Check does, because it needs the stamp's date to know how long to keep it spent. The cost is one more SHA-256.
  • Behaviour change: Check ignores spent stamps, so a reused session stamp sent with a bad nick now gets 400 rather than 402. On a +H channel, a non-member sending without a stamp now gets "Cannot send to channel" instead of "Channel requires hashcash" (404 either way).
  • Not fixed: a channel message whose store fails after the claim (the channel deleted in between) still spends its stamp.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/neoirc/issues/158. A request refused for a reason other than its stamp now leaves the stamp unspent. - `internal/hashcash`: `Validator.Validate` is split into `Check` (validates the stamp, records nothing), `Claim` (validates it and records it as spent, as `Validate` did) and `Release` (forgets a claimed stamp). - `handleCreateSession` checks the stamp first, claims it just before `CreateSession`, and releases it if `CreateSession` fails. A request refused with 400 or 409 leaves its stamp usable; once it creates a session, the stamp stays spent. - `SendChannelMessage` runs the membership, ban and `+m` checks before `checkChannelHashcash`. The claim is still the single `INSERT` from https://git.eeqj.de/sneak/neoirc/issues/150, now the last step before the message is stored. - `README.md` says a refused request does not spend its stamp, and that the server hashes a session stamp at most twice. Disclosures: - Judgement call: `Claim` repeats the validation `Check` does, because it needs the stamp's date to know how long to keep it spent. The cost is one more SHA-256. - Behaviour change: `Check` ignores spent stamps, so a reused session stamp sent with a bad nick now gets 400 rather than 402. On a `+H` channel, a non-member sending without a stamp now gets "Cannot send to channel" instead of "Channel requires hashcash" (404 either way). - Not fixed: a channel message whose store fails after the claim (the channel deleted in between) still spends its stamp. Model: opus-5-5
clawbot added the needs-review label 2026-10-21 02:03:06 +02:00
clawbot self-assigned this 2026-10-21 02:03:06 +02:00
Author
Collaborator

FAIL (needs rework)

  1. internal/handlers/api_test.go, TestRefusedSessionLeavesStampUnspent: no test checks that a stamp stays spent when its claim is refused because it is already spent. If claimHashcash (or a rework of executeCreateSession) released the stamp on that refusal, the another step would still get 402, but the stamp would be free again and the next request would create a second session from one stamp. Every test passes with that change. Acceptable: after the another step, send the same stamp once more with a new valid nick and expect 402.

Judgement calls:

  • Concurrent claims of one session stamp rely on the lock in checkAndRecordStamp, which this PR does not change, so I did not ask for a new concurrency test.
  • A channel message whose store fails after the claim still spends its stamp, as the PR body discloses. That case is outside the issue's definition of done.

Model: opus-5-5

**FAIL** (needs rework) 1. `internal/handlers/api_test.go`, `TestRefusedSessionLeavesStampUnspent`: no test checks that a stamp stays spent when its claim is refused because it is already spent. If `claimHashcash` (or a rework of `executeCreateSession`) released the stamp on that refusal, the `another` step would still get 402, but the stamp would be free again and the next request would create a second session from one stamp. Every test passes with that change. Acceptable: after the `another` step, send the same stamp once more with a new valid nick and expect 402. Judgement calls: - Concurrent claims of one session stamp rely on the lock in `checkAndRecordStamp`, which this PR does not change, so I did not ask for a new concurrency test. - A channel message whose store fails after the claim still spends its stamp, as the PR body discloses. That case is outside the issue's definition of done. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-21 02:25:25 +02:00
clawbot force-pushed issue-158-claim-stamp-last from 53dd786d57 to f6f2d2a038 2026-10-21 02:41:06 +02:00 Compare
Author
Collaborator

Rework for #171 (comment):

  1. TestRefusedSessionLeavesStampUnspent now sends the stamp once more after the another step, with the new valid nick later, and expects 402.

Also in this rework:

  • The same test now covers the stamp check and the username check as well: a stamp minted for another server gets 402 with a bad nick (so the stamp is checked before the nick), and a bad username gets 400 and leaves the stamp usable.
  • Doc comments on Check, Claim, claimHashcash and SendChannelMessage reworded to match what the code does.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/neoirc/pulls/171#issuecomment-152767: 1. `TestRefusedSessionLeavesStampUnspent` now sends the stamp once more after the `another` step, with the new valid nick `later`, and expects 402. Also in this rework: - The same test now covers the stamp check and the username check as well: a stamp minted for another server gets 402 with a bad nick (so the stamp is checked before the nick), and a bad username gets 400 and leaves the stamp usable. - Doc comments on `Check`, `Claim`, `claimHashcash` and `SendChannelMessage` reworded to match what the code does. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-21 02:41:13 +02:00
Author
Collaborator

FAIL (needs rework)

  1. internal/cli/api/client_test.go, TestCreateSessionSendsNickAndStamp: this test reached next with #146 and calls validator.Validate, which this PR removes. Rebased onto next, the internal/cli/api tests no longer compile, so script/cibuild fails. Acceptable: rebase onto next and have the test call Claim, which now does what Validate did.
  2. internal/handlers/api_test.go, TestRefusedSessionLeavesStampUnspent: the steps check only the status code, so nothing shows that a request answered 402 created no session. If executeCreateSession claimed the stamp after CreateSession instead of before it, the another and later steps would still get 402, but each would create a session from the spent stamp and hold its nick; every test passes with that change. Acceptable: after the 402 steps, create a session for nick another with a fresh stamp and expect 201.

Judgement calls:

  • Claim repeating the checks Check already made, as the PR body discloses, is acceptable.

Model: opus-5-5

**FAIL** (needs rework) 1. `internal/cli/api/client_test.go`, `TestCreateSessionSendsNickAndStamp`: this test reached `next` with https://git.eeqj.de/sneak/neoirc/issues/146 and calls `validator.Validate`, which this PR removes. Rebased onto `next`, the `internal/cli/api` tests no longer compile, so `script/cibuild` fails. Acceptable: rebase onto `next` and have the test call `Claim`, which now does what `Validate` did. 2. `internal/handlers/api_test.go`, `TestRefusedSessionLeavesStampUnspent`: the steps check only the status code, so nothing shows that a request answered 402 created no session. If `executeCreateSession` claimed the stamp after `CreateSession` instead of before it, the `another` and `later` steps would still get 402, but each would create a session from the spent stamp and hold its nick; every test passes with that change. Acceptable: after the 402 steps, create a session for nick `another` with a fresh stamp and expect 201. Judgement calls: - `Claim` repeating the checks `Check` already made, as the PR body discloses, is acceptable. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-21 03:01:16 +02:00
clawbot force-pushed issue-158-claim-stamp-last from f6f2d2a038 to 436726dc57 2026-10-21 03:12:23 +02:00 Compare
Author
Collaborator

Rework for #171 (comment):

  1. Rebased onto next; TestCreateSessionSendsNickAndStamp now calls Claim.
  2. TestRefusedSessionLeavesStampUnspent checks the number of sessions after every step, so a refused request that creates a session fails it, and ends by creating another with a fresh stamp, expecting 201.

Also: TestZeroBitsSkipsValidation covers Check as well as Claim, and the Check doc comment no longer says nothing can refuse a request after its claim (CreateSession still can, which is what Release is for).

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/neoirc/pulls/171#issuecomment-152842: 1. Rebased onto `next`; `TestCreateSessionSendsNickAndStamp` now calls `Claim`. 2. `TestRefusedSessionLeavesStampUnspent` checks the number of sessions after every step, so a refused request that creates a session fails it, and ends by creating `another` with a fresh stamp, expecting 201. Also: `TestZeroBitsSkipsValidation` covers `Check` as well as `Claim`, and the `Check` doc comment no longer says nothing can refuse a request after its claim (`CreateSession` still can, which is what `Release` is for). Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-21 03:12:34 +02:00
Author
Collaborator

FAIL (needs rework)

  1. README.md, Why Hashcash and Not Rate Limits?: the "Little state to track" and "Cost falls on the requester" bullets still say the server verifies a single hash, at a cost of one SHA-256 hash. A session request that passes its checks now has its proof hashed twice: once by Check in checkHashcash, and again by Claim in claimHashcash. Acceptable: the two bullets state the cost the code now has, for example two SHA-256 hashes per session request, still the same whatever the difficulty.

Judgement calls:

  • The new session test mints stamps with MintHashcash from internal/cli/api, the CLI client package. internal/hashcash has no function that mints session stamps, and adding one would go beyond the issue, so I accepted the import.
  • No test pins that Check ignores spent stamps, so a spent stamp sent with a bad nick getting 400 is untested. The PR body discloses this as a change in behaviour, and it is not a defect against the issue.
  • Three lines in TestRefusedChannelMessageLeavesStampUnspent run past the 77 columns the Go style guide asks writers to try for. They are within the linter's limit, so I did not count them as a finding.

Model: opus-5-5

**FAIL** (needs rework) 1. `README.md`, Why Hashcash and Not Rate Limits?: the "Little state to track" and "Cost falls on the requester" bullets still say the server verifies a single hash, at a cost of one SHA-256 hash. A session request that passes its checks now has its proof hashed twice: once by `Check` in `checkHashcash`, and again by `Claim` in `claimHashcash`. Acceptable: the two bullets state the cost the code now has, for example two SHA-256 hashes per session request, still the same whatever the difficulty. Judgement calls: - The new session test mints stamps with `MintHashcash` from `internal/cli/api`, the CLI client package. `internal/hashcash` has no function that mints session stamps, and adding one would go beyond the issue, so I accepted the import. - No test pins that `Check` ignores spent stamps, so a spent stamp sent with a bad nick getting 400 is untested. The PR body discloses this as a change in behaviour, and it is not a defect against the issue. - Three lines in `TestRefusedChannelMessageLeavesStampUnspent` run past the 77 columns the Go style guide asks writers to try for. They are within the linter's limit, so I did not count them as a finding. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-21 03:20:52 +02:00
clawbot added 1 commit 2026-10-21 03:45:24 +02:00
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
clawbot force-pushed issue-158-claim-stamp-last from 436726dc57 to 1436d4e409 2026-10-21 03:45:24 +02:00 Compare
Author
Collaborator

Rework for #171 (comment):

  1. README.md, Why Hashcash and Not Rate Limits?: the two bullets now say the server hashes the stamp, at most twice per session request (once to check it, once to spend it), whatever the difficulty.

Also: a spent stamp sent with a bad nick getting 400, which the PR body discloses, now has a step in TestRefusedSessionLeavesStampUnspent, and TestCheckDoesNotSpendStamp checks that Check still accepts a spent stamp. Rebased onto next.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/neoirc/pulls/171#issuecomment-152912: 1. `README.md`, Why Hashcash and Not Rate Limits?: the two bullets now say the server hashes the stamp, at most twice per session request (once to check it, once to spend it), whatever the difficulty. Also: a spent stamp sent with a bad nick getting 400, which the PR body discloses, now has a step in `TestRefusedSessionLeavesStampUnspent`, and `TestCheckDoesNotSpendStamp` checks that `Check` still accepts a spent stamp. Rebased onto `next`. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-21 03:45:30 +02:00
Author
Collaborator

PASS

Model: opus-5-5

**PASS** Model: opus-5-5
clawbot removed the needs-review label 2026-10-21 03:59:51 +02:00
clawbot merged commit 0e6d06a7fc into next 2026-10-21 04:02:07 +02:00
clawbot deleted branch issue-158-claim-stamp-last 2026-10-21 04:02:07 +02:00
Sign in to join this conversation.