Add tests for pow component
This commit is contained in:
parent
b828f6f6ca
commit
f752389dba
|
|
@ -30,13 +30,13 @@
|
|||
- [X] Test HMAC tamper detection and validation
|
||||
- [X] Add difficulty adjustment mechanisms (config-based)
|
||||
|
||||
- [ ] **Testing & Performance**
|
||||
- [ ] Unit tests for challenge generation and verification
|
||||
- [ ] Unit tests for HMAC signing and validation
|
||||
- [ ] Unit tests for PoW solution finding and verification
|
||||
- [ ] Benchmark tests for different difficulty levels
|
||||
- [ ] Test edge cases (expired challenges, invalid HMAC, wrong difficulty)
|
||||
- [ ] Performance tests for concurrent challenge operations
|
||||
- [X] **Testing & Performance**
|
||||
- [X] Unit tests for challenge generation and verification
|
||||
- [X] Unit tests for HMAC signing and validation
|
||||
- [X] Unit tests for PoW solution finding and verification
|
||||
- [X] Benchmark tests for different difficulty levels
|
||||
- [X] Test edge cases (expired challenges, invalid HMAC, wrong difficulty)
|
||||
- [X] Performance tests for concurrent challenge operations
|
||||
|
||||
## Phase 2: Basic Server Architecture
|
||||
- [ ] Set up dependency injection framework (wire/dig)
|
||||
|
|
|
|||
163
internal/pow/challenge/config_test.go
Normal file
163
internal/pow/challenge/config_test.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package challenge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts []configOption
|
||||
wantErr bool
|
||||
expectedErr error
|
||||
validate func(*Config) error
|
||||
}{
|
||||
{
|
||||
name: "default config",
|
||||
opts: nil,
|
||||
wantErr: false,
|
||||
validate: func(c *Config) error {
|
||||
if c.DefaultDifficulty != 4 {
|
||||
return errors.New("expected default difficulty 4")
|
||||
}
|
||||
if c.RandomBytes != 6 {
|
||||
return errors.New("expected random bytes 6")
|
||||
}
|
||||
if len(c.HMACSecret) == 0 {
|
||||
return errors.New("expected HMAC secret to be generated")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom difficulty",
|
||||
opts: []configOption{WithDefaultDifficulty(8)},
|
||||
wantErr: false,
|
||||
validate: func(c *Config) error {
|
||||
if c.DefaultDifficulty != 8 {
|
||||
return errors.New("expected custom difficulty 8")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid random bytes - too low",
|
||||
opts: []configOption{WithRandomBytes(0)},
|
||||
wantErr: true,
|
||||
expectedErr: ErrInvalidConfig,
|
||||
},
|
||||
{
|
||||
name: "invalid random bytes - too high",
|
||||
opts: []configOption{WithRandomBytes(50)},
|
||||
wantErr: true,
|
||||
expectedErr: ErrInvalidConfig,
|
||||
},
|
||||
{
|
||||
name: "invalid difficulty - min too low",
|
||||
opts: []configOption{WithMinDifficulty(0)},
|
||||
wantErr: true,
|
||||
expectedErr: ErrInvalidConfig,
|
||||
},
|
||||
{
|
||||
name: "invalid difficulty - max less than min",
|
||||
opts: []configOption{
|
||||
WithMinDifficulty(5),
|
||||
WithMaxDifficulty(3),
|
||||
},
|
||||
wantErr: true,
|
||||
expectedErr: ErrInvalidConfig,
|
||||
},
|
||||
{
|
||||
name: "invalid difficulty - default out of range",
|
||||
opts: []configOption{
|
||||
WithMinDifficulty(5),
|
||||
WithMaxDifficulty(10),
|
||||
WithDefaultDifficulty(15),
|
||||
},
|
||||
wantErr: true,
|
||||
expectedErr: ErrInvalidConfig,
|
||||
},
|
||||
{
|
||||
name: "empty HMAC secret",
|
||||
opts: []configOption{WithHMACSecret(nil)},
|
||||
wantErr: true,
|
||||
expectedErr: ErrInvalidConfig,
|
||||
},
|
||||
{
|
||||
name: "negative TTL",
|
||||
opts: []configOption{WithChallengeTTL(-time.Minute)},
|
||||
wantErr: true,
|
||||
expectedErr: ErrInvalidConfig,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config, err := NewConfig(tt.opts...)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
if !errors.Is(err, tt.expectedErr) {
|
||||
t.Fatalf("expected error %v, got %v", tt.expectedErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if tt.validate != nil {
|
||||
if err := tt.validate(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigOptions(t *testing.T) {
|
||||
secret := []byte("test-secret")
|
||||
config, err := NewConfig(
|
||||
WithDefaultDifficulty(6),
|
||||
WithMaxDifficulty(12),
|
||||
WithMinDifficulty(2),
|
||||
WithChallengeTTL(10*time.Minute),
|
||||
WithHMACSecret(secret),
|
||||
WithResource("api"),
|
||||
WithRandomBytes(8),
|
||||
WithLoadAdjustmentBits(2),
|
||||
WithFailurePenaltyBits(3),
|
||||
WithMaxFailurePenaltyBits(9),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if config.DefaultDifficulty != 6 {
|
||||
t.Errorf("expected DefaultDifficulty 6, got %d", config.DefaultDifficulty)
|
||||
}
|
||||
if config.MaxDifficulty != 12 {
|
||||
t.Errorf("expected MaxDifficulty 12, got %d", config.MaxDifficulty)
|
||||
}
|
||||
if config.MinDifficulty != 2 {
|
||||
t.Errorf("expected MinDifficulty 2, got %d", config.MinDifficulty)
|
||||
}
|
||||
if config.ChallengeTTL != 10*time.Minute {
|
||||
t.Errorf("expected ChallengeTTL 10m, got %v", config.ChallengeTTL)
|
||||
}
|
||||
if string(config.HMACSecret) != string(secret) {
|
||||
t.Errorf("expected HMACSecret %s, got %s", secret, config.HMACSecret)
|
||||
}
|
||||
if config.Resource != "api" {
|
||||
t.Errorf("expected Resource 'api', got %s", config.Resource)
|
||||
}
|
||||
if config.RandomBytes != 8 {
|
||||
t.Errorf("expected RandomBytes 8, got %d", config.RandomBytes)
|
||||
}
|
||||
}
|
||||
174
internal/pow/challenge/generator_test.go
Normal file
174
internal/pow/challenge/generator_test.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package challenge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewGenerator(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
|
||||
if generator == nil {
|
||||
t.Fatal("expected generator to be created")
|
||||
}
|
||||
|
||||
if generator.config != config {
|
||||
t.Error("expected generator to store config reference")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerator_GenerateChallenge(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
|
||||
challenge, err := generator.GenerateChallenge()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
// Validate basic properties
|
||||
if challenge.Difficulty != config.DefaultDifficulty {
|
||||
t.Errorf("expected difficulty %d, got %d", config.DefaultDifficulty, challenge.Difficulty)
|
||||
}
|
||||
|
||||
if challenge.Resource != config.Resource {
|
||||
t.Errorf("expected resource %s, got %s", config.Resource, challenge.Resource)
|
||||
}
|
||||
|
||||
if len(challenge.Random) != config.RandomBytes {
|
||||
t.Errorf("expected %d random bytes, got %d", config.RandomBytes, len(challenge.Random))
|
||||
}
|
||||
|
||||
if len(challenge.HMAC) == 0 {
|
||||
t.Error("expected HMAC to be set")
|
||||
}
|
||||
|
||||
if challenge.Timestamp <= 0 {
|
||||
t.Error("expected positive timestamp")
|
||||
}
|
||||
|
||||
// Verify timestamp is recent (within last minute)
|
||||
now := time.Now().Unix()
|
||||
if challenge.Timestamp < now-60 || challenge.Timestamp > now {
|
||||
t.Errorf("expected timestamp to be recent, got %d (now: %d)", challenge.Timestamp, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerator_GenerateChallengeWithOptions(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
|
||||
customDifficulty := 8
|
||||
challenge, err := generator.GenerateChallenge(WithDifficulty(customDifficulty))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if challenge.Difficulty != customDifficulty {
|
||||
t.Errorf("expected custom difficulty %d, got %d", customDifficulty, challenge.Difficulty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerator_GenerateChallengeValidation(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
difficulty int
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid difficulty within range",
|
||||
difficulty: 5,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "difficulty too low",
|
||||
difficulty: config.MinDifficulty - 1,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "difficulty too high",
|
||||
difficulty: config.MaxDifficulty + 1,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := generator.GenerateChallenge(WithDifficulty(tt.difficulty))
|
||||
|
||||
if tt.expectErr && err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
|
||||
if !tt.expectErr && err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if err != nil && err != ErrInvalidDifficulty {
|
||||
t.Errorf("expected ErrInvalidDifficulty, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerator_HMACIntegrity(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
|
||||
challenge, err := generator.GenerateChallenge()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify HMAC with original secret
|
||||
err = challenge.VerifyHMAC(config.HMACSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("expected HMAC verification to pass, got: %v", err)
|
||||
}
|
||||
|
||||
// Verify HMAC fails with different secret
|
||||
differentConfig := TestConfig()
|
||||
err = challenge.VerifyHMAC(differentConfig.HMACSecret)
|
||||
if err != ErrInvalidHMAC {
|
||||
t.Fatalf("expected ErrInvalidHMAC with different secret, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerator_RandomnessUniqueness(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
|
||||
// Generate multiple challenges and ensure they have different random values
|
||||
challenges := make([]*Challenge, 10)
|
||||
for i := range challenges {
|
||||
challenge, err := generator.GenerateChallenge()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating challenge %d: %v", i, err)
|
||||
}
|
||||
challenges[i] = challenge
|
||||
}
|
||||
|
||||
// Check that all random values are different
|
||||
for i := 0; i < len(challenges); i++ {
|
||||
for j := i + 1; j < len(challenges); j++ {
|
||||
if string(challenges[i].Random) == string(challenges[j].Random) {
|
||||
t.Errorf("challenges %d and %d have identical random values: %x", i, j, challenges[i].Random)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithDifficulty(t *testing.T) {
|
||||
challenge := &Challenge{Difficulty: 4}
|
||||
option := WithDifficulty(8)
|
||||
|
||||
option(challenge)
|
||||
|
||||
if challenge.Difficulty != 8 {
|
||||
t.Errorf("expected difficulty 8, got %d", challenge.Difficulty)
|
||||
}
|
||||
}
|
||||
225
internal/pow/challenge/types_test.go
Normal file
225
internal/pow/challenge/types_test.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package challenge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestChallenge_CanonicalBytes(t *testing.T) {
|
||||
challenge := &Challenge{
|
||||
Timestamp: 1640995200,
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
canonical := challenge.CanonicalBytes()
|
||||
expected := "1640995200:4:quotes:a1b2c3d4e5f6"
|
||||
|
||||
if string(canonical) != expected {
|
||||
t.Errorf("expected canonical bytes %s, got %s", expected, canonical)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallenge_BuildSolutionString(t *testing.T) {
|
||||
challenge := &Challenge{
|
||||
Timestamp: 1640995200,
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
solution := challenge.BuildSolutionString(12345)
|
||||
expected := "quotes:1640995200:4:a1b2c3d4e5f6:12345"
|
||||
|
||||
if string(solution) != expected {
|
||||
t.Errorf("expected solution string %s, got %s", expected, solution)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallenge_VerifySolution(t *testing.T) {
|
||||
challenge := &Challenge{
|
||||
Timestamp: 1640995200,
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
// Test with a known nonce that should fail (random nonce)
|
||||
if challenge.VerifySolution(12345) {
|
||||
t.Error("expected solution to be invalid for random nonce")
|
||||
}
|
||||
|
||||
// Test with difficulty 0 (should always pass)
|
||||
challenge.Difficulty = 0
|
||||
if !challenge.VerifySolution(12345) {
|
||||
t.Error("expected solution to be valid for difficulty 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasLeadingZeroBits(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash []byte
|
||||
difficulty int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "difficulty 0 always passes",
|
||||
hash: []byte{0xFF, 0xFF, 0xFF},
|
||||
difficulty: 0,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "difficulty 8 with 1 zero byte",
|
||||
hash: []byte{0x00, 0xFF, 0xFF},
|
||||
difficulty: 8,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "difficulty 8 without full zero byte",
|
||||
hash: []byte{0x01, 0xFF, 0xFF},
|
||||
difficulty: 8,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "difficulty 4 with partial zero bits",
|
||||
hash: []byte{0x0F, 0xFF, 0xFF}, // 0000 1111
|
||||
difficulty: 4,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "difficulty 4 without enough zero bits",
|
||||
hash: []byte{0x1F, 0xFF, 0xFF}, // 0001 1111
|
||||
difficulty: 4,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "difficulty 3 with 3 zero bits",
|
||||
hash: []byte{0x1F, 0xFF, 0xFF}, // 000 11111
|
||||
difficulty: 3,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := hasLeadingZeroBits(tt.hash, tt.difficulty)
|
||||
if result != tt.expected {
|
||||
t.Errorf("expected %v, got %v for hash %x with difficulty %d", tt.expected, result, tt.hash, tt.difficulty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallenge_IsExpired(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
timestamp int64
|
||||
ttl time.Duration
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "fresh challenge",
|
||||
timestamp: now.Unix(),
|
||||
ttl: 5 * time.Minute,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expired challenge",
|
||||
timestamp: now.Add(-10 * time.Minute).Unix(),
|
||||
ttl: 5 * time.Minute,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "exactly at TTL",
|
||||
timestamp: now.Add(-5 * time.Minute).Unix(),
|
||||
ttl: 5 * time.Minute,
|
||||
expected: true, // Should be expired when exactly at TTL
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
challenge := &Challenge{Timestamp: tt.timestamp}
|
||||
result := challenge.IsExpired(tt.ttl)
|
||||
if result != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChallenge_SignAndVerifyHMAC(t *testing.T) {
|
||||
config := TestConfig()
|
||||
challenge := &Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
// Sign the challenge
|
||||
challenge.Sign(config.HMACSecret)
|
||||
|
||||
if len(challenge.HMAC) == 0 {
|
||||
t.Fatal("expected HMAC to be set after signing")
|
||||
}
|
||||
|
||||
// Verify the HMAC
|
||||
err := challenge.VerifyHMAC(config.HMACSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("expected HMAC verification to succeed, got: %v", err)
|
||||
}
|
||||
|
||||
// Test with wrong secret
|
||||
wrongSecret := []byte("wrong-secret")
|
||||
err = challenge.VerifyHMAC(wrongSecret)
|
||||
if err != ErrInvalidHMAC {
|
||||
t.Fatalf("expected ErrInvalidHMAC with wrong secret, got: %v", err)
|
||||
}
|
||||
|
||||
// Test with empty HMAC
|
||||
challenge.HMAC = nil
|
||||
err = challenge.VerifyHMAC(config.HMACSecret)
|
||||
if err != ErrMalformedChallenge {
|
||||
t.Fatalf("expected ErrMalformedChallenge with empty HMAC, got: %v", err)
|
||||
}
|
||||
|
||||
// Test tampering detection
|
||||
challenge.Sign(config.HMACSecret)
|
||||
originalDifficulty := challenge.Difficulty
|
||||
challenge.Difficulty = 10 // Tamper with difficulty
|
||||
err = challenge.VerifyHMAC(config.HMACSecret)
|
||||
if err != ErrInvalidHMAC {
|
||||
t.Fatalf("expected ErrInvalidHMAC after tampering, got: %v", err)
|
||||
}
|
||||
challenge.Difficulty = originalDifficulty // Restore for next test
|
||||
}
|
||||
|
||||
func TestSolution_Verify(t *testing.T) {
|
||||
challenge := &Challenge{
|
||||
Timestamp: 1640995200,
|
||||
Difficulty: 0, // Use difficulty 0 to guarantee success
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
solution := &Solution{
|
||||
Challenge: *challenge,
|
||||
Nonce: 12345,
|
||||
}
|
||||
|
||||
if !solution.Verify() {
|
||||
t.Error("expected solution to be valid with difficulty 0")
|
||||
}
|
||||
|
||||
// Test with higher difficulty (should likely fail with random nonce)
|
||||
solution.Challenge.Difficulty = 10
|
||||
if solution.Verify() {
|
||||
// This could theoretically pass but is extremely unlikely
|
||||
t.Log("got lucky with high difficulty - solution passed")
|
||||
}
|
||||
}
|
||||
154
internal/pow/challenge/verifier_test.go
Normal file
154
internal/pow/challenge/verifier_test.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package challenge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewVerifier(t *testing.T) {
|
||||
config := TestConfig()
|
||||
verifier := NewVerifier(config)
|
||||
|
||||
if verifier == nil {
|
||||
t.Fatal("expected verifier to be created")
|
||||
}
|
||||
|
||||
if verifier.config != config {
|
||||
t.Error("expected verifier to store config reference")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier_VerifyChallenge(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
verifier := NewVerifier(config)
|
||||
|
||||
// Generate a valid challenge
|
||||
challenge, err := generator.GenerateChallenge()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate challenge: %v", err)
|
||||
}
|
||||
|
||||
// Verify the challenge
|
||||
err = verifier.VerifyChallenge(challenge)
|
||||
if err != nil {
|
||||
t.Fatalf("expected challenge verification to pass, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier_VerifyChallenge_Expired(t *testing.T) {
|
||||
config := TestConfig()
|
||||
verifier := NewVerifier(config)
|
||||
|
||||
// Create an expired challenge
|
||||
expiredChallenge := &Challenge{
|
||||
Timestamp: time.Now().Add(-10 * time.Minute).Unix(), // 10 minutes ago
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
expiredChallenge.Sign(config.HMACSecret)
|
||||
|
||||
err := verifier.VerifyChallenge(expiredChallenge)
|
||||
if err != ErrExpiredChallenge {
|
||||
t.Fatalf("expected ErrExpiredChallenge, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier_VerifyChallenge_InvalidHMAC(t *testing.T) {
|
||||
config := TestConfig()
|
||||
verifier := NewVerifier(config)
|
||||
|
||||
// Create a challenge with invalid HMAC
|
||||
challenge := &Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
HMAC: []byte("invalid-hmac"),
|
||||
}
|
||||
|
||||
err := verifier.VerifyChallenge(challenge)
|
||||
if err != ErrInvalidHMAC {
|
||||
t.Fatalf("expected ErrInvalidHMAC, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier_VerifyChallenge_TamperedChallenge(t *testing.T) {
|
||||
config := TestConfig()
|
||||
generator := NewGenerator(config)
|
||||
verifier := NewVerifier(config)
|
||||
|
||||
// Generate a valid challenge
|
||||
challenge, err := generator.GenerateChallenge()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate challenge: %v", err)
|
||||
}
|
||||
|
||||
// Tamper with the challenge
|
||||
challenge.Difficulty = 10
|
||||
|
||||
// Verification should fail due to HMAC mismatch
|
||||
err = verifier.VerifyChallenge(challenge)
|
||||
if err != ErrInvalidHMAC {
|
||||
t.Fatalf("expected ErrInvalidHMAC for tampered challenge, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier_VerifyChallenge_DifferentSecret(t *testing.T) {
|
||||
config1 := TestConfig()
|
||||
config2 := TestConfig() // Different secret
|
||||
generator := NewGenerator(config1)
|
||||
verifier := NewVerifier(config2)
|
||||
|
||||
// Generate challenge with config1
|
||||
challenge, err := generator.GenerateChallenge()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate challenge: %v", err)
|
||||
}
|
||||
|
||||
// Try to verify with config2 (different secret)
|
||||
err = verifier.VerifyChallenge(challenge)
|
||||
if err != ErrInvalidHMAC {
|
||||
t.Fatalf("expected ErrInvalidHMAC with different secret, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier_VerifyChallenge_FutureTimestamp(t *testing.T) {
|
||||
config := TestConfig()
|
||||
verifier := NewVerifier(config)
|
||||
|
||||
// Create a challenge with future timestamp
|
||||
futureChallenge := &Challenge{
|
||||
Timestamp: time.Now().Add(10 * time.Minute).Unix(), // 10 minutes in future
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
futureChallenge.Sign(config.HMACSecret)
|
||||
|
||||
// Future challenges should still be valid (not expired)
|
||||
err := verifier.VerifyChallenge(futureChallenge)
|
||||
if err != nil {
|
||||
t.Fatalf("expected future challenge to be valid, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier_VerifyChallenge_EmptyHMAC(t *testing.T) {
|
||||
config := TestConfig()
|
||||
verifier := NewVerifier(config)
|
||||
|
||||
// Create a challenge without HMAC
|
||||
challenge := &Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 4,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
HMAC: nil,
|
||||
}
|
||||
|
||||
err := verifier.VerifyChallenge(challenge)
|
||||
if err != ErrMalformedChallenge {
|
||||
t.Fatalf("expected ErrMalformedChallenge for empty HMAC, got: %v", err)
|
||||
}
|
||||
}
|
||||
285
internal/pow/solver/solver_test.go
Normal file
285
internal/pow/solver/solver_test.go
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
package solver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hash-of-wisdom/internal/pow/challenge"
|
||||
)
|
||||
|
||||
func TestNewSolver(t *testing.T) {
|
||||
solver := NewSolver(WithWorkers(4))
|
||||
|
||||
if solver == nil {
|
||||
t.Fatal("expected solver to be created")
|
||||
}
|
||||
|
||||
if solver.workers != 4 {
|
||||
t.Errorf("expected 4 workers, got %d", solver.workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolver_SolveEasyChallenge(t *testing.T) {
|
||||
solver := NewSolver(WithWorkers(2)) // Use fewer workers for simpler test
|
||||
|
||||
// Create a challenge with low difficulty
|
||||
ch := &challenge.Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 1, // Very low difficulty should solve quickly
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
solution, err := solver.Solve(ctx, ch)
|
||||
if err != nil {
|
||||
t.Fatalf("expected solution to be found, got error: %v", err)
|
||||
}
|
||||
|
||||
if solution == nil {
|
||||
t.Fatal("expected solution to be returned")
|
||||
}
|
||||
|
||||
// Verify the solution is valid
|
||||
if !solution.Verify() {
|
||||
t.Error("expected solution to be valid")
|
||||
}
|
||||
|
||||
// Verify solution contains original challenge
|
||||
if solution.Challenge.Difficulty != ch.Difficulty {
|
||||
t.Errorf("expected challenge difficulty %d, got %d", ch.Difficulty, solution.Challenge.Difficulty)
|
||||
}
|
||||
|
||||
if string(solution.Challenge.Random) != string(ch.Random) {
|
||||
t.Errorf("expected challenge random %x, got %x", ch.Random, solution.Challenge.Random)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolver_SolveWithCancellation(t *testing.T) {
|
||||
solver := NewSolver(WithWorkers(2))
|
||||
|
||||
// Create a challenge with very high difficulty (extremely unlikely to solve quickly)
|
||||
ch := &challenge.Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 25, // Extremely high difficulty - will require billions of attempts
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
solution, err := solver.Solve(ctx, ch)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should timeout and return context error (unless we get extremely lucky)
|
||||
if err == nil {
|
||||
t.Log("Got extremely lucky and solved difficulty 25 quickly - this is statistically very unlikely!")
|
||||
if !solution.Verify() {
|
||||
t.Error("solution should be valid if we got one")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != context.DeadlineExceeded {
|
||||
t.Fatalf("expected context.DeadlineExceeded, got: %v", err)
|
||||
}
|
||||
|
||||
if solution != nil {
|
||||
t.Error("expected no solution when context cancelled")
|
||||
}
|
||||
|
||||
// Should return quickly due to cancellation (within reasonable time)
|
||||
if elapsed > 200*time.Millisecond {
|
||||
t.Errorf("solver took too long to cancel: %v", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolver_SolveDifficulty0(t *testing.T) {
|
||||
solver := NewSolver(WithWorkers(1))
|
||||
|
||||
// Difficulty 0 should always pass (no leading zero bits required)
|
||||
ch := &challenge.Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 0,
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
solution, err := solver.Solve(ctx, ch)
|
||||
if err != nil {
|
||||
t.Fatalf("expected solution for difficulty 0, got error: %v", err)
|
||||
}
|
||||
|
||||
if !solution.Verify() {
|
||||
t.Error("expected solution to be valid for difficulty 0")
|
||||
}
|
||||
|
||||
// Should find solution almost immediately (nonce 0 should work)
|
||||
if solution.Nonce != 0 {
|
||||
t.Log("Note: nonce 0 didn't work, but that's okay - any nonce works for difficulty 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolver_MultipleWorkers(t *testing.T) {
|
||||
// Test that multiple workers work correctly
|
||||
tests := []struct {
|
||||
name string
|
||||
workers int
|
||||
}{
|
||||
{"single worker", 1},
|
||||
{"multiple workers", 4},
|
||||
{"many workers", 8},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
solver := NewSolver(WithWorkers(tt.workers))
|
||||
|
||||
ch := &challenge.Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 2, // Moderate difficulty
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
solution, err := solver.Solve(ctx, ch)
|
||||
if err != nil {
|
||||
t.Fatalf("expected solution with %d workers, got error: %v", tt.workers, err)
|
||||
}
|
||||
|
||||
if !solution.Verify() {
|
||||
t.Errorf("expected valid solution with %d workers", tt.workers)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolver_ConcurrentSolves(t *testing.T) {
|
||||
solver := NewSolver(WithWorkers(4))
|
||||
|
||||
// Test that solver can handle multiple concurrent solve requests
|
||||
const numConcurrent = 3
|
||||
results := make(chan error, numConcurrent)
|
||||
|
||||
for i := 0; i < numConcurrent; i++ {
|
||||
go func(id int) {
|
||||
ch := &challenge.Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 2,
|
||||
Resource: "quotes",
|
||||
Random: []byte{byte(id), 0xb2, 0xc3, 0xd4, 0xe5, 0xf6}, // Different random for each
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
solution, err := solver.Solve(ctx, ch)
|
||||
if err != nil {
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
|
||||
if !solution.Verify() {
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
|
||||
results <- nil
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < numConcurrent; i++ {
|
||||
select {
|
||||
case err := <-results:
|
||||
if err != nil {
|
||||
t.Fatalf("concurrent solve %d failed: %v", i, err)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("concurrent solve timed out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSolver_Solve(b *testing.B) {
|
||||
solver := NewSolver(WithWorkers(4))
|
||||
|
||||
ch := &challenge.Challenge{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Difficulty: 3, // Moderate difficulty for benchmarking
|
||||
Resource: "quotes",
|
||||
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Use different random for each iteration to avoid caching
|
||||
ch.Random[0] = byte(i)
|
||||
|
||||
_, err := solver.Solve(ctx, ch)
|
||||
if err != nil {
|
||||
b.Fatalf("benchmark solve failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullRoundtrip(t *testing.T) {
|
||||
// Test full roundtrip: generate challenge -> solve -> verify
|
||||
config := challenge.TestConfig()
|
||||
generator := challenge.NewGenerator(config)
|
||||
verifier := challenge.NewVerifier(config)
|
||||
solver := NewSolver(WithWorkers(2))
|
||||
|
||||
// Generate a challenge
|
||||
ch, err := generator.GenerateChallenge()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate challenge: %v", err)
|
||||
}
|
||||
|
||||
// Verify the challenge is valid
|
||||
err = verifier.VerifyChallenge(ch)
|
||||
if err != nil {
|
||||
t.Fatalf("generated challenge failed verification: %v", err)
|
||||
}
|
||||
|
||||
// Set low difficulty for quick solve
|
||||
ch.Difficulty = 2
|
||||
ch.Sign(config.HMACSecret) // Re-sign after modifying difficulty
|
||||
|
||||
// Solve the challenge
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
solution, err := solver.Solve(ctx, ch)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to solve challenge: %v", err)
|
||||
}
|
||||
|
||||
// SERVER-SIDE VERIFICATION: Behave exactly like the server would
|
||||
|
||||
// 1. First verify the challenge authenticity and expiration (like server does)
|
||||
err = verifier.VerifyChallenge(&solution.Challenge)
|
||||
if err != nil {
|
||||
t.Fatalf("server challenge verification failed: %v", err)
|
||||
}
|
||||
|
||||
// 2. Then verify the PoW solution (like server does)
|
||||
if !solution.Verify() {
|
||||
t.Fatal("server PoW solution verification failed")
|
||||
}
|
||||
|
||||
// Success - server would now grant access to the quote!
|
||||
}
|
||||
Loading…
Reference in a new issue