Compare commits
11 commits
750223b2cc
...
764ac2e2a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
764ac2e2a7 | ||
|
|
439e0775f5 | ||
|
|
fdef33fecc | ||
|
|
51c586362b | ||
|
|
bfe56e5025 | ||
|
|
d21c650508 | ||
|
|
d04fcbb245 | ||
|
|
2e0bbd9485 | ||
|
|
fb0bfd18c9 | ||
|
|
925de15ce1 | ||
|
|
2a4060bc8a |
|
|
@ -3,32 +3,32 @@
|
||||||
## 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**
|
||||||
- [ ] Initialize Go module and basic project structure
|
- [X] Initialize Go module and basic project structure
|
||||||
- [ ] 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**
|
- [ ] **Testing & Performance**
|
||||||
- [ ] Unit tests for challenge generation and verification
|
- [ ] Unit tests for challenge generation and verification
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package pow
|
package challenge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
|
@ -11,17 +11,15 @@ func TestConfig() *Config {
|
||||||
secret := make([]byte, 32)
|
secret := make([]byte, 32)
|
||||||
rand.Read(secret)
|
rand.Read(secret)
|
||||||
|
|
||||||
return &Config{
|
config, err := NewConfig(
|
||||||
DefaultDifficulty: 4,
|
WithHMACSecret(secret),
|
||||||
MaxDifficulty: 10,
|
WithDefaultDifficulty(4),
|
||||||
MinDifficulty: 3,
|
WithRandomBytes(6),
|
||||||
ChallengeTTL: 5 * time.Minute,
|
)
|
||||||
HMACSecret: secret,
|
if err != nil {
|
||||||
Resource: "quotes",
|
panic("Failed to create test config: " + err.Error())
|
||||||
LoadAdjustmentBits: 1,
|
|
||||||
FailurePenaltyBits: 2,
|
|
||||||
MaxFailurePenaltyBits: 6,
|
|
||||||
}
|
}
|
||||||
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestChallenge returns a valid challenge for testing
|
// TestChallenge returns a valid challenge for testing
|
||||||
|
|
@ -30,8 +28,8 @@ func TestChallenge() *Challenge {
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
Difficulty: 4,
|
Difficulty: 4,
|
||||||
Resource: "quotes",
|
Resource: "quotes",
|
||||||
Random: "a1b2c3d4e5f6",
|
Random: []byte{0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6},
|
||||||
HMAC: "", // To be filled by generator
|
HMAC: nil, // To be filled by generator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +37,7 @@ func TestChallenge() *Challenge {
|
||||||
func TestSolution() *Solution {
|
func TestSolution() *Solution {
|
||||||
return &Solution{
|
return &Solution{
|
||||||
Challenge: *TestChallenge(),
|
Challenge: *TestChallenge(),
|
||||||
Nonce: "42",
|
Nonce: 42,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
134
internal/pow/challenge/types.go
Normal file
134
internal/pow/challenge/types.go
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PoW package specific errors
|
||||||
|
var (
|
||||||
|
ErrInvalidHMAC = errors.New("challenge HMAC signature is invalid")
|
||||||
|
ErrExpiredChallenge = errors.New("challenge has expired")
|
||||||
|
ErrInvalidSolution = errors.New("proof of work solution is invalid")
|
||||||
|
ErrInvalidDifficulty = errors.New("difficulty level is invalid")
|
||||||
|
ErrMalformedChallenge = errors.New("challenge format is malformed")
|
||||||
|
ErrInvalidConfig = errors.New("configuration is invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Challenge represents a Proof of Work challenge with HMAC authentication
|
||||||
|
type Challenge struct {
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Difficulty int `json:"difficulty"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
Random []byte `json:"random"`
|
||||||
|
HMAC []byte `json:"hmac"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalBytes returns the canonical byte representation for HMAC signing
|
||||||
|
func (c *Challenge) CanonicalBytes() []byte {
|
||||||
|
// Use consistent field ordering: timestamp:difficulty:resource:random
|
||||||
|
buf := make([]byte, 0, 64)
|
||||||
|
buf = fmt.Appendf(buf, "%d:%d:%s:%x",
|
||||||
|
c.Timestamp,
|
||||||
|
c.Difficulty,
|
||||||
|
c.Resource,
|
||||||
|
c.Random,
|
||||||
|
)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildSolutionString creates the string used for PoW computation
|
||||||
|
func (c *Challenge) BuildSolutionString(nonce uint64) []byte {
|
||||||
|
// Format: resource:timestamp:difficulty:random:nonce
|
||||||
|
buf := make([]byte, 0, 64)
|
||||||
|
buf = fmt.Appendf(buf, "%s:%d:%d:%x:%d",
|
||||||
|
c.Resource,
|
||||||
|
c.Timestamp,
|
||||||
|
c.Difficulty,
|
||||||
|
c.Random,
|
||||||
|
nonce,
|
||||||
|
)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySolution verifies if a solution is valid for this challenge
|
||||||
|
func (c *Challenge) VerifySolution(nonce uint64) bool {
|
||||||
|
// Build solution string
|
||||||
|
solutionStr := c.BuildSolutionString(nonce)
|
||||||
|
|
||||||
|
// Compute SHA-256 hash
|
||||||
|
hash := sha256.Sum256(solutionStr)
|
||||||
|
|
||||||
|
// Check if hash has required leading zero bits
|
||||||
|
return hasLeadingZeroBits(hash[:], c.Difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasLeadingZeroBits checks if hash has the required number of leading zero bits
|
||||||
|
func hasLeadingZeroBits(hash []byte, difficulty int) bool {
|
||||||
|
full := difficulty >> 3 // number of whole zero bytes
|
||||||
|
rem := uint(difficulty & 7) // remaining leading zero bits
|
||||||
|
|
||||||
|
for i := range full {
|
||||||
|
if hash[i] != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rem == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
mask := byte(0xFF) << (8 - rem) // e.g., rem=3 => 11100000
|
||||||
|
return (hash[full] & mask) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExpired checks if challenge has exceeded TTL
|
||||||
|
func (c *Challenge) IsExpired(ttl time.Duration) bool {
|
||||||
|
challengeTime := time.Unix(c.Timestamp, 0)
|
||||||
|
return time.Since(challengeTime) > ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign creates and sets HMAC signature for the challenge
|
||||||
|
func (c *Challenge) Sign(secret []byte) {
|
||||||
|
// Create canonical bytes for signing (excluding HMAC field)
|
||||||
|
canonical := c.CanonicalBytes()
|
||||||
|
|
||||||
|
// Create HMAC
|
||||||
|
h := hmac.New(sha256.New, secret)
|
||||||
|
h.Write(canonical)
|
||||||
|
c.HMAC = h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyHMAC verifies the HMAC signature of the challenge
|
||||||
|
func (c *Challenge) VerifyHMAC(secret []byte) error {
|
||||||
|
if len(c.HMAC) == 0 {
|
||||||
|
return ErrMalformedChallenge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create canonical bytes (excluding HMAC)
|
||||||
|
canonical := c.CanonicalBytes()
|
||||||
|
|
||||||
|
// Compute expected HMAC
|
||||||
|
h := hmac.New(sha256.New, secret)
|
||||||
|
h.Write(canonical)
|
||||||
|
expectedSignature := h.Sum(nil)
|
||||||
|
|
||||||
|
// Compare signatures
|
||||||
|
if !hmac.Equal(c.HMAC, expectedSignature) {
|
||||||
|
return ErrInvalidHMAC
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solution represents a client's solution to a PoW challenge
|
||||||
|
type Solution struct {
|
||||||
|
Challenge Challenge `json:"challenge"`
|
||||||
|
Nonce uint64 `json:"nonce"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies if this solution is valid
|
||||||
|
func (s *Solution) Verify() bool {
|
||||||
|
return s.Challenge.VerifySolution(s.Nonce)
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package pow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"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 only after validation
|
|
||||||
challenge.Random = g.generateRandom(12) // 12 hex chars = 6 bytes
|
|
||||||
|
|
||||||
// Sign the challenge with HMAC
|
|
||||||
signature, err := g.signChallenge(challenge)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to sign challenge: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge.HMAC = signature
|
|
||||||
return challenge, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// signChallenge creates HMAC signature for the challenge
|
|
||||||
func (g *Generator) signChallenge(challenge *Challenge) (string, error) {
|
|
||||||
// Create canonical string for signing (excluding HMAC field)
|
|
||||||
canonical := g.canonicalChallengeString(challenge)
|
|
||||||
|
|
||||||
// Create HMAC
|
|
||||||
h := hmac.New(sha256.New, g.config.HMACSecret)
|
|
||||||
h.Write([]byte(canonical))
|
|
||||||
signature := h.Sum(nil)
|
|
||||||
|
|
||||||
// Return base64url encoded signature
|
|
||||||
return base64.RawURLEncoding.EncodeToString(signature), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// canonicalChallengeString creates a consistent string representation for HMAC
|
|
||||||
func (g *Generator) canonicalChallengeString(challenge *Challenge) string {
|
|
||||||
// Use consistent field ordering: timestamp:difficulty:resource:random
|
|
||||||
return fmt.Sprintf("%d:%d:%s:%s",
|
|
||||||
challenge.Timestamp,
|
|
||||||
challenge.Difficulty,
|
|
||||||
challenge.Resource,
|
|
||||||
challenge.Random,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateRandom creates cryptographic random hex string
|
|
||||||
func (g *Generator) generateRandom(length int) string {
|
|
||||||
bytes := make([]byte, length/2)
|
|
||||||
rand.Read(bytes)
|
|
||||||
return hex.EncodeToString(bytes)
|
|
||||||
}
|
|
||||||
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"
|
||||||
|
|
||||||
|
"word-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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
package pow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PoW package specific errors
|
|
||||||
var (
|
|
||||||
ErrInvalidHMAC = errors.New("challenge HMAC signature is invalid")
|
|
||||||
ErrExpiredChallenge = errors.New("challenge has expired")
|
|
||||||
ErrInvalidSolution = errors.New("proof of work solution is invalid")
|
|
||||||
ErrInvalidDifficulty = errors.New("difficulty level is invalid")
|
|
||||||
ErrMalformedChallenge = errors.New("challenge format is malformed")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Challenge represents a Proof of Work challenge with HMAC authentication
|
|
||||||
type Challenge struct {
|
|
||||||
Timestamp int64 `json:"timestamp"`
|
|
||||||
Difficulty int `json:"difficulty"`
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Random string `json:"random"`
|
|
||||||
HMAC string `json:"hmac"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solution represents a client's solution to a PoW challenge
|
|
||||||
type Solution struct {
|
|
||||||
Challenge Challenge `json:"challenge"`
|
|
||||||
Nonce string `json:"nonce"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChallengeRequest represents a request for a new challenge
|
|
||||||
type ChallengeRequest struct{}
|
|
||||||
|
|
||||||
// SolutionRequest represents a client's submission of a solved challenge
|
|
||||||
type SolutionRequest struct {
|
|
||||||
Challenge Challenge `json:"challenge"`
|
|
||||||
Nonce string `json:"nonce"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
LoadAdjustmentBits int // Extra difficulty bits when server under load
|
|
||||||
FailurePenaltyBits int // Extra difficulty bits per failure group
|
|
||||||
MaxFailurePenaltyBits int // Maximum failure penalty bits
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue