- 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.
126 lines
3.2 KiB
Go
126 lines
3.2 KiB
Go
package checkpoint_test
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"testing"
|
|
|
|
"git.n1ko.dev/Niko/niko_trust/internal/checkpoint"
|
|
)
|
|
|
|
func testCkpt() *checkpoint.Checkpoint {
|
|
var root, prev [32]byte
|
|
rand.Read(root[:])
|
|
rand.Read(prev[:])
|
|
return &checkpoint.Checkpoint{
|
|
Epoch: 3,
|
|
Size: 42,
|
|
Root: root,
|
|
Prev: prev,
|
|
CreatedAt: 1_700_000_000,
|
|
}
|
|
}
|
|
|
|
func TestEncodeDecodeRoundTrip(t *testing.T) {
|
|
c := testCkpt()
|
|
b, err := c.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := checkpoint.Decode(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Epoch != c.Epoch || got.Size != c.Size || got.CreatedAt != c.CreatedAt {
|
|
t.Fatal("field mismatch")
|
|
}
|
|
if got.Root != c.Root || got.Prev != c.Prev {
|
|
t.Fatal("hash mismatch")
|
|
}
|
|
again, _ := c.Encode()
|
|
if !bytes.Equal(b, again) {
|
|
t.Fatal("encoding nondeterministic")
|
|
}
|
|
}
|
|
|
|
func TestGenesisPrevIsZeros(t *testing.T) {
|
|
c := &checkpoint.Checkpoint{Epoch: 1, Size: 0, CreatedAt: 1_700_000_000}
|
|
if c.Prev != ([32]byte{}) {
|
|
t.Fatal("genesis prev must be zeros")
|
|
}
|
|
}
|
|
|
|
func TestTamperedBytesRejected(t *testing.T) {
|
|
c := testCkpt()
|
|
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
|
b, err := c.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sig, err := checkpoint.Sign(priv, b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := checkpoint.Verify(pub, b, sig); err != nil {
|
|
t.Fatal("honest signature rejected")
|
|
}
|
|
for i := len(checkpoint.Magic); i < len(b); i++ {
|
|
bad := append([]byte(nil), b...)
|
|
bad[i] ^= 0x01
|
|
if checkpoint.Verify(pub, bad, sig) == nil {
|
|
t.Fatalf("tampered byte %d verified", i)
|
|
}
|
|
// A mutated body is also a different checkpoint entirely.
|
|
if _, err := checkpoint.Decode(bad); err == nil && i >= len(b)-1 {
|
|
// Mutating created_at's last byte may still decode if it stays
|
|
// in range; signature binding covers that case above.
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecodeRejectsGarbage(t *testing.T) {
|
|
cases := [][]byte{
|
|
nil,
|
|
{},
|
|
bytes.Repeat([]byte{0x00}, 21),
|
|
append(append([]byte(nil), checkpoint.Magic...), 0x02), // unknown version
|
|
checkpoint.Magic[:10],
|
|
}
|
|
for i, b := range cases {
|
|
if _, err := checkpoint.Decode(b); err == nil {
|
|
t.Errorf("case %d decoded garbage", i)
|
|
}
|
|
}
|
|
// Trailing byte after the last field.
|
|
c := testCkpt()
|
|
b, _ := c.Encode()
|
|
if _, err := checkpoint.Decode(append(b, 0x00)); err == nil {
|
|
t.Error("trailing byte accepted")
|
|
}
|
|
}
|
|
|
|
func TestChainLinkage(t *testing.T) {
|
|
c1 := &checkpoint.Checkpoint{Epoch: 1, Size: 5, CreatedAt: 1_700_000_000}
|
|
b1, _ := c1.Encode()
|
|
id1 := checkpoint.ID(b1)
|
|
|
|
c2 := &checkpoint.Checkpoint{Epoch: 2, Size: 6, Root: id1, Prev: id1, CreatedAt: 1_700_000_060}
|
|
if c2.Prev != id1 {
|
|
t.Fatal("prev must equal previous checkpoint's ID")
|
|
}
|
|
}
|
|
|
|
func TestSignKeySizeEnforced(t *testing.T) {
|
|
b, _ := testCkpt().Encode()
|
|
if _, err := checkpoint.Sign(ed25519.PrivateKey(make([]byte, 10)), b); err == nil {
|
|
t.Fatal("short private key accepted")
|
|
}
|
|
if err := checkpoint.Verify(ed25519.PublicKey(make([]byte, 10)), b, make([]byte, 64)); err == nil {
|
|
t.Fatal("short public key accepted")
|
|
}
|
|
if err := checkpoint.Verify(ed25519.PublicKey(make([]byte, 32)), b, make([]byte, 63)); err == nil {
|
|
t.Fatal("short signature accepted")
|
|
}
|
|
}
|