- 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.
133 lines
3.9 KiB
Go
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
|
|
}
|
|
})
|
|
}
|
|
}
|