- 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.
154 lines
4.6 KiB
Go
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
|
|
}
|