check / check (push) Waiting to run
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
313 lines
6.1 KiB
Go
313 lines
6.1 KiB
Go
package hashcash_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/neoirc/internal/hashcash"
|
|
)
|
|
|
|
const testBits = 2
|
|
|
|
// mintStampWithDate creates a valid hashcash stamp using
|
|
// the given date string.
|
|
func mintStampWithDate(
|
|
tb testing.TB,
|
|
bits int,
|
|
resource string,
|
|
date string,
|
|
) string {
|
|
tb.Helper()
|
|
|
|
prefix := fmt.Sprintf(
|
|
"1:%d:%s:%s::", bits, date, resource,
|
|
)
|
|
|
|
for {
|
|
counterVal, err := rand.Int(
|
|
rand.Reader, big.NewInt(1<<48),
|
|
)
|
|
if err != nil {
|
|
tb.Fatalf("random counter: %v", err)
|
|
}
|
|
|
|
stamp := prefix + hex.EncodeToString(
|
|
counterVal.Bytes(),
|
|
)
|
|
hash := sha256.Sum256([]byte(stamp))
|
|
|
|
if hasLeadingZeroBits(hash[:], bits) {
|
|
return stamp
|
|
}
|
|
}
|
|
}
|
|
|
|
// hasLeadingZeroBits checks if hash has at least numBits
|
|
// leading zero bits. Duplicated here for test minting.
|
|
func hasLeadingZeroBits(
|
|
hash []byte,
|
|
numBits int,
|
|
) bool {
|
|
fullBytes := numBits / 8
|
|
remainBits := numBits % 8
|
|
|
|
for idx := range fullBytes {
|
|
if hash[idx] != 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if remainBits > 0 && fullBytes < len(hash) {
|
|
mask := byte(0xFF << (8 - remainBits))
|
|
|
|
if hash[fullBytes]&mask != 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func todayDate() string {
|
|
return time.Now().UTC().Format("060102")
|
|
}
|
|
|
|
func TestMintAndValidate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "test-resource", todayDate(),
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("valid stamp rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReplayDetection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "test-resource", todayDate(),
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("first use failed: %v", err)
|
|
}
|
|
|
|
err = validator.Claim(stamp, testBits)
|
|
if err == nil {
|
|
t.Fatal("replay not detected")
|
|
}
|
|
}
|
|
|
|
// TestCheckDoesNotSpendStamp checks that a stamp Check
|
|
// accepts can still be claimed, and only once.
|
|
func TestCheckDoesNotSpendStamp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "test-resource", todayDate(),
|
|
)
|
|
|
|
for range 2 {
|
|
err := validator.Check(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("valid stamp rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("claim after check failed: %v", err)
|
|
}
|
|
|
|
err = validator.Claim(stamp, testBits)
|
|
if err == nil {
|
|
t.Fatal("replay not detected")
|
|
}
|
|
}
|
|
|
|
// TestReleasedStampCanBeClaimedAgain checks that Release
|
|
// gives a claimed stamp back.
|
|
func TestReleasedStampCanBeClaimedAgain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "test-resource", todayDate(),
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("first claim failed: %v", err)
|
|
}
|
|
|
|
validator.Release(stamp)
|
|
|
|
err = validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("claim after release failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResourceMismatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("correct-resource")
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "wrong-resource", todayDate(),
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err == nil {
|
|
t.Fatal("expected resource mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestInvalidStampFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
|
|
err := validator.Claim(
|
|
"not:a:valid:stamp", testBits,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected error for bad format")
|
|
}
|
|
}
|
|
|
|
func TestBadVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
stamp := fmt.Sprintf(
|
|
"2:%d:%s:%s::abc123",
|
|
testBits, todayDate(), "test-resource",
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err == nil {
|
|
t.Fatal("expected bad version error")
|
|
}
|
|
}
|
|
|
|
func TestInsufficientDifficulty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
// Claimed bits=1, but we require testBits=2.
|
|
stamp := fmt.Sprintf(
|
|
"1:1:%s:%s::counter",
|
|
todayDate(), "test-resource",
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err == nil {
|
|
t.Fatal("expected insufficient bits error")
|
|
}
|
|
}
|
|
|
|
func TestExpiredStamp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
oldDate := time.Now().Add(-72 * time.Hour).
|
|
UTC().Format("060102")
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "test-resource", oldDate,
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err == nil {
|
|
t.Fatal("expected expired stamp error")
|
|
}
|
|
}
|
|
|
|
func TestZeroBitsSkipsValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
|
|
err := validator.Claim("garbage", 0)
|
|
if err != nil {
|
|
t.Fatalf("zero bits should skip: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLongDateFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
longDate := time.Now().UTC().Format("060102150405")
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "test-resource", longDate,
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("long date stamp rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBadDateFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
stamp := fmt.Sprintf(
|
|
"1:%d:BADDATE:%s::counter",
|
|
testBits, "test-resource",
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err == nil {
|
|
t.Fatal("expected bad date error")
|
|
}
|
|
}
|
|
|
|
func TestMultipleUniqueStamps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := hashcash.NewValidator("test-resource")
|
|
|
|
for range 5 {
|
|
stamp := mintStampWithDate(
|
|
t, testBits, "test-resource", todayDate(),
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf("unique stamp rejected: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHigherBitsStillValid(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Mint with bits=4 but validate requiring only 2.
|
|
validator := hashcash.NewValidator("test-resource")
|
|
stamp := mintStampWithDate(
|
|
t, 4, "test-resource", todayDate(),
|
|
)
|
|
|
|
err := validator.Claim(stamp, testBits)
|
|
if err != nil {
|
|
t.Fatalf(
|
|
"higher-difficulty stamp rejected: %v",
|
|
err,
|
|
)
|
|
}
|
|
}
|