From 0d563adf31b4286eaf627a39362ebce4879e5a90 Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 10 Mar 2026 10:38:45 -0700 Subject: [PATCH] test: add hashcash validator tests with bits=2 Comprehensive test suite covering: - Mint and validate with bits=2 - Replay detection - Resource mismatch - Invalid format, bad version, bad date - Insufficient difficulty - Expired stamps - Zero bits bypass - Long date format (YYMMDDHHMMSS) - Multiple unique stamps - Higher difficulty stamps accepted at lower threshold --- internal/hashcash/hashcash_test.go | 261 +++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 internal/hashcash/hashcash_test.go diff --git a/internal/hashcash/hashcash_test.go b/internal/hashcash/hashcash_test.go new file mode 100644 index 0000000..28dbe9f --- /dev/null +++ b/internal/hashcash/hashcash_test.go @@ -0,0 +1,261 @@ +package hashcash_test + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "math/big" + "testing" + "time" + + "git.eeqj.de/sneak/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.Validate(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.Validate(stamp, testBits) + if err != nil { + t.Fatalf("first use failed: %v", err) + } + + err = validator.Validate(stamp, testBits) + if err == nil { + t.Fatal("replay not detected") + } +} + +func TestResourceMismatch(t *testing.T) { + t.Parallel() + + validator := hashcash.NewValidator("correct-resource") + stamp := mintStampWithDate( + t, testBits, "wrong-resource", todayDate(), + ) + + err := validator.Validate(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.Validate( + "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.Validate(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.Validate(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.Validate(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.Validate("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.Validate(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.Validate(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.Validate(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.Validate(stamp, testBits) + if err != nil { + t.Fatalf( + "higher-difficulty stamp rejected: %v", + err, + ) + } +}