286 lines
7.2 KiB
Go
286 lines
7.2 KiB
Go
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!
|
|
}
|