mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
142 lines
4.0 KiB
Go
142 lines
4.0 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// serve_gate_test.go — a peer must not be able to choose how much of our memory it
|
|
// spends.
|
|
//
|
|
// Answering a catch-up request walks up to maxContextBlocks blocks out of the VM and
|
|
// assembles them into one multi-hundred-kilobyte message. The request RATE is not
|
|
// ours to pick: a node that has fallen behind asks every peer, as fast as its timer
|
|
// allows, for as long as it stays behind. Unbounded, the number of answers under
|
|
// construction becomes this node's memory ceiling.
|
|
|
|
package chains
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/math/set"
|
|
)
|
|
|
|
func gatedHandler() *blockHandler {
|
|
return &blockHandler{
|
|
servingPeers: set.NewSet[ids.NodeID](16),
|
|
servingSlots: make(chan struct{}, maxConcurrentServes),
|
|
}
|
|
}
|
|
|
|
// TestOneAnswerPerPeer: a peer we are already answering does not get a second
|
|
// assembly started on its behalf. This is the bound that matters, because the flood
|
|
// comes from ONE behind node re-asking, not from many peers asking once.
|
|
func TestOneAnswerPerPeer(t *testing.T) {
|
|
b := gatedHandler()
|
|
peer := ids.GenerateTestNodeID()
|
|
|
|
if !b.beginServe(peer) {
|
|
t.Fatal("first request from a peer must be answered")
|
|
}
|
|
if b.beginServe(peer) {
|
|
t.Fatal("a peer already being answered must not start a second assembly")
|
|
}
|
|
|
|
b.endServe(peer)
|
|
if !b.beginServe(peer) {
|
|
t.Fatal("after finishing, the same peer must be servable again")
|
|
}
|
|
b.endServe(peer)
|
|
}
|
|
|
|
// TestServingIsCappedAcrossPeers: the per-peer rule alone still lets N peers cost N
|
|
// assemblies, so there is a ceiling over all of them.
|
|
func TestServingIsCappedAcrossPeers(t *testing.T) {
|
|
b := gatedHandler()
|
|
var held []ids.NodeID
|
|
|
|
for i := 0; i < maxConcurrentServes; i++ {
|
|
p := ids.GenerateTestNodeID()
|
|
if !b.beginServe(p) {
|
|
t.Fatalf("peer %d is within the cap and must be answered", i)
|
|
}
|
|
held = append(held, p)
|
|
}
|
|
if b.beginServe(ids.GenerateTestNodeID()) {
|
|
t.Fatalf("the %dth concurrent answer must be declined", maxConcurrentServes+1)
|
|
}
|
|
|
|
// Finishing one frees exactly one slot — not more, not none.
|
|
b.endServe(held[0])
|
|
next := ids.GenerateTestNodeID()
|
|
if !b.beginServe(next) {
|
|
t.Fatal("a freed slot must be reusable")
|
|
}
|
|
if b.beginServe(ids.GenerateTestNodeID()) {
|
|
t.Fatal("only one slot was freed, so only one more answer may start")
|
|
}
|
|
|
|
b.endServe(next)
|
|
for _, p := range held[1:] {
|
|
b.endServe(p)
|
|
}
|
|
}
|
|
|
|
// TestSlotsSurviveChurn: begin/end must balance exactly, or the gate slowly closes on
|
|
// a healthy node and catch-up stops working for everyone — a worse failure than the
|
|
// one the gate prevents.
|
|
func TestSlotsSurviveChurn(t *testing.T) {
|
|
b := gatedHandler()
|
|
for i := 0; i < maxConcurrentServes*50; i++ {
|
|
p := ids.GenerateTestNodeID()
|
|
if !b.beginServe(p) {
|
|
t.Fatalf("iteration %d: gate closed on a handler with nothing in flight — a slot leaked", i)
|
|
}
|
|
b.endServe(p)
|
|
}
|
|
if len(b.servingSlots) != 0 {
|
|
t.Fatalf("%d slots still held with nothing in flight", len(b.servingSlots))
|
|
}
|
|
if b.servingPeers.Len() != 0 {
|
|
t.Fatalf("%d peers still marked in flight", b.servingPeers.Len())
|
|
}
|
|
}
|
|
|
|
// TestConcurrentAskersCannotExceedTheCap drives the gate the way the network does —
|
|
// many goroutines at once — and asserts the cap holds and nothing leaks.
|
|
func TestConcurrentAskersCannotExceedTheCap(t *testing.T) {
|
|
b := gatedHandler()
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
live, peak := 0, 0
|
|
|
|
for i := 0; i < 200; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
p := ids.GenerateTestNodeID()
|
|
if !b.beginServe(p) {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
live++
|
|
if live > peak {
|
|
peak = live
|
|
}
|
|
mu.Unlock()
|
|
|
|
mu.Lock()
|
|
live--
|
|
mu.Unlock()
|
|
b.endServe(p)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
if peak > maxConcurrentServes {
|
|
t.Fatalf("%d answers were under construction at once, cap is %d", peak, maxConcurrentServes)
|
|
}
|
|
if len(b.servingSlots) != 0 || b.servingPeers.Len() != 0 {
|
|
t.Fatalf("leaked after churn: %d slots, %d peers", len(b.servingSlots), b.servingPeers.Len())
|
|
}
|
|
}
|