Compare commits
10 commits
f9d4ee9017
...
764ac2e2a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
764ac2e2a7 | ||
|
|
439e0775f5 | ||
|
|
fdef33fecc | ||
|
|
51c586362b | ||
|
|
bfe56e5025 | ||
|
|
d21c650508 | ||
|
|
d04fcbb245 | ||
|
|
2e0bbd9485 | ||
|
|
fb0bfd18c9 | ||
|
|
925de15ce1 |
11
.mockery.yaml
Normal file
11
.mockery.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
with-expecter: true
|
||||
dir: "{{.InterfaceDir}}/mocks"
|
||||
mockname: "Mock{{.InterfaceName}}"
|
||||
outpkg: "{{.PackageName}}"
|
||||
filename: "mock_{{.InterfaceName | snakecase}}.go"
|
||||
packages:
|
||||
github.com/word-of-wisdom/internal/pow:
|
||||
interfaces:
|
||||
Generator:
|
||||
Verifier:
|
||||
Solver:
|
||||
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]
|
||||
|
|
@ -3,32 +3,32 @@
|
|||
## Phase 1: Proof of Work Package Implementation
|
||||
**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] Create PoW challenge structure and types
|
||||
- [ ] Set up testing framework and utilities
|
||||
- [X] Set up testing framework and utilities
|
||||
|
||||
- [ ] **Challenge Generation & HMAC Security**
|
||||
- [ ] Implement HMAC-signed challenge generation (stateless)
|
||||
- [ ] Create challenge authenticity verification
|
||||
- [ ] Add timestamp validation for replay protection (5 minutes TTL)
|
||||
- [ ] Implement canonical challenge field ordering for HMAC
|
||||
- [ ] Add Base64URL encoding for HMAC signatures
|
||||
- [ ] Implement challenge string construction (`quotes:timestamp:difficulty:random`)
|
||||
- [X] **Challenge Generation & HMAC Security**
|
||||
- [X] Implement HMAC-signed challenge generation (stateless)
|
||||
- [X] Create challenge authenticity verification
|
||||
- [X] Add timestamp validation for replay protection (5 minutes TTL)
|
||||
- [X] Implement canonical challenge field ordering for HMAC
|
||||
- [X] Add Base64URL encoding for HMAC signatures (JSON handles this)
|
||||
- [X] Implement challenge string construction (`quotes:timestamp:difficulty:random`)
|
||||
|
||||
- [ ] **PoW Algorithm Implementation**
|
||||
- [ ] Implement SHA-256 based PoW solution algorithm
|
||||
- [ ] Implement leading zero bit counting for difficulty
|
||||
- [ ] Create nonce iteration and solution finding
|
||||
- [ ] Add difficulty scaling (3-10 bits range)
|
||||
- [ ] Create challenge string format: `quotes:timestamp:difficulty:random:nonce`
|
||||
- [ ] Implement hash verification for submitted solutions
|
||||
- [X] **PoW Algorithm Implementation**
|
||||
- [X] Implement SHA-256 based PoW solution algorithm
|
||||
- [X] Implement leading zero bit counting for difficulty
|
||||
- [X] Create nonce iteration and solution finding
|
||||
- [X] Add difficulty scaling (3-10 bits range)
|
||||
- [X] Create challenge string format: `quotes:timestamp:difficulty:random:nonce`
|
||||
- [X] Implement hash verification for submitted solutions
|
||||
|
||||
- [ ] **Verification & Validation**
|
||||
- [ ] Create challenge verification logic with HMAC validation
|
||||
- [ ] Add solution validation against original challenge
|
||||
- [ ] Test HMAC tamper detection and validation
|
||||
- [ ] Add difficulty adjustment mechanisms
|
||||
- [X] **Verification & Validation**
|
||||
- [X] Create challenge verification logic with HMAC validation
|
||||
- [X] Add solution validation against original challenge
|
||||
- [X] Test HMAC tamper detection and validation
|
||||
- [X] Add difficulty adjustment mechanisms (config-based)
|
||||
|
||||
- [ ] **Testing & Performance**
|
||||
- [ ] Unit tests for challenge generation and verification
|
||||
|
|
|
|||
|
|
@ -97,22 +97,20 @@ All protocol messages use a binary format with the following structure:
|
|||
- **Format**:
|
||||
```json
|
||||
{
|
||||
"id": "challenge_unique_id",
|
||||
"timestamp": 1640995200,
|
||||
"difficulty": 4,
|
||||
"resource": "192.168.1.100:8080",
|
||||
"resource": "quotes",
|
||||
"random": "a1b2c3d4e5f6",
|
||||
"hmac": "base64url_encoded_signature"
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions**:
|
||||
- **id**: Unique identifier for this challenge
|
||||
- **timestamp**: Unix timestamp when challenge was created
|
||||
- **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
|
||||
- **hmac**: HMAC-SHA256 signature of canonical challenge fields
|
||||
- **hmac**: HMAC-SHA256 signature of canonical challenge fields (also serves as unique identifier)
|
||||
|
||||
**Security Notes**:
|
||||
- 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
|
||||
{
|
||||
"challenge": {
|
||||
"id": "challenge_unique_id",
|
||||
"timestamp": 1640995200,
|
||||
"difficulty": 4,
|
||||
"resource": "192.168.1.100:8080",
|
||||
"resource": "quotes",
|
||||
"random": "a1b2c3d4e5f6",
|
||||
"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
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 (
|
||||
"crypto/hmac"
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue