niko_trust/internal/pow/vectors_test.go
Niko Marmeladkov 20cc52c3a5 feat: network layer — PoW, checkpoint chain, gossip, light node, WS, delegation, rotation, BFT
- BLAKE3 keyed proof-of-work on object storage and auth challenges,
  with frozen vectors cross-checked against an independent Python
  reference implementing the single-block hash it needs.
- Sparse Merkle trie over object IDs: order-independent roots,
  inclusion and absence proofs (internal/smt).
- Signed checkpoint chain per relay: transport key amendment to INV-1,
  /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe
  epoch continuity (internal/checkpoint).
- Head gossip with TOFU pinning and equivocation detection; light node
  (cmd/lightnode) that stores no history: quorum of pinned relays,
  every served object proven against the agreed root, LRU disk cache.
- WebSocket streaming on relay and light node (coder/websocket):
  scoped channels mirroring REST, raw envelopes verified client-side;
  light node marks streamed objects unproven until checkpoint coverage.
- Protocol v1 additions: DelegationClaim tag 0x07 with deterministic
  chain resolution in verify.Graph, KeyRotationRequest/Confirm tags
  0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge;
  spec sections, frozen vectors appended byte-identically, Python
  reference extended.
- Optional permissioned BFT finality over gossip (internal/bft):
  prevote/precommit with quorum certificates verifiable offline.
- Quick wins: Policy.TrustedIssuers, per-type stored metrics,
  batch fetch, lexicographic lists with stable cursor pagination.
- Security review of the network layer (docs/SECURITY-REVIEW.md) with
  findings F-01..F-09; hub send/close race and unstable pagination
  fixed under review.

12 packages green, vet/gofmt clean, protocol fuzzing stable.
2026-08-25 20:38:40 +03:00

133 lines
3.9 KiB
Go

package pow_test
import (
"encoding/hex"
"encoding/json"
"os"
"testing"
"git.n1ko.dev/Niko/niko_trust/internal/pow"
)
const powVectorsPath = "../../testdata/vectors/pow_vectors.json"
type powVectorFile struct {
Domain string `json:"domain"`
HashSpec string `json:"hash_spec"`
MaxDifficulty int `json:"max_difficulty"`
Vectors []struct {
Name string `json:"name"`
KeyHex string `json:"key_hex"`
TargetHex string `json:"target_hex"`
Difficulty int `json:"difficulty"`
Counter uint32 `json:"counter"`
SumHex string `json:"sum_hex"`
LeadingZeroBits int `json:"leading_zero_bits"`
} `json:"vectors"`
Rejects []struct {
Name string `json:"name"`
KeyHex string `json:"key_hex"`
TargetHex string `json:"target_hex"`
Difficulty int `json:"difficulty"`
Counter uint32 `json:"counter"`
Reason string `json:"reason"`
} `json:"rejects"`
ConfigRejects []struct {
Name string `json:"name"`
Difficulty int `json:"difficulty"`
Reason string `json:"reason"`
} `json:"config_rejects"`
}
func loadPoWVectors(t *testing.T) *powVectorFile {
t.Helper()
b, err := os.ReadFile(powVectorsPath)
if err != nil {
t.Fatalf("read vectors: %v", err)
}
var vf powVectorFile
if err := json.Unmarshal(b, &vf); err != nil {
t.Fatalf("parse vectors: %v", err)
}
return &vf
}
// TestPoWVectorsAgainstReference reproduces every frozen vector produced by
// the independent Python reference. A disagreement here means one of the two
// implementations misread docs/POW.md.
func TestPoWVectorsAgainstReference(t *testing.T) {
vf := loadPoWVectors(t)
if vf.Domain != pow.Domain {
t.Fatalf("domain drift: file %q, code %q", vf.Domain, pow.Domain)
}
if vf.MaxDifficulty != pow.MaxDifficulty {
t.Fatalf("max difficulty drift: file %d, code %d", vf.MaxDifficulty, pow.MaxDifficulty)
}
for _, v := range vf.Vectors {
t.Run(v.Name, func(t *testing.T) {
key, err := pow.ParseKey(v.KeyHex)
if err != nil {
t.Fatal(err)
}
var target [pow.TargetSize]byte
tb, err := hex.DecodeString(v.TargetHex)
if err != nil || len(tb) != pow.TargetSize {
t.Fatalf("bad target hex: %v", err)
}
copy(target[:], tb)
got := pow.Sum(key, target, v.Counter)
want, _ := hex.DecodeString(v.SumHex)
if hex.EncodeToString(got[:]) != v.SumHex {
t.Fatalf("sum mismatch:\n got %x\nwant %x", got[:], want)
}
if n := pow.LeadingZeroBits(got); n < v.Difficulty {
t.Fatalf("vector claims validity at difficulty %d but hash has %d bits", v.Difficulty, n)
}
if got2 := pow.LeadingZeroBits(got); got2 != v.LeadingZeroBits {
t.Fatalf("leading zero bits: got %d, want %d", got2, v.LeadingZeroBits)
}
if !pow.Verify(key, target, v.Difficulty, v.Counter) {
t.Fatal("Verify rejected a reference-valid solution")
}
// The recorded counter must be the smallest solution: nothing
// below it may verify. Solving again and comparing enforces both
// minimality and solver determinism.
solved, ok := pow.Solve(key, target, v.Difficulty)
if !ok {
t.Fatal("Solve exhausted counter space on a solvable vector")
}
if solved != v.Counter {
t.Fatalf("Solve returned %d, reference minimal counter is %d", solved, v.Counter)
}
})
}
}
func TestPoWRejectsAgainstReference(t *testing.T) {
vf := loadPoWVectors(t)
for _, r := range vf.Rejects {
t.Run(r.Name, func(t *testing.T) {
key, err := pow.ParseKey(r.KeyHex)
if err != nil {
t.Fatal(err)
}
var target [pow.TargetSize]byte
tb, _ := hex.DecodeString(r.TargetHex)
copy(target[:], tb)
if pow.Verify(key, target, r.Difficulty, r.Counter) {
t.Fatalf("reject %q verifies", r.Name)
}
})
}
for _, c := range vf.ConfigRejects {
t.Run(c.Name, func(t *testing.T) {
if c.Difficulty > pow.MaxDifficulty {
return // configuration layer must clamp or refuse; unit scope ends here
}
})
}
}