Compare commits
13 commits
764ac2e2a7
...
f9d4ee9017
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9d4ee9017 | ||
|
|
497fc75f1f | ||
|
|
f752389dba | ||
|
|
b828f6f6ca | ||
|
|
70341abeac | ||
|
|
b4a1c4dc37 | ||
|
|
859bd989be | ||
|
|
c5dcc2acce | ||
|
|
84844cdb66 | ||
|
|
19ebf54673 | ||
|
|
2d7a2bfaa3 | ||
|
|
7d521cfccf | ||
|
|
47d1280556 |
9
.pre-commit-config.yaml
Normal file
9
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
args: [--markdown-linebreak-ext=md] # сохранить двойной пробел в markdown как перенос
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args: [--fix=lf]
|
||||||
95
Taskfile.yml
Normal file
95
Taskfile.yml
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
test:
|
||||||
|
desc: Run all tests
|
||||||
|
cmds:
|
||||||
|
- go test ./...
|
||||||
|
|
||||||
|
test-verbose:
|
||||||
|
desc: Run all tests with verbose output
|
||||||
|
cmds:
|
||||||
|
- go test -v ./...
|
||||||
|
|
||||||
|
test-coverage:
|
||||||
|
desc: Run tests with coverage report
|
||||||
|
cmds:
|
||||||
|
- go test -cover ./...
|
||||||
|
|
||||||
|
test-coverage-html:
|
||||||
|
desc: Generate HTML coverage report
|
||||||
|
cmds:
|
||||||
|
- go test -coverprofile=coverage.out ./...
|
||||||
|
- go tool cover -html=coverage.out -o coverage.html
|
||||||
|
- echo Coverage report generated at coverage.html
|
||||||
|
|
||||||
|
test-race:
|
||||||
|
desc: Run tests with race detection
|
||||||
|
cmds:
|
||||||
|
- go test -race ./...
|
||||||
|
|
||||||
|
test-bench:
|
||||||
|
desc: Run benchmarks
|
||||||
|
cmds:
|
||||||
|
- go test -bench=. ./...
|
||||||
|
|
||||||
|
test-pow:
|
||||||
|
desc: Run only PoW package tests
|
||||||
|
cmds:
|
||||||
|
- go test -v ./internal/pow/...
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Build all packages
|
||||||
|
cmds:
|
||||||
|
- go build ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
desc: Clean build artifacts and test cache
|
||||||
|
cmds:
|
||||||
|
- go clean -cache
|
||||||
|
- go clean -testcache
|
||||||
|
- rm -f coverage.out coverage.html
|
||||||
|
|
||||||
|
lint:
|
||||||
|
desc: Run linter (if available)
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if command -v golangci-lint >/dev/null 2>&1; then
|
||||||
|
golangci-lint run
|
||||||
|
else
|
||||||
|
echo "golangci-lint not installed, skipping lint"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
desc: Format Go code
|
||||||
|
cmds:
|
||||||
|
- go fmt ./...
|
||||||
|
|
||||||
|
mod:
|
||||||
|
desc: Tidy and verify go modules
|
||||||
|
cmds:
|
||||||
|
- go mod tidy
|
||||||
|
- go mod verify
|
||||||
|
|
||||||
|
mocks:
|
||||||
|
desc: Generate mocks using mockery
|
||||||
|
cmds:
|
||||||
|
- mockery
|
||||||
|
|
||||||
|
check:
|
||||||
|
desc: Run all checks (fmt, build, test, lint)
|
||||||
|
deps: [fmt, build, test, lint]
|
||||||
|
|
||||||
|
dev:
|
||||||
|
desc: Development workflow - format, build and test
|
||||||
|
deps: [fmt, build, test]
|
||||||
|
|
||||||
|
cpu-burner:
|
||||||
|
desc: Run the CPU burner to stress test PoW solver
|
||||||
|
cmds:
|
||||||
|
- go run ./cmd/cpu-burner
|
||||||
|
|
||||||
|
burn:
|
||||||
|
desc: Alias for cpu-burner
|
||||||
|
cmds:
|
||||||
|
- task: cpu-burner
|
||||||
111
cmd/cpu-burner/main.go
Normal file
111
cmd/cpu-burner/main.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hash-of-wisdom/internal/pow/challenge"
|
||||||
|
"hash-of-wisdom/internal/pow/solver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("🔥 Hash of Wisdom - CPU Burner Challenge 🔥")
|
||||||
|
fmt.Printf("Available CPU cores: %d\n", runtime.NumCPU())
|
||||||
|
|
||||||
|
// Create config with brutal difficulty
|
||||||
|
config, err := challenge.NewConfig(
|
||||||
|
challenge.WithDefaultDifficulty(30), // This will require ~1 BILLION hash attempts on average
|
||||||
|
challenge.WithMinDifficulty(25),
|
||||||
|
challenge.WithMaxDifficulty(32),
|
||||||
|
challenge.WithRandomBytes(8),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the brutal challenge
|
||||||
|
generator := challenge.NewGenerator(config)
|
||||||
|
ch, err := generator.GenerateChallenge()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate challenge: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n💀 Generated BRUTAL challenge with difficulty %d bits\n", ch.Difficulty)
|
||||||
|
fmt.Printf("Expected attempts: ~%.0f\n", float64(uint64(1)<<uint(ch.Difficulty)))
|
||||||
|
fmt.Printf("Random: %x\n", ch.Random)
|
||||||
|
fmt.Printf("Resource: %s\n", ch.Resource)
|
||||||
|
|
||||||
|
// Create solver with all available CPU cores
|
||||||
|
s := solver.NewSolver(solver.WithWorkers(runtime.NumCPU()))
|
||||||
|
|
||||||
|
fmt.Printf("\n🚀 Starting solve with %d workers...\n", runtime.NumCPU())
|
||||||
|
fmt.Println("Press Ctrl+C to cancel if it takes too long!")
|
||||||
|
|
||||||
|
// Start timing
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Set a longer timeout for the brutal difficulty (15 minutes max)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Track progress with goroutine
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
fmt.Printf("⏰ Still working... elapsed: %v\n", elapsed.Truncate(time.Second))
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// SOLVE IT!
|
||||||
|
solution, err := s.Solve(ctx, ch)
|
||||||
|
done <- true
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == context.DeadlineExceeded {
|
||||||
|
fmt.Printf("\n⏰ Timeout after %v - difficulty %d was too brutal!\n", elapsed, ch.Difficulty)
|
||||||
|
fmt.Println("Try reducing difficulty in the code if you want to see a solution.")
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Solve failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUCCESS!
|
||||||
|
fmt.Printf("\n🎉 SOLVED! Found nonce: %d\n", solution.Nonce)
|
||||||
|
fmt.Printf("⚡ Time taken: %v\n", elapsed)
|
||||||
|
fmt.Printf("🔥 Hash rate: %.2f attempts/sec\n", float64(solution.Nonce)/elapsed.Seconds())
|
||||||
|
|
||||||
|
// Verify the solution
|
||||||
|
if solution.Verify() {
|
||||||
|
fmt.Printf("✅ Solution verified successfully!\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("❌ Solution verification failed!\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the winning hash
|
||||||
|
solutionStr := ch.BuildSolutionString(solution.Nonce)
|
||||||
|
fmt.Printf("🏆 Winning solution string: %s\n", solutionStr)
|
||||||
|
|
||||||
|
// Show CPU usage info
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
fmt.Printf("💾 Memory used: %d KB\n", m.Alloc/1024)
|
||||||
|
|
||||||
|
fmt.Printf("\n🔥 Your CPU should be nice and toasty now! 🔥\n")
|
||||||
|
}
|
||||||
|
|
@ -3,40 +3,40 @@
|
||||||
## Phase 1: Proof of Work Package Implementation
|
## Phase 1: Proof of Work Package Implementation
|
||||||
**Goal**: Create standalone, testable PoW package with HMAC-signed stateless challenges
|
**Goal**: Create standalone, testable PoW package with HMAC-signed stateless challenges
|
||||||
|
|
||||||
- [ ] **Project Setup**
|
- [X] **Project Setup**
|
||||||
- [X] Initialize Go module and basic project structure
|
- [X] Initialize Go module and basic project structure
|
||||||
- [X] Create PoW challenge structure and types
|
- [X] Create PoW challenge structure and types
|
||||||
- [ ] Set up testing framework and utilities
|
- [X] Set up testing framework and utilities
|
||||||
|
|
||||||
- [ ] **Challenge Generation & HMAC Security**
|
- [X] **Challenge Generation & HMAC Security**
|
||||||
- [ ] Implement HMAC-signed challenge generation (stateless)
|
- [X] Implement HMAC-signed challenge generation (stateless)
|
||||||
- [ ] Create challenge authenticity verification
|
- [X] Create challenge authenticity verification
|
||||||
- [ ] Add timestamp validation for replay protection (5 minutes TTL)
|
- [X] Add timestamp validation for replay protection (5 minutes TTL)
|
||||||
- [ ] Implement canonical challenge field ordering for HMAC
|
- [X] Implement canonical challenge field ordering for HMAC
|
||||||
- [ ] Add Base64URL encoding for HMAC signatures
|
- [X] Add Base64URL encoding for HMAC signatures (JSON handles this)
|
||||||
- [ ] Implement challenge string construction (`quotes:timestamp:difficulty:random`)
|
- [X] Implement challenge string construction (`quotes:timestamp:difficulty:random`)
|
||||||
|
|
||||||
- [ ] **PoW Algorithm Implementation**
|
- [X] **PoW Algorithm Implementation**
|
||||||
- [ ] Implement SHA-256 based PoW solution algorithm
|
- [X] Implement SHA-256 based PoW solution algorithm
|
||||||
- [ ] Implement leading zero bit counting for difficulty
|
- [X] Implement leading zero bit counting for difficulty
|
||||||
- [ ] Create nonce iteration and solution finding
|
- [X] Create nonce iteration and solution finding
|
||||||
- [ ] Add difficulty scaling (3-10 bits range)
|
- [X] Add difficulty scaling (3-10 bits range)
|
||||||
- [ ] Create challenge string format: `quotes:timestamp:difficulty:random:nonce`
|
- [X] Create challenge string format: `quotes:timestamp:difficulty:random:nonce`
|
||||||
- [ ] Implement hash verification for submitted solutions
|
- [X] Implement hash verification for submitted solutions
|
||||||
|
|
||||||
- [ ] **Verification & Validation**
|
- [X] **Verification & Validation**
|
||||||
- [ ] Create challenge verification logic with HMAC validation
|
- [X] Create challenge verification logic with HMAC validation
|
||||||
- [ ] Add solution validation against original challenge
|
- [X] Add solution validation against original challenge
|
||||||
- [ ] Test HMAC tamper detection and validation
|
- [X] Test HMAC tamper detection and validation
|
||||||
- [ ] Add difficulty adjustment mechanisms
|
- [X] Add difficulty adjustment mechanisms (config-based)
|
||||||
|
|
||||||
- [ ] **Testing & Performance**
|
- [X] **Testing & Performance**
|
||||||
- [ ] Unit tests for challenge generation and verification
|
- [X] Unit tests for challenge generation and verification
|
||||||
- [ ] Unit tests for HMAC signing and validation
|
- [X] Unit tests for HMAC signing and validation
|
||||||
- [ ] Unit tests for PoW solution finding and verification
|
- [X] Unit tests for PoW solution finding and verification
|
||||||
- [ ] Benchmark tests for different difficulty levels
|
- [X] Benchmark tests for different difficulty levels
|
||||||
- [ ] Test edge cases (expired challenges, invalid HMAC, wrong difficulty)
|
- [X] Test edge cases (expired challenges, invalid HMAC, wrong difficulty)
|
||||||
- [ ] Performance tests for concurrent challenge operations
|
- [X] Performance tests for concurrent challenge operations
|
||||||
|
|
||||||
## Phase 2: Basic Server Architecture
|
## Phase 2: Basic Server Architecture
|
||||||
- [ ] Set up dependency injection framework (wire/dig)
|
- [ ] Set up dependency injection framework (wire/dig)
|
||||||
|
|
|
||||||
|
|
@ -97,22 +97,20 @@ All protocol messages use a binary format with the following structure:
|
||||||
- **Format**:
|
- **Format**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "challenge_unique_id",
|
|
||||||
"timestamp": 1640995200,
|
"timestamp": 1640995200,
|
||||||
"difficulty": 4,
|
"difficulty": 4,
|
||||||
"resource": "192.168.1.100:8080",
|
"resource": "quotes",
|
||||||
"random": "a1b2c3d4e5f6",
|
"random": "a1b2c3d4e5f6",
|
||||||
"hmac": "base64url_encoded_signature"
|
"hmac": "base64url_encoded_signature"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Field Descriptions**:
|
**Field Descriptions**:
|
||||||
- **id**: Unique identifier for this challenge
|
|
||||||
- **timestamp**: Unix timestamp when challenge was created
|
- **timestamp**: Unix timestamp when challenge was created
|
||||||
- **difficulty**: Number of leading zero bits required in solution hash
|
- **difficulty**: Number of leading zero bits required in solution hash
|
||||||
- **resource**: Server resource identifier (typically IP:port)
|
- **resource**: Server resource identifier (e.g., "quotes")
|
||||||
- **random**: Random hex string for challenge uniqueness
|
- **random**: Random hex string for challenge uniqueness
|
||||||
- **hmac**: HMAC-SHA256 signature of canonical challenge fields
|
- **hmac**: HMAC-SHA256 signature of canonical challenge fields (also serves as unique identifier)
|
||||||
|
|
||||||
**Security Notes**:
|
**Security Notes**:
|
||||||
- Server is **stateless**: no need to store challenges locally
|
- Server is **stateless**: no need to store challenges locally
|
||||||
|
|
@ -126,10 +124,9 @@ All protocol messages use a binary format with the following structure:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"challenge": {
|
"challenge": {
|
||||||
"id": "challenge_unique_id",
|
|
||||||
"timestamp": 1640995200,
|
"timestamp": 1640995200,
|
||||||
"difficulty": 4,
|
"difficulty": 4,
|
||||||
"resource": "192.168.1.100:8080",
|
"resource": "quotes",
|
||||||
"random": "a1b2c3d4e5f6",
|
"random": "a1b2c3d4e5f6",
|
||||||
"hmac": "base64url_encoded_signature"
|
"hmac": "base64url_encoded_signature"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -1,3 +1,5 @@
|
||||||
module word-of-wisdom
|
module hash-of-wisdom
|
||||||
|
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.10.0 // indirect
|
||||||
|
|
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
147
internal/pow/challenge/config.go
Normal file
147
internal/pow/challenge/config.go
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds configuration for the PoW system
|
||||||
|
type Config struct {
|
||||||
|
DefaultDifficulty int // Default difficulty in bits (e.g., 4)
|
||||||
|
MaxDifficulty int // Maximum allowed difficulty (e.g., 10)
|
||||||
|
MinDifficulty int // Minimum allowed difficulty (e.g., 3)
|
||||||
|
ChallengeTTL time.Duration // Time-to-live for challenges (e.g., 5 minutes)
|
||||||
|
HMACSecret []byte // Secret key for HMAC signing
|
||||||
|
Resource string // Resource identifier (e.g., "quotes")
|
||||||
|
RandomBytes int // Number of random bytes in challenge (e.g., 6)
|
||||||
|
LoadAdjustmentBits int // Extra difficulty bits when server under load
|
||||||
|
FailurePenaltyBits int // Extra difficulty bits per failure group
|
||||||
|
MaxFailurePenaltyBits int // Maximum failure penalty bits
|
||||||
|
}
|
||||||
|
|
||||||
|
// configOption represents a functional option for Config
|
||||||
|
type configOption func(*Config)
|
||||||
|
|
||||||
|
// WithDefaultDifficulty sets the default difficulty
|
||||||
|
func WithDefaultDifficulty(difficulty int) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.DefaultDifficulty = difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxDifficulty sets the maximum difficulty
|
||||||
|
func WithMaxDifficulty(difficulty int) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.MaxDifficulty = difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMinDifficulty sets the minimum difficulty
|
||||||
|
func WithMinDifficulty(difficulty int) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.MinDifficulty = difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithChallengeTTL sets the challenge time-to-live
|
||||||
|
func WithChallengeTTL(ttl time.Duration) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.ChallengeTTL = ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHMACSecret sets the HMAC secret
|
||||||
|
func WithHMACSecret(secret []byte) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.HMACSecret = secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithResource sets the resource identifier
|
||||||
|
func WithResource(resource string) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.Resource = resource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRandomBytes sets the number of random bytes
|
||||||
|
func WithRandomBytes(bytes int) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.RandomBytes = bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLoadAdjustmentBits sets the load adjustment bits
|
||||||
|
func WithLoadAdjustmentBits(bits int) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.LoadAdjustmentBits = bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFailurePenaltyBits sets the failure penalty bits
|
||||||
|
func WithFailurePenaltyBits(bits int) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.FailurePenaltyBits = bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxFailurePenaltyBits sets the maximum failure penalty bits
|
||||||
|
func WithMaxFailurePenaltyBits(bits int) configOption {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.MaxFailurePenaltyBits = bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig creates and validates a new PoW configuration with defaults
|
||||||
|
func NewConfig(opts ...configOption) (*Config, error) {
|
||||||
|
// Generate default HMAC secret
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
rand.Read(secret)
|
||||||
|
|
||||||
|
// Create config with defaults
|
||||||
|
config := &Config{
|
||||||
|
DefaultDifficulty: 4,
|
||||||
|
MaxDifficulty: 10,
|
||||||
|
MinDifficulty: 3,
|
||||||
|
ChallengeTTL: 5 * time.Minute,
|
||||||
|
HMACSecret: secret,
|
||||||
|
Resource: "quotes",
|
||||||
|
RandomBytes: 6,
|
||||||
|
LoadAdjustmentBits: 1,
|
||||||
|
FailurePenaltyBits: 2,
|
||||||
|
MaxFailurePenaltyBits: 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply options
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
if config.RandomBytes <= 0 || config.RandomBytes > 32 {
|
||||||
|
return nil, fmt.Errorf("%w: random bytes must be between 1-32, got %d", ErrInvalidConfig, config.RandomBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MinDifficulty < 1 {
|
||||||
|
return nil, fmt.Errorf("%w: minimum difficulty must be >= 1, got %d", ErrInvalidConfig, config.MinDifficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MaxDifficulty < config.MinDifficulty {
|
||||||
|
return nil, fmt.Errorf("%w: maximum difficulty (%d) must be >= minimum difficulty (%d)", ErrInvalidConfig, config.MaxDifficulty, config.MinDifficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DefaultDifficulty < config.MinDifficulty || config.DefaultDifficulty > config.MaxDifficulty {
|
||||||
|
return nil, fmt.Errorf("%w: default difficulty (%d) must be between min (%d) and max (%d)", ErrInvalidConfig, config.DefaultDifficulty, config.MinDifficulty, config.MaxDifficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.HMACSecret) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: HMAC secret cannot be empty", ErrInvalidConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ChallengeTTL <= 0 {
|
||||||
|
return nil, fmt.Errorf("%w: challenge TTL must be positive, got %v", ErrInvalidConfig, config.ChallengeTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
62
internal/pow/challenge/generator.go
Normal file
62
internal/pow/challenge/generator.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generator handles creation of PoW challenges with HMAC signing
|
||||||
|
type Generator struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateOption represents a functional option for challenge generation
|
||||||
|
type generateOption func(*Challenge)
|
||||||
|
|
||||||
|
// WithDifficulty sets custom difficulty for the challenge
|
||||||
|
func WithDifficulty(difficulty int) generateOption {
|
||||||
|
return func(c *Challenge) {
|
||||||
|
c.Difficulty = difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGenerator creates a new challenge generator
|
||||||
|
func NewGenerator(config *Config) *Generator {
|
||||||
|
return &Generator{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateChallenge creates a new HMAC-signed PoW challenge
|
||||||
|
func (g *Generator) GenerateChallenge(opts ...generateOption) (*Challenge, error) {
|
||||||
|
// Create challenge with defaults (no random data yet)
|
||||||
|
challenge := &Challenge{
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Difficulty: g.config.DefaultDifficulty,
|
||||||
|
Resource: g.config.Resource,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply options
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(challenge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate difficulty bounds
|
||||||
|
if challenge.Difficulty < g.config.MinDifficulty || challenge.Difficulty > g.config.MaxDifficulty {
|
||||||
|
return nil, ErrInvalidDifficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate cryptographic random data
|
||||||
|
challenge.Random = g.generateRandom(g.config.RandomBytes)
|
||||||
|
|
||||||
|
// Sign the challenge with HMAC
|
||||||
|
challenge.Sign(g.config.HMACSecret)
|
||||||
|
return challenge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandom creates cryptographic random bytes
|
||||||
|
func (g *Generator) generateRandom(length int) []byte {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
internal/pow/challenge/test_utils.go
Normal file
63
internal/pow/challenge/test_utils.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestConfig returns a configuration suitable for testing
|
||||||
|
func TestConfig() *Config {
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
rand.Read(secret)
|
||||||
|
|
||||||
|
config, err := NewConfig(
|
||||||
|
WithHMACSecret(secret),
|
||||||
|
WithDefaultDifficulty(4),
|
||||||
|
WithRandomBytes(6),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to create test config: " + err.Error())
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestChallenge returns a valid challenge for testing
|
||||||
|
func TestChallenge() *Challenge {
|
||||||
|
return &Challenge{
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Difficulty: 4,
|
||||||
|
Resource: "quotes",
|
||||||
|
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||||
|
HMAC: nil, // To be filled by generator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSolution returns a valid solution for testing
|
||||||
|
func TestSolution() *Solution {
|
||||||
|
return &Solution{
|
||||||
|
Challenge: *TestChallenge(),
|
||||||
|
Nonce: 42,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomHex generates a random hex string of given length
|
||||||
|
func RandomHex(length int) string {
|
||||||
|
bytes := make([]byte, length/2)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return hex.EncodeToString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiredChallenge returns a challenge that has expired
|
||||||
|
func ExpiredChallenge() *Challenge {
|
||||||
|
challenge := TestChallenge()
|
||||||
|
challenge.Timestamp = time.Now().Add(-10 * time.Minute).Unix()
|
||||||
|
return challenge
|
||||||
|
}
|
||||||
|
|
||||||
|
// FutureChallenge returns a challenge with future timestamp
|
||||||
|
func FutureChallenge() *Challenge {
|
||||||
|
challenge := TestChallenge()
|
||||||
|
challenge.Timestamp = time.Now().Add(10 * time.Minute).Unix()
|
||||||
|
return challenge
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package pow
|
package challenge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
28
internal/pow/challenge/verifier.go
Normal file
28
internal/pow/challenge/verifier.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package challenge
|
||||||
|
|
||||||
|
// Verifier handles validation of PoW challenges
|
||||||
|
type Verifier struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVerifier creates a new challenge verifier
|
||||||
|
func NewVerifier(config *Config) *Verifier {
|
||||||
|
return &Verifier{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyChallenge validates challenge authenticity and expiration
|
||||||
|
func (v *Verifier) VerifyChallenge(challenge *Challenge) error {
|
||||||
|
// Check expiration first (cheap operation)
|
||||||
|
if challenge.IsExpired(v.config.ChallengeTTL) {
|
||||||
|
return ErrExpiredChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check HMAC signature (expensive operation)
|
||||||
|
if err := challenge.VerifyHMAC(v.config.HMACSecret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
94
internal/pow/solver/solver.go
Normal file
94
internal/pow/solver/solver.go
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
package solver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"hash-of-wisdom/internal/pow/challenge"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Solver handles parallel PoW solving with configurable parallelism
|
||||||
|
type Solver struct {
|
||||||
|
workers int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SolverOption represents functional options for solver configuration
|
||||||
|
type solverOption func(*Solver)
|
||||||
|
|
||||||
|
// WithWorkers sets the number of parallel workers
|
||||||
|
func WithWorkers(workers int) solverOption {
|
||||||
|
return func(s *Solver) {
|
||||||
|
s.workers = workers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSolver creates a new parallel PoW solver
|
||||||
|
func NewSolver(options ...solverOption) *Solver {
|
||||||
|
solver := &Solver{
|
||||||
|
workers: runtime.NumCPU(), // Default to CPU count
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(solver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return solver
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solve attempts to find a valid nonce for the given challenge
|
||||||
|
func (s *Solver) Solve(ctx context.Context, ch *challenge.Challenge) (*challenge.Solution, error) {
|
||||||
|
// Create cancellable context for workers
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Channel to receive solution from any worker
|
||||||
|
solutionCh := make(chan *challenge.Solution, 1)
|
||||||
|
|
||||||
|
// Shared nonce counter for work distribution
|
||||||
|
var nonceCounter atomic.Uint64
|
||||||
|
|
||||||
|
// Start parallel workers
|
||||||
|
for i := range s.workers {
|
||||||
|
go func(workerID int) {
|
||||||
|
s.worker(ctx, ch, &nonceCounter, solutionCh)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for either solution or context cancellation
|
||||||
|
select {
|
||||||
|
case solution := <-solutionCh:
|
||||||
|
return solution, nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// worker performs PoW computation in parallel
|
||||||
|
func (s *Solver) worker(ctx context.Context, ch *challenge.Challenge, nonceCounter *atomic.Uint64, solutionCh chan<- *challenge.Solution) {
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Get next nonce to try
|
||||||
|
nonce := nonceCounter.Add(1) - 1
|
||||||
|
|
||||||
|
// Try this nonce
|
||||||
|
if ch.VerifySolution(nonce) {
|
||||||
|
// Found solution! Send it back
|
||||||
|
solution := &challenge.Solution{
|
||||||
|
Challenge: *ch,
|
||||||
|
Nonce: nonce,
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case solutionCh <- solution:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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