hash-of-wisdom/internal/pow/types.go

135 lines
3.5 KiB
Go

package pow
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)
}