niko_trust/internal/checkpoint/checkpoint.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

154 lines
4.6 KiB
Go

// Package checkpoint defines the relay's signed commitment to its object
// set: the header chain that light nodes follow.
//
// A checkpoint is transport-layer infrastructure. It commits to the Merkle
// root of the objects a relay stores, to the previous checkpoint (forming a
// tamper-evident chain per relay), and to nothing else. It is never a trust
// statement: the relay key that signs it may not sign claims or approvals,
// and consumers derive no authority from it beyond "this relay asserts its
// log looks like this". This is the documented narrowing of INV-1
// (docs/TRUST-MODEL.md): the relay holds one key whose entire power is to
// describe its own storage.
//
// Canonical encoding, reusing TCE primitives so that two implementations
// cannot disagree about byte-exactness:
//
// MAGIC("trust.n1ko.dev/ckpt/1\0") || version=1 ||
// epoch || size || root(32) || prev(32) || created_at
//
// All integers are uvarints; timestamps follow the protocol's range rules.
package checkpoint
import (
"crypto/ed25519"
"crypto/sha256"
"errors"
"fmt"
"git.n1ko.dev/Niko/niko_trust/internal/tce"
)
// Magic frames a checkpoint and is domain separation at the outermost level:
// a checkpoint can never be reinterpreted as a TCE object or vice versa.
var Magic = []byte("trust.n1ko.dev/ckpt/1\x00")
// Version is the checkpoint format version.
const Version = 1
// Errors returned for malformed checkpoints.
var (
ErrBadMagic = errors.New("checkpoint: bad magic")
ErrVersion = errors.New("checkpoint: unsupported version")
ErrSignature = errors.New("checkpoint: signature does not verify")
)
// Checkpoint is one signed head of the relay's log.
type Checkpoint struct {
// Epoch counts checkpoints this relay has produced, starting at 1.
Epoch uint64
// Size is the number of distinct objects committed to by Root.
Size uint64
// Root is the sparse Merkle root over the stored object IDs.
Root [32]byte
// Prev is SHA-256 of the previous checkpoint's canonical bytes; zeros
// for epoch 1. Linking heads makes per-relay history tamper-evident
// without any global consensus.
Prev [32]byte
// CreatedAt is the relay's assertion of signing time.
CreatedAt uint64
}
// Encode returns the canonical bytes.
func (c *Checkpoint) Encode() ([]byte, error) {
if err := tce.ValidateTimestamp(c.CreatedAt, false); err != nil {
return nil, fmt.Errorf("checkpoint: created_at: %w", err)
}
e := tce.NewEncoder()
e.Uvarint(Version)
e.Uvarint(c.Epoch)
e.Uvarint(c.Size)
e.FixedBytes("root", c.Root[:], 32)
e.FixedBytes("prev", c.Prev[:], 32)
e.Uvarint(c.CreatedAt)
body, err := e.Bytes()
if err != nil {
return nil, err
}
out := make([]byte, 0, len(Magic)+len(body))
out = append(out, Magic...)
return append(out, body...), nil
}
// Decode strictly parses canonical bytes: exact magic, version, field sizes,
// timestamp range, no trailing bytes.
func Decode(b []byte) (*Checkpoint, error) {
if len(b) < len(Magic) || string(b[:len(Magic)]) != string(Magic) {
return nil, ErrBadMagic
}
d := tce.NewDecoder(b[len(Magic):])
version, err := d.Uvarint()
if err != nil {
return nil, err
}
if version != Version {
return nil, ErrVersion
}
var c Checkpoint
if c.Epoch, err = d.Uvarint(); err != nil {
return nil, err
}
if c.Size, err = d.Uvarint(); err != nil {
return nil, err
}
root, err := d.FixedBytes(32)
if err != nil {
return nil, err
}
copy(c.Root[:], root)
prev, err := d.FixedBytes(32)
if err != nil {
return nil, err
}
copy(c.Prev[:], prev)
if c.CreatedAt, err = d.Uvarint(); err != nil {
return nil, err
}
if err := tce.ValidateTimestamp(c.CreatedAt, false); err != nil {
return nil, fmt.Errorf("checkpoint: created_at: %w", err)
}
if err := d.End(); err != nil {
return nil, err
}
return &c, nil
}
// ID is the content address of a checkpoint: SHA-256 of its canonical
// bytes. It is also the value linked by the next checkpoint's Prev.
func ID(b []byte) [32]byte {
return sha256.Sum256(b)
}
// Sign returns the Ed25519 signature of the relay key over the canonical
// bytes. Only a relay's transport key ever produces this.
func Sign(key ed25519.PrivateKey, b []byte) ([]byte, error) {
if len(key) != ed25519.PrivateKeySize {
return nil, errors.New("checkpoint: bad relay key")
}
return ed25519.Sign(key, b), nil
}
// Verify checks a checkpoint's signature over its canonical bytes.
func Verify(key ed25519.PublicKey, b, sig []byte) error {
if len(key) != ed25519.PublicKeySize || len(sig) != ed25519.SignatureSize {
return ErrSignature
}
// ed25519.Verify panics on a mis-sized key, hence the length gate above.
if !ed25519.Verify(key, b, sig) {
return ErrSignature
}
return nil
}