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

271 lines
6.8 KiB
Go

// Package smt implements a compressed sparse Merkle trie over fixed-size
// keys, committed to by a single root hash.
//
// Purpose. Two relays that store the same set of objects must compute the
// same root regardless of insertion order: the root is a function of the
// set, so comparing one 32-byte string detects divergence between mirrors
// instantly (docs/CHECKPOINT.md). The trie is grow-only — objects are never
// removed from the log even when revoked — which keeps every operation
// monotone and the proofs simple.
//
// Hash domain (SHA-256 throughout):
//
// leaf = H(0x00 || key)
// branch = H(0x01 || uvarint(prefixLen) || prefixBits || left || right)
// empty = H(0x02)
//
// The prefix is inside the branch hash so the trie's shape is committed to:
// without it, proofs could not be checked for canonicality. A branch exists
// only where both children exist and splits exactly at its keys' first
// differing bit, which makes the shape a function of the set alone.
//
// Proofs. An inclusion proof walks from the key's leaf to the root through
// sibling hashes. An absence proof witnesses the exact spot where the key
// would have lived: either a neighbouring leaf whose path agrees with the
// queried key up to that leaf's depth (variant A), or the branch whose
// prefix the queried key leaves mid-way (variant B). Both recombine with
// the queried key's own bits, so a proof for one key verifies for no other.
package smt
import (
"crypto/sha256"
"encoding/binary"
"errors"
)
// KeySize is the exact key size: an object content ID.
const KeySize = 32
// EmptyRoot is the root of a trie containing nothing.
var EmptyRoot = sha256.Sum256([]byte{0x02})
// Errors reported by proof generation and verification.
var (
ErrPresent = errors.New("smt: key is present")
ErrAbsent = errors.New("smt: key is absent")
ErrBadProof = errors.New("smt: bad proof")
)
type node struct {
leaf bool
// Leaf fields.
key [KeySize]byte
// Branch fields: prefix holds the bits between the parent's split and
// this node's own split, packed MSB-first, prefixLen valid bits. A
// branch always has both children.
prefix []byte
prefixLen int
left *node
right *node
}
// Trie is a grow-only set commitment. Not safe for concurrent use; callers
// serialise access.
type Trie struct {
root *node
n int
}
// New returns an empty trie.
func New() *Trie { return &Trie{} }
// Len returns the number of distinct keys inserted.
func (t *Trie) Len() int { return t.n }
// ---------------------------------------------------------------- primitives
func bit(k *[KeySize]byte, i int) byte {
return k[i>>3] >> (7 - uint(i&7)) & 1
}
func prefixBit(p []byte, i int) byte {
return p[i>>3] >> (7 - uint(i&7)) & 1
}
func commonPrefixLen(a, b *[KeySize]byte) int {
for i := 0; i < KeySize*8; i++ {
if bit(a, i) != bit(b, i) {
return i
}
}
return KeySize * 8
}
func packBits(k *[KeySize]byte, from, n int) []byte {
out := make([]byte, (n+7)/8)
for i := 0; i < n; i++ {
if bit(k, from+i) == 1 {
out[i>>3] |= 0x80 >> uint(i&7)
}
}
return out
}
func appendUvarint(b []byte, v int) []byte {
var tmp [10]byte
n := binary.PutUvarint(tmp[:], uint64(v))
return append(b, tmp[:n]...)
}
func leafHash(key *[KeySize]byte) [32]byte {
h := sha256.New()
h.Write([]byte{0x00})
h.Write(key[:])
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
func branchHash(prefix []byte, prefixLen int, left, right *[32]byte) [32]byte {
h := sha256.New()
h.Write([]byte{0x01})
var tmp [10]byte
n := binary.PutUvarint(tmp[:], uint64(prefixLen))
h.Write(tmp[:n])
h.Write(prefix)
h.Write(left[:])
h.Write(right[:])
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
func (t *Trie) hash(n *node) [32]byte {
if n.leaf {
return leafHash(&n.key)
}
l := t.hash(n.left)
r := t.hash(n.right)
return branchHash(n.prefix, n.prefixLen, &l, &r)
}
// Root returns the set commitment.
func (t *Trie) Root() [32]byte {
if t.root == nil {
return EmptyRoot
}
return t.hash(t.root)
}
// ------------------------------------------------------------------- insert
// Insert adds key, reporting whether it was new.
func (t *Trie) Insert(key [KeySize]byte) bool {
root, added := insertNode(t.root, &key, 0)
t.root = root
if added {
t.n++
}
return added
}
// Contains reports whether key has been inserted.
func (t *Trie) Contains(key [KeySize]byte) bool {
n := t.root
d := 0
for n != nil && !n.leaf {
i := 0
for i < n.prefixLen && bit(&key, d+i) == prefixBit(n.prefix, i) {
i++
}
if i < n.prefixLen {
return false // diverged inside the prefix
}
d += n.prefixLen
if bit(&key, d) == 0 {
n = n.left
} else {
n = n.right
}
d++
}
return n != nil && n.key == key
}
// insertNode inserts key into the subtree rooted at n, whose split decision
// begins at depth d. It returns the possibly-replaced subtree.
func insertNode(n *node, key *[KeySize]byte, d int) (*node, bool) {
if n == nil {
return &node{leaf: true, key: *key}, true
}
if n.leaf {
if n.key == *key {
return n, false
}
k := commonPrefixLen(&n.key, key)
// Shared bits d..k-1 become the new branch's prefix; bit k splits.
newBranch := &node{
prefix: packBits(key, d, k-d),
prefixLen: k - d,
}
if bit(key, k) == 0 {
newBranch.left = &node{leaf: true, key: *key}
newBranch.right = n
} else {
newBranch.left = n
newBranch.right = &node{leaf: true, key: *key}
}
return newBranch, true
}
// Branch: follow the prefix while it matches.
i := 0
for i < n.prefixLen && bit(key, d+i) == prefixBit(n.prefix, i) {
i++
}
if i < n.prefixLen {
// The key diverges inside the prefix at absolute bit d+i. Split the
// branch there: the upper half becomes the new branch's prefix, the
// old branch keeps the remainder as its own prefix.
j := d + i
upperPrefix := make([]byte, (i+7)/8)
for b := 0; b < i; b++ {
if prefixBit(n.prefix, b) == 1 {
upperPrefix[b>>3] |= 0x80 >> uint(b&7)
}
}
upper := &node{
prefix: upperPrefix,
prefixLen: i,
}
tail := copyTailBits(n.prefix, i, n.prefixLen)
lower := &node{
prefix: tail,
prefixLen: n.prefixLen - i - 1,
left: n.left,
right: n.right,
}
leaf := &node{leaf: true, key: *key}
if bit(key, j) == 0 {
upper.left = leaf
upper.right = lower
} else {
upper.left = lower
upper.right = leaf
}
return upper, true
}
sd := d + n.prefixLen
var added bool
if bit(key, sd) == 0 {
n.left, added = insertNode(n.left, key, sd+1)
} else {
n.right, added = insertNode(n.right, key, sd+1)
}
return n, added
}
// copyTailBits extracts bits [from+1, end) of p, MSB-packed.
func copyTailBits(p []byte, from, end int) []byte {
n := end - from - 1
out := make([]byte, (n+7)/8)
for i := 0; i < n; i++ {
if prefixBit(p, from+1+i) == 1 {
out[i>>3] |= 0x80 >> uint(i&7)
}
}
return out
}