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, ) } }