mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
Core runtime for network/ per docs/NETWORK.md. One env var (BASE_NETWORK=quasar) opts Base into PQ-native HA replication via luxfi/[email protected] Quasar DAG; one DAG per shard. Standalone mode (default) returns a no-op Network with zero overhead. Package surface: network.FromEnv() — parse BASE_* env, build Network network.Network.InstallWALHook(c, id) — install modernc commit hook network.Network.WriterFor(shardID) — router owner lookup network.Network.MembersFor(shardID) — router replica set network.Network.Metrics() — Prom collectors Frame = {ShardID, Seq, PrevSeq, Ts, Salt, Payload, Cksm, Bytes}. Content-addressed by (Salt, Cksm) so apply is idempotent across pod restarts. Frame.Bytes is the exact serialised wire form — archive replay never re-signs. Quasar adapter (quasar.go): the upstream public API is Engine-based (Submit(Block)/Finalized() <-chan Block), not the shard-scoped Submit(shardID, frame) the design doc assumed. Adapter wraps one Engine per shard; Block.ChainID = SHA256(shardID), Block.Data = Frame.encode(). threshold < 2 routes to NewTestEngine for single-node paths; production multi-node uses NewEngine with k = floor(N/2)+1. modernc.org/sqlite exposes RegisterCommitHook, not sqlite3_wal_hook — same semantics for our use. core/base_network.go bridges sqlite.HookRegisterer to network.HookRegisterer via a one-line adapter so the network package stays driver-agnostic. Hook into core/base.go: one field (network baseNetwork), one call at end of Bootstrap (app.attachNetwork(ctx)), one call in ResetBootstrapState (app.network.Stop). Standalone path unchanged. Metrics: every collector in the doc (base_shards, base_active_shards, base_wal_lag_bytes, base_hot_shards, base_lease_contentions, base_archive_lag_bytes) plus frames/errors counters for the produce/finalize loop. Callers bind to their own registry; prometheus.DefaultRegisterer untouched. Tests — 20 pass, 0 fail: wal_test.go frame encode/decode roundtrip, tamper, idempotency key router_test.go HRW determinism, balance, quorum math network_test.go FromEnv standalone/quasar, fail-fast validation integration_test.go 3 in-process nodes share memoryHub transport; A and B write different shards; all 3 finalise both frames and increment FramesIngested on the non-writer. core/ regression 384 PASS / 2 FAIL — identical to pre-change baseline. go.mod: +github.com/luxfi/consensus v1.22.85.
99 lines
2.3 KiB
Go
99 lines
2.3 KiB
Go
package network
|
|
|
|
import "testing"
|
|
|
|
func TestRouterDeterministic(t *testing.T) {
|
|
members := []NodeID{"a", "b", "c", "d", "e"}
|
|
r1 := newRouter(members, 3)
|
|
r2 := newRouter(append([]NodeID{"e", "b"}, "a", "c", "d"), 3)
|
|
|
|
for _, shard := range []string{"u1", "u2", "u3", "u4", "u5"} {
|
|
a := r1.membersFor(shard)
|
|
b := r2.membersFor(shard)
|
|
if len(a) != len(b) {
|
|
t.Fatalf("shard %s: length mismatch %v vs %v", shard, a, b)
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
t.Fatalf("shard %s: order mismatch at %d: %v vs %v", shard, i, a, b)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouterReplicationCapped(t *testing.T) {
|
|
r := newRouter([]NodeID{"a", "b"}, 5) // replication > members
|
|
ms := r.membersFor("x")
|
|
if len(ms) != 2 {
|
|
t.Errorf("len(membersFor) = %d, want 2 (capped at member count)", len(ms))
|
|
}
|
|
}
|
|
|
|
func TestRouterEmptyMembers(t *testing.T) {
|
|
r := newRouter(nil, 3)
|
|
if r.ownerOf("x") != "" {
|
|
t.Error("owner of empty ring should be \"\"")
|
|
}
|
|
if len(r.membersFor("x")) != 0 {
|
|
t.Error("members of empty ring should be empty")
|
|
}
|
|
}
|
|
|
|
func TestRouterOwnerIsFirstMember(t *testing.T) {
|
|
r := newRouter([]NodeID{"a", "b", "c"}, 3)
|
|
for _, shard := range []string{"q", "r", "s"} {
|
|
ms := r.membersFor(shard)
|
|
if r.ownerOf(shard) != ms[0] {
|
|
t.Errorf("ownerOf(%s) = %q but membersFor[0] = %q", shard, r.ownerOf(shard), ms[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouterBalancedEnough(t *testing.T) {
|
|
// 5 members, 1000 shards. Owner-counts should be within a reasonable band.
|
|
members := []NodeID{"a", "b", "c", "d", "e"}
|
|
r := newRouter(members, 1)
|
|
|
|
counts := map[NodeID]int{}
|
|
for i := 0; i < 1000; i++ {
|
|
counts[r.ownerOf(shardName(i))]++
|
|
}
|
|
// Expect each in [150, 250] (perfect is 200). Rendezvous hash bias is small.
|
|
for _, m := range members {
|
|
if c := counts[m]; c < 120 || c > 280 {
|
|
t.Errorf("member %s: %d shards, out of balance band", m, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func shardName(i int) string {
|
|
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
b := [4]byte{
|
|
letters[(i>>12)%len(letters)],
|
|
letters[(i>>8)%len(letters)],
|
|
letters[(i>>4)%len(letters)],
|
|
letters[i%len(letters)],
|
|
}
|
|
return string(b[:])
|
|
}
|
|
|
|
func TestQuorum(t *testing.T) {
|
|
cases := []struct {
|
|
n int
|
|
want int
|
|
}{
|
|
{0, 1},
|
|
{1, 1},
|
|
{2, 2},
|
|
{3, 2},
|
|
{4, 3},
|
|
{5, 3},
|
|
{7, 4},
|
|
}
|
|
for _, c := range cases {
|
|
if got := quorum(c.n); got != c.want {
|
|
t.Errorf("quorum(%d) = %d, want %d", c.n, got, c.want)
|
|
}
|
|
}
|
|
}
|