mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
The caps decide which transfers a block may carry — admissible() refuses a transfer over the per-transfer cap and admit() refuses one over the daily cap, both inside block verification. Reading them from the operator's chain config meant two validators could hold different numbers and disagree about whether a transfer was admissible, and the first one they disagreed about is a transfer the builder keeps proposing and everyone else keeps refusing. A cap each operator sets for themselves is not a cap. Limits (minConfirmations, the two spend caps, the signer bond, the set size) move to Genesis, where the network declares them once and every validator reads the same numbers. BridgeConfig keeps only ExternalChains — endpoints and KMS paths, how THIS node reaches the chains it was told to bridge, which cannot change which transfers are valid. The dead BridgeFee field goes. MaxSigners keeps its default: it is a capacity, not a risk decision.
119 lines
4.2 KiB
Go
119 lines
4.2 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package bridgevm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// vmWithPending builds a VM holding n transfers ready for a block, and the ids
|
|
// they should be carried in.
|
|
func vmWithPending(t *testing.T, n int) (*VM, []ids.ID) {
|
|
t.Helper()
|
|
cfg := testLimits()
|
|
cfg.DailyBridgeLimit = 1 << 40
|
|
vm := bootOn(t, memdb.New(), cfg)
|
|
|
|
want := make([]ids.ID, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
req := requestFor(uint64(i+1), 1)
|
|
pend(vm, req)
|
|
want = append(want, req.ID)
|
|
}
|
|
sort.Slice(want, func(i, j int) bool { return bytes.Compare(want[i][:], want[j][:]) < 0 })
|
|
return vm, want
|
|
}
|
|
|
|
// TestBuildBlockOrdersRequestsDeterministically proves a block is a function of
|
|
// its inputs. BuildBlock collected requests by ranging pendingBridges, a map, so
|
|
// Go's randomised iteration decided the order — and that order is hashed into the
|
|
// block id (computeID) and written to the wire. The same pending set produced a
|
|
// different block on every build.
|
|
func TestBuildBlockOrdersRequestsDeterministically(t *testing.T) {
|
|
require := require.New(t)
|
|
vm, want := vmWithPending(t, 24)
|
|
|
|
blk := build(t, vm)
|
|
|
|
got := make([]ids.ID, 0, len(want))
|
|
for _, req := range blk.BridgeRequests {
|
|
got = append(got, req.ID)
|
|
}
|
|
require.Equal(want, got, "requests must be carried in id order")
|
|
|
|
// Rebuilding from the same pending set must reproduce the same block. The
|
|
// timestamp is the one thing that moves, so it is held still.
|
|
again := build(t, vm)
|
|
again.BlockTimestamp = blk.BlockTimestamp
|
|
again.ID_ = again.computeID()
|
|
require.Equal(blk.ID(), again.ID(), "the same pending set must build the same block")
|
|
require.Equal(blk.Bytes(), again.Bytes())
|
|
}
|
|
|
|
// TestBuildBlockCapIsADeterministicPrefix proves the size cap selects a defined
|
|
// subset. Applied while ranging a map it picked 100 requests at random out of the
|
|
// ready set, so a request could be passed over indefinitely for no reason and two
|
|
// builds carried different work.
|
|
func TestBuildBlockCapIsADeterministicPrefix(t *testing.T) {
|
|
require := require.New(t)
|
|
vm, want := vmWithPending(t, maxRequestsPerBlock+40)
|
|
|
|
blk := build(t, vm)
|
|
|
|
got := make([]ids.ID, 0, maxRequestsPerBlock)
|
|
for _, req := range blk.BridgeRequests {
|
|
got = append(got, req.ID)
|
|
}
|
|
require.Len(got, maxRequestsPerBlock)
|
|
require.Equal(want[:maxRequestsPerBlock], got, "the cap must take the lowest ids, not a random draw")
|
|
}
|
|
|
|
// TestBuildBlockRefusesWhenNothingIsReady keeps an empty block off the chain:
|
|
// Verify refuses one, so building one would wedge the proposer.
|
|
func TestBuildBlockRefusesWhenNothingIsReady(t *testing.T) {
|
|
vm, _ := vmWithPending(t, 0)
|
|
_, err := vm.BuildBlock(context.Background())
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// TestWhatIsBuiltIsWhatVerifies is the property that keeps the chain moving.
|
|
//
|
|
// Assembly and verification used different rules: the builder filtered on
|
|
// confirmation depth alone while Verify also applied the per-transfer cap and
|
|
// the daily cap. One oversized transfer in the pending set was therefore
|
|
// proposed by the builder, refused by every node, put straight back in the
|
|
// pending set by the rejection, and proposed again — block production stopped
|
|
// and did not resume.
|
|
func TestWhatIsBuiltIsWhatVerifies(t *testing.T) {
|
|
cfg := testLimits()
|
|
cfg.MaxBridgeAmount = 100
|
|
cfg.DailyBridgeLimit = 250
|
|
vm := bootOn(t, memdb.New(), cfg)
|
|
|
|
oversized := requestFor(1, 5_000) // over the per-transfer cap
|
|
fine := requestFor(2, 100)
|
|
pend(vm, oversized, fine)
|
|
|
|
blk := build(t, vm)
|
|
require.Len(t, blk.BridgeRequests, 1, "the builder must leave behind what Verify would refuse")
|
|
require.Equal(t, fine.ID, blk.BridgeRequests[0].ID)
|
|
require.NoError(t, blk.Verify(context.Background()), "what was built must verify")
|
|
require.NoError(t, blk.Accept(context.Background()))
|
|
|
|
// The daily cap is the same rule in the same place: 100 has moved of 250,
|
|
// so the next block carries what fits and stops there.
|
|
pend(vm, requestFor(3, 100), requestFor(4, 100))
|
|
second := build(t, vm)
|
|
require.Len(t, second.BridgeRequests, 1, "the builder stops at the daily cap, as Verify does")
|
|
require.NoError(t, second.Verify(context.Background()))
|
|
}
|