hash-of-wisdom/internal/pow/solver/solver_test.go

286 lines
7.2 KiB
Go
Raw Normal View History

2025-08-22 15:33:45 +03:00
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!
}