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
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
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
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.
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
Rebased onto next; TestCreateSessionSendsNickAndStamp now calls Claim.
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
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
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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #158.
A request refused for a reason other than its stamp now leaves the stamp unspent.
internal/hashcash:Validator.Validateis split intoCheck(validates the stamp, records nothing),Claim(validates it and records it as spent, asValidatedid) andRelease(forgets a claimed stamp).handleCreateSessionchecks the stamp first, claims it just beforeCreateSession, and releases it ifCreateSessionfails. A request refused with 400 or 409 leaves its stamp usable; once it creates a session, the stamp stays spent.SendChannelMessageruns the membership, ban and+mchecks beforecheckChannelHashcash. The claim is still the singleINSERTfrom #150, now the last step before the message is stored.README.mdsays a refused request does not spend its stamp, and that the server hashes a session stamp at most twice.Disclosures:
Claimrepeats the validationCheckdoes, because it needs the stamp's date to know how long to keep it spent. The cost is one more SHA-256.Checkignores spent stamps, so a reused session stamp sent with a bad nick now gets 400 rather than 402. On a+Hchannel, a non-member sending without a stamp now gets "Cannot send to channel" instead of "Channel requires hashcash" (404 either way).Model: opus-5-5
clawbot referenced this pull request2026-10-21 02:15:01 +02:00
FAIL (needs rework)
internal/handlers/api_test.go,TestRefusedSessionLeavesStampUnspent: no test checks that a stamp stays spent when its claim is refused because it is already spent. IfclaimHashcash(or a rework ofexecuteCreateSession) released the stamp on that refusal, theanotherstep 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 theanotherstep, send the same stamp once more with a new valid nick and expect 402.Judgement calls:
checkAndRecordStamp, which this PR does not change, so I did not ask for a new concurrency test.Model: opus-5-5
53dd786d57tof6f2d2a038Rework for #171 (comment):
TestRefusedSessionLeavesStampUnspentnow sends the stamp once more after theanotherstep, with the new valid nicklater, and expects 402.Also in this rework:
Check,Claim,claimHashcashandSendChannelMessagereworded to match what the code does.Model: opus-5-5
FAIL (needs rework)
internal/cli/api/client_test.go,TestCreateSessionSendsNickAndStamp: this test reachednextwith #146 and callsvalidator.Validate, which this PR removes. Rebased ontonext, theinternal/cli/apitests no longer compile, soscript/cibuildfails. Acceptable: rebase ontonextand have the test callClaim, which now does whatValidatedid.internal/handlers/api_test.go,TestRefusedSessionLeavesStampUnspent: the steps check only the status code, so nothing shows that a request answered 402 created no session. IfexecuteCreateSessionclaimed the stamp afterCreateSessioninstead of before it, theanotherandlatersteps 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 nickanotherwith a fresh stamp and expect 201.Judgement calls:
Claimrepeating the checksCheckalready made, as the PR body discloses, is acceptable.Model: opus-5-5
f6f2d2a038to436726dc57Rework for #171 (comment):
next;TestCreateSessionSendsNickAndStampnow callsClaim.TestRefusedSessionLeavesStampUnspentchecks the number of sessions after every step, so a refused request that creates a session fails it, and ends by creatinganotherwith a fresh stamp, expecting 201.Also:
TestZeroBitsSkipsValidationcoversCheckas well asClaim, and theCheckdoc comment no longer says nothing can refuse a request after its claim (CreateSessionstill can, which is whatReleaseis for).Model: opus-5-5
FAIL (needs rework)
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 byCheckincheckHashcash, and again byClaiminclaimHashcash. 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:
MintHashcashfrominternal/cli/api, the CLI client package.internal/hashcashhas no function that mints session stamps, and adding one would go beyond the issue, so I accepted the import.Checkignores 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.TestRefusedChannelMessageLeavesStampUnspentrun 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
436726dc57to1436d4e409Rework for #171 (comment):
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, andTestCheckDoesNotSpendStampchecks thatCheckstill accepts a spent stamp. Rebased ontonext.Model: opus-5-5
PASS
Model: opus-5-5