mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
bridgevm: the network declares its bridge limits, not the operator
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.
This commit is contained in:
@@ -126,7 +126,7 @@ func TestTheHeightIndexNamesOnlyAcceptedBlocks(t *testing.T) {
|
|||||||
// the number that bounds what this bridge can move.
|
// the number that bounds what this bridge can move.
|
||||||
func TestTheDailyCapSurvivesARestart(t *testing.T) {
|
func TestTheDailyCapSurvivesARestart(t *testing.T) {
|
||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxBridgeAmount = 600
|
cfg.MaxBridgeAmount = 600
|
||||||
cfg.DailyBridgeLimit = 1000
|
cfg.DailyBridgeLimit = 1000
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ func TestTheDailyCapSurvivesARestart(t *testing.T) {
|
|||||||
|
|
||||||
// TestTheCapIsCountedPerDestination keeps one busy route from closing another.
|
// TestTheCapIsCountedPerDestination keeps one busy route from closing another.
|
||||||
func TestTheCapIsCountedPerDestination(t *testing.T) {
|
func TestTheCapIsCountedPerDestination(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxBridgeAmount = 100
|
cfg.MaxBridgeAmount = 100
|
||||||
cfg.DailyBridgeLimit = 100
|
cfg.DailyBridgeLimit = 100
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
@@ -187,7 +187,7 @@ func TestTheCapIsCountedPerDestination(t *testing.T) {
|
|||||||
// window is the BLOCK's time — the whole point of measuring it there rather
|
// window is the BLOCK's time — the whole point of measuring it there rather
|
||||||
// than off each node's clock.
|
// than off each node's clock.
|
||||||
func TestTheWindowReopensOnTheNextDay(t *testing.T) {
|
func TestTheWindowReopensOnTheNextDay(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxBridgeAmount = 100
|
cfg.MaxBridgeAmount = 100
|
||||||
cfg.DailyBridgeLimit = 100
|
cfg.DailyBridgeLimit = 100
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
|
|||||||
+1
-1
@@ -221,7 +221,7 @@ func (b *Block) Verify(ctx context.Context) error {
|
|||||||
// admit records each transfer as it goes, so a block carrying the same
|
// admit records each transfer as it goes, so a block carrying the same
|
||||||
// one twice fails on the second: the first has already been recorded
|
// one twice fails on the second: the first has already been recorded
|
||||||
// settled, in this same state.
|
// settled, in this same state.
|
||||||
if err := state.admit(&b.vm.config, day, req); err != nil {
|
if err := state.admit(&b.vm.limits, day, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
// they should be carried in.
|
// they should be carried in.
|
||||||
func vmWithPending(t *testing.T, n int) (*VM, []ids.ID) {
|
func vmWithPending(t *testing.T, n int) (*VM, []ids.ID) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.DailyBridgeLimit = 1 << 40
|
cfg.DailyBridgeLimit = 1 << 40
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ func TestBuildBlockRefusesWhenNothingIsReady(t *testing.T) {
|
|||||||
// pending set by the rejection, and proposed again — block production stopped
|
// pending set by the rejection, and proposed again — block production stopped
|
||||||
// and did not resume.
|
// and did not resume.
|
||||||
func TestWhatIsBuiltIsWhatVerifies(t *testing.T) {
|
func TestWhatIsBuiltIsWhatVerifies(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxBridgeAmount = 100
|
cfg.MaxBridgeAmount = 100
|
||||||
cfg.DailyBridgeLimit = 250
|
cfg.DailyBridgeLimit = 250
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ import (
|
|||||||
// starting somewhere else.
|
// starting somewhere else.
|
||||||
func TestAChainThatCannotReadItsTipDoesNotStart(t *testing.T) {
|
func TestAChainThatCannotReadItsTipDoesNotStart(t *testing.T) {
|
||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
vm := bootOn(t, db, testConfig())
|
vm := bootOn(t, db, testLimits())
|
||||||
pend(vm, requestFor(1, 100))
|
pend(vm, requestFor(1, 100))
|
||||||
blk := buildAndAccept(t, vm)
|
blk := buildAndAccept(t, vm)
|
||||||
|
|
||||||
// The tip is recorded; the block it names is gone.
|
// The tip is recorded; the block it names is gone.
|
||||||
require.NoError(t, db.Delete(blockRecordKey(blk.ID())))
|
require.NoError(t, db.Delete(blockRecordKey(blk.ID())))
|
||||||
require.Error(t, (&VM{}).Initialize(context.Background(), initFor(db, testConfig())))
|
require.Error(t, (&VM{}).Initialize(context.Background(), initFor(db, testLimits())))
|
||||||
}
|
}
|
||||||
|
|
||||||
// blockRecordKey is where the shared store keeps a block's bytes.
|
// blockRecordKey is where the shared store keeps a block's bytes.
|
||||||
@@ -43,7 +43,7 @@ func TestAChainWithAnUnreadableTipDoesNotStart(t *testing.T) {
|
|||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
require.NoError(t, db.Put([]byte("chain/tip"), make([]byte, ids.IDLen)))
|
require.NoError(t, db.Put([]byte("chain/tip"), make([]byte, ids.IDLen)))
|
||||||
// Zero is a well-formed id naming no block.
|
// Zero is a well-formed id naming no block.
|
||||||
require.Error(t, (&VM{}).Initialize(context.Background(), initFor(db, testConfig())))
|
require.Error(t, (&VM{}).Initialize(context.Background(), initFor(db, testLimits())))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestARelayerThatIsNotReadingReportsIt. A node with chains wired and a
|
// TestARelayerThatIsNotReadingReportsIt. A node with chains wired and a
|
||||||
@@ -212,7 +212,7 @@ func TestRPC_ReplaceSignerWithANamedReplacement(t *testing.T) {
|
|||||||
|
|
||||||
func TestRPC_TheWaitlistIsNamed(t *testing.T) {
|
func TestRPC_TheWaitlistIsNamed(t *testing.T) {
|
||||||
srv, vm := newRPCRig(t)
|
srv, vm := newRPCRig(t)
|
||||||
vm.config.MaxSigners = 1
|
vm.limits.MaxSigners = 1
|
||||||
first, waiting := ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
|
first, waiting := ids.GenerateTestNodeID(), ids.GenerateTestNodeID()
|
||||||
require.NoError(t, registerSigner(vm, first))
|
require.NoError(t, registerSigner(vm, first))
|
||||||
require.NoError(t, registerSigner(vm, waiting))
|
require.NoError(t, registerSigner(vm, waiting))
|
||||||
@@ -268,7 +268,7 @@ func TestRPC_GetInfoOnceThereIsAGroupKey(t *testing.T) {
|
|||||||
// A counter bridge_getInfo cannot read is reported, not summed as zero.
|
// A counter bridge_getInfo cannot read is reported, not summed as zero.
|
||||||
func TestRPC_GetInfoSaysWhenItCannotRead(t *testing.T) {
|
func TestRPC_GetInfoSaysWhenItCannotRead(t *testing.T) {
|
||||||
vm := bootOn(t, &unreadableDB{Database: memdb.New(), prefix: movedPrefix, err: errUnreadable},
|
vm := bootOn(t, &unreadableDB{Database: memdb.New(), prefix: movedPrefix, err: errUnreadable},
|
||||||
testConfig())
|
testLimits())
|
||||||
srv, _ := serveVM(t, vm)
|
srv, _ := serveVM(t, vm)
|
||||||
|
|
||||||
code, msg := callRPC(t, srv.URL, "bridge_getInfo", nil, &GetBridgeInfoReply{})
|
code, msg := callRPC(t, srv.URL, "bridge_getInfo", nil, &GetBridgeInfoReply{})
|
||||||
@@ -408,7 +408,7 @@ func (*brokenIterator) Value() []byte { return nil }
|
|||||||
|
|
||||||
func TestAStoreThatCannotBeWalkedDoesNotBoot(t *testing.T) {
|
func TestAStoreThatCannotBeWalkedDoesNotBoot(t *testing.T) {
|
||||||
err := (&VM{}).Initialize(context.Background(),
|
err := (&VM{}).Initialize(context.Background(),
|
||||||
initFor(unlistableDB{Database: memdb.New()}, testConfig()))
|
initFor(unlistableDB{Database: memdb.New()}, testLimits()))
|
||||||
require.ErrorContains(t, err, "read settlement records")
|
require.ErrorContains(t, err, "read settlement records")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,20 @@ const (
|
|||||||
// would make every cross-node block mismatch look like correct behaviour.
|
// would make every cross-node block mismatch look like correct behaviour.
|
||||||
var testChainID = ids.ID{'b', '-', 'c', 'h', 'a', 'i', 'n', '-', 't', 'e', 's', 't'}
|
var testChainID = ids.ID{'b', '-', 'c', 'h', 'a', 'i', 'n', '-', 't', 'e', 's', 't'}
|
||||||
|
|
||||||
func testConfig() BridgeConfig {
|
// testLimits is what the NETWORK declares in genesis.
|
||||||
return BridgeConfig{
|
func testLimits() Limits {
|
||||||
|
return Limits{
|
||||||
MinConfirmations: 12,
|
MinConfirmations: 12,
|
||||||
MaxBridgeAmount: 1_000_000,
|
MaxBridgeAmount: 1_000_000,
|
||||||
DailyBridgeLimit: 10_000_000,
|
DailyBridgeLimit: 10_000_000,
|
||||||
RequireValidatorBond: minValidatorBond,
|
RequireValidatorBond: minValidatorBond,
|
||||||
MaxSigners: 100,
|
MaxSigners: 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testChains is what the OPERATOR declares: how this node reaches them.
|
||||||
|
func testChains() BridgeConfig {
|
||||||
|
return BridgeConfig{
|
||||||
ExternalChains: []ExternalChainConfig{
|
ExternalChains: []ExternalChainConfig{
|
||||||
{Name: "lux-testnet-c", ChainID: uint64(srcChain), Gateway: "0x0000000000000000000000000000000000000001"},
|
{Name: "lux-testnet-c", ChainID: uint64(srcChain), Gateway: "0x0000000000000000000000000000000000000001"},
|
||||||
{Name: "zoo-testnet", ChainID: uint64(dstChain), Gateway: "0x0000000000000000000000000000000000000002"},
|
{Name: "zoo-testnet", ChainID: uint64(dstChain), Gateway: "0x0000000000000000000000000000000000000002"},
|
||||||
@@ -82,8 +89,12 @@ func (b *refusingBatch) Write() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initFor is what the node hands Initialize.
|
// initFor is what the node hands Initialize.
|
||||||
func initFor(db database.Database, cfg BridgeConfig) vmcore.Init {
|
func initFor(db database.Database, lim Limits) vmcore.Init {
|
||||||
raw, err := json.Marshal(cfg)
|
raw, err := json.Marshal(testChains())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gen, err := json.Marshal(Genesis{Timestamp: 1000000, Limits: lim})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -95,31 +106,31 @@ func initFor(db database.Database, cfg BridgeConfig) vmcore.Init {
|
|||||||
NodeID: ids.GenerateTestNodeID(),
|
NodeID: ids.GenerateTestNodeID(),
|
||||||
},
|
},
|
||||||
DB: db,
|
DB: db,
|
||||||
Genesis: []byte(`{"timestamp":1000000}`),
|
Genesis: gen,
|
||||||
Config: raw,
|
Config: raw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bootOn brings a VM up over db exactly as the node does, with cfg as its
|
// bootOn brings a VM up over db exactly as the node does, with cfg as its
|
||||||
// configuration.
|
// configuration.
|
||||||
func bootOn(t *testing.T, db database.Database, cfg BridgeConfig) *VM {
|
func bootOn(t *testing.T, db database.Database, lim Limits) *VM {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
vm := &VM{}
|
vm := &VM{}
|
||||||
require.NoError(t, vm.Initialize(context.Background(), initFor(db, cfg)))
|
require.NoError(t, vm.Initialize(context.Background(), initFor(db, lim)))
|
||||||
return vm
|
return vm
|
||||||
}
|
}
|
||||||
|
|
||||||
// boot is a VM on a fresh database with the standard configuration.
|
// boot is a VM on a fresh database with the standard configuration.
|
||||||
func boot(t *testing.T) *VM {
|
func boot(t *testing.T) *VM {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return bootOn(t, memdb.New(), testConfig())
|
return bootOn(t, memdb.New(), testLimits())
|
||||||
}
|
}
|
||||||
|
|
||||||
// bootRefusing is a VM whose database can be told to refuse a flush.
|
// bootRefusing is a VM whose database can be told to refuse a flush.
|
||||||
func bootRefusing(t *testing.T) (*VM, *refusingDB) {
|
func bootRefusing(t *testing.T) (*VM, *refusingDB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db := &refusingDB{Database: memdb.New()}
|
db := &refusingDB{Database: memdb.New()}
|
||||||
return bootOn(t, db, testConfig()), db
|
return bootOn(t, db, testLimits()), db
|
||||||
}
|
}
|
||||||
|
|
||||||
// transferFor is one locked transfer. The request's id is the digest of its
|
// transferFor is one locked transfer. The request's id is the digest of its
|
||||||
|
|||||||
+7
-7
@@ -148,7 +148,7 @@ func counterBytes(v uint64) []byte {
|
|||||||
// transfer that fails here can never be carried by any block, and holding one
|
// transfer that fails here can never be carried by any block, and holding one
|
||||||
// would have it proposed, refused, and proposed again for as long as the node
|
// would have it proposed, refused, and proposed again for as long as the node
|
||||||
// runs.
|
// runs.
|
||||||
func admissible(cfg *BridgeConfig, req *BridgeRequest) error {
|
func admissible(lim *Limits, req *BridgeRequest) error {
|
||||||
transfer, err := req.transfer()
|
transfer, err := req.transfer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -169,9 +169,9 @@ func admissible(cfg *BridgeConfig, req *BridgeRequest) error {
|
|||||||
if req.Amount == 0 {
|
if req.Amount == 0 {
|
||||||
return fmt.Errorf("bridgevm: request %s moves nothing", req.ID)
|
return fmt.Errorf("bridgevm: request %s moves nothing", req.ID)
|
||||||
}
|
}
|
||||||
if req.Amount > cfg.MaxBridgeAmount {
|
if req.Amount > lim.MaxBridgeAmount {
|
||||||
return fmt.Errorf("bridgevm: request %s moves %d, over the %d per-transfer cap",
|
return fmt.Errorf("bridgevm: request %s moves %d, over the %d per-transfer cap",
|
||||||
req.ID, req.Amount, cfg.MaxBridgeAmount)
|
req.ID, req.Amount, lim.MaxBridgeAmount)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -183,8 +183,8 @@ func admissible(cfg *BridgeConfig, req *BridgeRequest) error {
|
|||||||
// order. Two rules would disagree eventually, and the first transfer they
|
// order. Two rules would disagree eventually, and the first transfer they
|
||||||
// disagreed about would be one the builder keeps proposing and every node
|
// disagreed about would be one the builder keeps proposing and every node
|
||||||
// keeps refusing — block production stops and does not resume.
|
// keeps refusing — block production stops and does not resume.
|
||||||
func (s *spend) admit(cfg *BridgeConfig, day int64, req *BridgeRequest) error {
|
func (s *spend) admit(lim *Limits, day int64, req *BridgeRequest) error {
|
||||||
if err := admissible(cfg, req); err != nil {
|
if err := admissible(lim, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
already, err := s.isSettled(req.ID)
|
already, err := s.isSettled(req.ID)
|
||||||
@@ -200,9 +200,9 @@ func (s *spend) admit(cfg *BridgeConfig, day int64, req *BridgeRequest) error {
|
|||||||
}
|
}
|
||||||
// Written as a subtraction so a total near the top of the range cannot
|
// Written as a subtraction so a total near the top of the range cannot
|
||||||
// wrap past the cap.
|
// wrap past the cap.
|
||||||
if req.Amount > cfg.DailyBridgeLimit-was {
|
if req.Amount > lim.DailyBridgeLimit-was {
|
||||||
return fmt.Errorf("bridgevm: request %s would move %d to chain %d, over the %d daily cap (%d already moved)",
|
return fmt.Errorf("bridgevm: request %s would move %d to chain %d, over the %d daily cap (%d already moved)",
|
||||||
req.ID, req.Amount, req.DstChainID, cfg.DailyBridgeLimit, was)
|
req.ID, req.Amount, req.DstChainID, lim.DailyBridgeLimit, was)
|
||||||
}
|
}
|
||||||
s.record(day, req, was)
|
s.record(day, req, was)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func TestASpendItCannotReadIsNotZero(t *testing.T) {
|
|||||||
|
|
||||||
// And the rule that reads it refuses rather than admitting on a total it
|
// And the rule that reads it refuses rather than admitting on a total it
|
||||||
// could not obtain.
|
// could not obtain.
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
require.ErrorIs(t, s.admit(&cfg, 1, requestFor(1, 100)), errUnreadable)
|
require.ErrorIs(t, s.admit(&cfg, 1, requestFor(1, 100)), errUnreadable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ func TestASettlementItCannotReadIsNotAbsent(t *testing.T) {
|
|||||||
_, err := s.isSettled(ids.GenerateTestID())
|
_, err := s.isSettled(ids.GenerateTestID())
|
||||||
require.ErrorIs(t, err, errUnreadable)
|
require.ErrorIs(t, err, errUnreadable)
|
||||||
|
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
require.ErrorIs(t, s.admit(&cfg, 1, requestFor(1, 100)), errUnreadable)
|
require.ErrorIs(t, s.admit(&cfg, 1, requestFor(1, 100)), errUnreadable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ func TestAChildsSpendIsItsOwn(t *testing.T) {
|
|||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
parent := newSpend(db)
|
parent := newSpend(db)
|
||||||
req := requestFor(1, 100)
|
req := requestFor(1, 100)
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
require.NoError(t, parent.admit(&cfg, 1, req))
|
require.NoError(t, parent.admit(&cfg, 1, req))
|
||||||
|
|
||||||
child := parent.clone()
|
child := parent.clone()
|
||||||
@@ -142,7 +142,7 @@ func TestAChildsSpendIsItsOwn(t *testing.T) {
|
|||||||
// A total near the top of the range does not wrap past the cap.
|
// A total near the top of the range does not wrap past the cap.
|
||||||
func TestTheCapDoesNotWrap(t *testing.T) {
|
func TestTheCapDoesNotWrap(t *testing.T) {
|
||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxBridgeAmount = ^uint64(0)
|
cfg.MaxBridgeAmount = ^uint64(0)
|
||||||
cfg.DailyBridgeLimit = 1000
|
cfg.DailyBridgeLimit = 1000
|
||||||
require.NoError(t, db.Put(movedKey(1, dstChain), counterBytes(999)))
|
require.NoError(t, db.Put(movedKey(1, dstChain), counterBytes(999)))
|
||||||
|
|||||||
+2
-2
@@ -190,8 +190,8 @@ func (r *releaser) releaseOnce(ctx context.Context, req *BridgeRequest, transfer
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("bridgevm: source confirmation check: %w", err)
|
return fmt.Errorf("bridgevm: source confirmation check: %w", err)
|
||||||
}
|
}
|
||||||
if conf < r.vm.config.MinConfirmations {
|
if conf < r.vm.limits.MinConfirmations {
|
||||||
return fmt.Errorf("%w: %d < %d", errInsufficientConfirmations, conf, r.vm.config.MinConfirmations)
|
return fmt.Errorf("%w: %d < %d", errInsufficientConfirmations, conf, r.vm.limits.MinConfirmations)
|
||||||
}
|
}
|
||||||
destTx, err := r.vm.releaseTransfer(ctx, transfer)
|
destTx, err := r.vm.releaseTransfer(ctx, transfer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func releaseReq() *BridgeRequest {
|
|||||||
func confirmationRig(t *testing.T, depth uint32) (*releaser, *sourceClient, *recordingClient, *fixedAttester) {
|
func confirmationRig(t *testing.T, depth uint32) (*releaser, *sourceClient, *recordingClient, *fixedAttester) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
vm, dst, ac := releaseRig(t, nil)
|
vm, dst, ac := releaseRig(t, nil)
|
||||||
vm.config.MinConfirmations = 12
|
vm.limits.MinConfirmations = 12
|
||||||
src := &sourceClient{recordingClient: &recordingClient{}, depth: depth}
|
src := &sourceClient{recordingClient: &recordingClient{}, depth: depth}
|
||||||
vm.evmByChainID[1] = src
|
vm.evmByChainID[1] = src
|
||||||
return &releaser{vm: vm}, src, dst, ac
|
return &releaser{vm: vm}, src, dst, ac
|
||||||
|
|||||||
+1
-1
@@ -663,7 +663,7 @@ func (s *Service) chainConfig(cfg ExternalChainConfig) ChainConfigReply {
|
|||||||
ChainID: strconv.FormatUint(cfg.ChainID, 10),
|
ChainID: strconv.FormatUint(cfg.ChainID, 10),
|
||||||
ChainName: cfg.Name,
|
ChainName: cfg.Name,
|
||||||
BridgeContract: cfg.Gateway,
|
BridgeContract: cfg.Gateway,
|
||||||
Confirmations: int(s.vm.config.MinConfirmations),
|
Confirmations: int(s.vm.limits.MinConfirmations),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ func TestRPC_GetSupportedChains(t *testing.T) {
|
|||||||
require.Equal(t, strconv.FormatUint(vm.config.ExternalChains[i].ChainID, 10), c.ChainID)
|
require.Equal(t, strconv.FormatUint(vm.config.ExternalChains[i].ChainID, 10), c.ChainID)
|
||||||
require.Equal(t, vm.config.ExternalChains[i].Name, c.ChainName)
|
require.Equal(t, vm.config.ExternalChains[i].Name, c.ChainName)
|
||||||
require.True(t, c.Enabled)
|
require.True(t, c.Enabled)
|
||||||
require.Equal(t, int(vm.config.MinConfirmations), c.Confirmations)
|
require.Equal(t, int(vm.limits.MinConfirmations), c.Confirmations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// boots one.
|
// boots one.
|
||||||
func signerVM(t *testing.T, maxSigners int) *VM {
|
func signerVM(t *testing.T, maxSigners int) *VM {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxSigners = maxSigners
|
cfg.MaxSigners = maxSigners
|
||||||
return bootOn(t, memdb.New(), cfg)
|
return bootOn(t, memdb.New(), cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ func TestATransferSettledByAnEarlierBlockCannotBeSettledAgain(t *testing.T) {
|
|||||||
// and a re-observed lock was released a second time.
|
// and a re-observed lock was released a second time.
|
||||||
func TestATransferSettledBeforeARestartCannotBeSettledAgain(t *testing.T) {
|
func TestATransferSettledBeforeARestartCannotBeSettledAgain(t *testing.T) {
|
||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
vm := bootOn(t, db, testConfig())
|
vm := bootOn(t, db, testLimits())
|
||||||
pend(vm, requestFor(1, 500))
|
pend(vm, requestFor(1, 500))
|
||||||
first := buildAndAccept(t, vm)
|
first := buildAndAccept(t, vm)
|
||||||
|
|
||||||
restarted := bootOn(t, db, testConfig())
|
restarted := bootOn(t, db, testLimits())
|
||||||
again := blockOn(t, restarted, first, now(), requestFor(1, 500))
|
again := blockOn(t, restarted, first, now(), requestFor(1, 500))
|
||||||
require.ErrorIs(t, again.Verify(context.Background()), errReplay)
|
require.ErrorIs(t, again.Verify(context.Background()), errReplay)
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func TestATransferInFlightCannotAlsoRideItsChild(t *testing.T) {
|
|||||||
// TestTheDailyCapHoldsAcrossBlocksInFlight. Checked against the tip alone, a
|
// TestTheDailyCapHoldsAcrossBlocksInFlight. Checked against the tip alone, a
|
||||||
// run of blocks that are all in the air each get the whole day's allowance.
|
// run of blocks that are all in the air each get the whole day's allowance.
|
||||||
func TestTheDailyCapHoldsAcrossBlocksInFlight(t *testing.T) {
|
func TestTheDailyCapHoldsAcrossBlocksInFlight(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxBridgeAmount = 100
|
cfg.MaxBridgeAmount = 100
|
||||||
cfg.DailyBridgeLimit = 150
|
cfg.DailyBridgeLimit = 150
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
@@ -140,7 +140,7 @@ func TestARequestMustBeTheDigestOfWhatItCarries(t *testing.T) {
|
|||||||
|
|
||||||
func TestARequestMustBeWellFormed(t *testing.T) {
|
func TestARequestMustBeWellFormed(t *testing.T) {
|
||||||
vm := boot(t)
|
vm := boot(t)
|
||||||
cfg := &vm.config
|
cfg := &vm.limits
|
||||||
|
|
||||||
// A recipient that is not an address cannot be released to.
|
// A recipient that is not an address cannot be released to.
|
||||||
short := requestFor(1, 500)
|
short := requestFor(1, 500)
|
||||||
@@ -307,8 +307,8 @@ func TestTwoAcceptsAtOnceLeaveOneChain(t *testing.T) {
|
|||||||
// database holding settled transfers is a read fault, not a new chain.
|
// database holding settled transfers is a read fault, not a new chain.
|
||||||
func TestARestartBeforeTheFirstBlockComesUp(t *testing.T) {
|
func TestARestartBeforeTheFirstBlockComesUp(t *testing.T) {
|
||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
first := bootOn(t, db, testConfig())
|
first := bootOn(t, db, testLimits())
|
||||||
second := bootOn(t, db, testConfig())
|
second := bootOn(t, db, testLimits())
|
||||||
|
|
||||||
require.Equal(t, first.genesisBlock.ID(), second.genesisBlock.ID(),
|
require.Equal(t, first.genesisBlock.ID(), second.genesisBlock.ID(),
|
||||||
"one chain, two nodes of it")
|
"one chain, two nodes of it")
|
||||||
@@ -576,7 +576,7 @@ func TestABlockOfAnotherChainIsNotABlockOfThisOne(t *testing.T) {
|
|||||||
// been spent — durably, with Initialize returning nil.
|
// been spent — durably, with Initialize returning nil.
|
||||||
func TestAChainDoesNotRestartOverItself(t *testing.T) {
|
func TestAChainDoesNotRestartOverItself(t *testing.T) {
|
||||||
db := memdb.New()
|
db := memdb.New()
|
||||||
vm := bootOn(t, db, testConfig())
|
vm := bootOn(t, db, testLimits())
|
||||||
pend(vm, requestFor(1, 500))
|
pend(vm, requestFor(1, 500))
|
||||||
buildAndAccept(t, vm)
|
buildAndAccept(t, vm)
|
||||||
|
|
||||||
@@ -584,7 +584,7 @@ func TestAChainDoesNotRestartOverItself(t *testing.T) {
|
|||||||
// they say this database is not a new chain.
|
// they say this database is not a new chain.
|
||||||
require.NoError(t, db.Delete([]byte("chain/tip")))
|
require.NoError(t, db.Delete([]byte("chain/tip")))
|
||||||
|
|
||||||
err := (&VM{}).Initialize(context.Background(), initFor(db, testConfig()))
|
err := (&VM{}).Initialize(context.Background(), initFor(db, testLimits()))
|
||||||
require.ErrorContains(t, err, "refusing to restart the chain over it")
|
require.ErrorContains(t, err, "refusing to restart the chain over it")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-45
@@ -46,26 +46,25 @@ var (
|
|||||||
// minimum in two places eventually enforces the smaller one.
|
// minimum in two places eventually enforces the smaller one.
|
||||||
const minValidatorBond uint64 = 1_000_000 * 1e9 // 1M LUX
|
const minValidatorBond uint64 = 1_000_000 * 1e9 // 1M LUX
|
||||||
|
|
||||||
// BridgeConfig contains VM configuration
|
// Limits are the bridge's risk parameters, and they are declared by the
|
||||||
type BridgeConfig struct {
|
// NETWORK in genesis rather than by the operator. They decide which transfers a
|
||||||
|
// block may carry (ledger.go admissible/admit), so two nodes holding different
|
||||||
|
// values disagree about whether a transfer is admissible — and the first one
|
||||||
|
// they disagree about is a transfer the builder keeps proposing and everyone
|
||||||
|
// else keeps refusing, which stops block production and does not resume. A cap
|
||||||
|
// each operator sets for themselves is not a cap.
|
||||||
|
//
|
||||||
|
// Nothing here is defaulted. A bridge whose cap is whatever the code happened
|
||||||
|
// to pick is a bridge nobody decided the risk of, and a zero per-transfer cap
|
||||||
|
// refuses every transfer silently at every block, which looks exactly like a
|
||||||
|
// bridge nobody is using.
|
||||||
|
type Limits struct {
|
||||||
// MinConfirmations is how deep a lock must be buried on its source chain
|
// MinConfirmations is how deep a lock must be buried on its source chain
|
||||||
// before this chain will carry it. It is applied where the lock is read,
|
// before this chain will carry it. It is applied where the lock is read,
|
||||||
// so a lock that is not yet deep enough is left for a later pass rather
|
// so a lock that is not yet deep enough is left for a later pass rather
|
||||||
// than admitted and held.
|
// than admitted and held.
|
||||||
MinConfirmations uint32 `json:"minConfirmations"`
|
MinConfirmations uint32 `json:"minConfirmations"`
|
||||||
BridgeFee uint64 `json:"bridgeFee"` // Fee in LUX for bridge operations
|
|
||||||
|
|
||||||
// ExternalChains is the ONE declaration of which chains this bridge
|
|
||||||
// serves. It carries endpoints, gateway + custody addresses, and KMS paths
|
|
||||||
// for the relayer keys — never key material. A relayer node reads this and
|
|
||||||
// calls EnableBridgeRelease with a KMS-backed KeyProvider and a Warp-backed
|
|
||||||
// AttestationClient; every other node uses it to name the chains it will
|
|
||||||
// carry transfers between.
|
|
||||||
ExternalChains []ExternalChainConfig `json:"externalChains,omitempty"`
|
|
||||||
|
|
||||||
// The spend caps. Both are declared, never defaulted: a bridge whose cap
|
|
||||||
// is whatever the code happens to pick is a bridge nobody decided the risk
|
|
||||||
// of.
|
|
||||||
MaxBridgeAmount uint64 `json:"maxBridgeAmount"` // Maximum amount per transfer
|
MaxBridgeAmount uint64 `json:"maxBridgeAmount"` // Maximum amount per transfer
|
||||||
DailyBridgeLimit uint64 `json:"dailyBridgeLimit"` // Maximum per destination per day
|
DailyBridgeLimit uint64 `json:"dailyBridgeLimit"` // Maximum per destination per day
|
||||||
|
|
||||||
@@ -75,26 +74,36 @@ type BridgeConfig struct {
|
|||||||
MaxSigners int `json:"maxSigners"`
|
MaxSigners int `json:"maxSigners"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate refuses a configuration this chain cannot enforce. A bridge with no
|
// validate refuses limits this chain cannot enforce.
|
||||||
// declared cap does not fail safe by accident: a zero per-transfer cap refuses
|
func (l *Limits) validate() error {
|
||||||
// every transfer silently at every block, which looks exactly like a bridge
|
|
||||||
// nobody is using.
|
|
||||||
func (c *BridgeConfig) validate() error {
|
|
||||||
switch {
|
switch {
|
||||||
case c.MinConfirmations == 0:
|
case l.MinConfirmations == 0:
|
||||||
return errors.New("bridgevm: minConfirmations must be at least 1")
|
return errors.New("bridgevm: minConfirmations must be at least 1")
|
||||||
case c.MaxBridgeAmount == 0:
|
case l.MaxBridgeAmount == 0:
|
||||||
return errors.New("bridgevm: maxBridgeAmount must be declared")
|
return errors.New("bridgevm: maxBridgeAmount must be declared")
|
||||||
case c.DailyBridgeLimit < c.MaxBridgeAmount:
|
case l.DailyBridgeLimit < l.MaxBridgeAmount:
|
||||||
return fmt.Errorf("bridgevm: dailyBridgeLimit %d is below maxBridgeAmount %d",
|
return fmt.Errorf("bridgevm: dailyBridgeLimit %d is below maxBridgeAmount %d",
|
||||||
c.DailyBridgeLimit, c.MaxBridgeAmount)
|
l.DailyBridgeLimit, l.MaxBridgeAmount)
|
||||||
case c.RequireValidatorBond < minValidatorBond:
|
case l.RequireValidatorBond < minValidatorBond:
|
||||||
return fmt.Errorf("bridgevm: requireValidatorBond %d is below the %d minimum",
|
return fmt.Errorf("bridgevm: requireValidatorBond %d is below the %d minimum",
|
||||||
c.RequireValidatorBond, minValidatorBond)
|
l.RequireValidatorBond, minValidatorBond)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BridgeConfig is the operator's half: how THIS node reaches the chains the
|
||||||
|
// network told it to bridge. Nothing here can change which transfers are
|
||||||
|
// valid, so two operators may hold different values without disagreeing.
|
||||||
|
type BridgeConfig struct {
|
||||||
|
// ExternalChains is the ONE declaration of which chains this bridge
|
||||||
|
// serves. It carries endpoints, gateway + custody addresses, and KMS paths
|
||||||
|
// for the relayer keys — never key material. A relayer node reads this and
|
||||||
|
// calls EnableBridgeRelease with a KMS-backed KeyProvider and a Warp-backed
|
||||||
|
// AttestationClient; every other node uses it to name the chains it will
|
||||||
|
// carry transfers between.
|
||||||
|
ExternalChains []ExternalChainConfig `json:"externalChains,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// SignerSet tracks the current MPC signer set (LP-333)
|
// SignerSet tracks the current MPC signer set (LP-333)
|
||||||
// First MaxSigners validators opt-in without reshare. Reshare ONLY on slot
|
// First MaxSigners validators opt-in without reshare. Reshare ONLY on slot
|
||||||
// replacement.
|
// replacement.
|
||||||
@@ -216,6 +225,7 @@ const maxRequestsPerBlock = 100
|
|||||||
type VM struct {
|
type VM struct {
|
||||||
rt *runtime.Runtime
|
rt *runtime.Runtime
|
||||||
config BridgeConfig
|
config BridgeConfig
|
||||||
|
limits Limits
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
|
||||||
// chainID is the chain this VM is running, hashed into every block id so a
|
// chainID is the chain this VM is running, hashed into every block id so a
|
||||||
@@ -341,15 +351,23 @@ func (vm *VM) Initialize(
|
|||||||
return fmt.Errorf("bridgevm: parse config: %w", err)
|
return fmt.Errorf("bridgevm: parse config: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if vm.config.MaxSigners == 0 {
|
|
||||||
vm.config.MaxSigners = 100 // LP-333: the set freezes here
|
genesis := &Genesis{}
|
||||||
|
if len(vmInit.Genesis) > 0 {
|
||||||
|
if err := json.Unmarshal(vmInit.Genesis, genesis); err != nil {
|
||||||
|
return fmt.Errorf("bridgevm: parse genesis: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := vm.config.validate(); err != nil {
|
vm.limits = genesis.Limits
|
||||||
|
if vm.limits.MaxSigners == 0 {
|
||||||
|
vm.limits.MaxSigners = 100 // LP-333: the set freezes here. A capacity, not a risk decision.
|
||||||
|
}
|
||||||
|
if err := vm.limits.validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.signerSet = &SignerSet{
|
vm.signerSet = &SignerSet{
|
||||||
Signers: make([]*SignerInfo, 0, vm.config.MaxSigners),
|
Signers: make([]*SignerInfo, 0, vm.limits.MaxSigners),
|
||||||
Waitlist: make([]ids.NodeID, 0),
|
Waitlist: make([]ids.NodeID, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,13 +382,6 @@ func (vm *VM) Initialize(
|
|||||||
// carries the chain list; a relayer reads vm.config.ExternalChains and calls
|
// carries the chain list; a relayer reads vm.config.ExternalChains and calls
|
||||||
// EnableBridgeRelease. Non-relayer nodes never broadcast.
|
// EnableBridgeRelease. Non-relayer nodes never broadcast.
|
||||||
|
|
||||||
genesis := &Genesis{}
|
|
||||||
if len(vmInit.Genesis) > 0 {
|
|
||||||
if err := json.Unmarshal(vmInit.Genesis, genesis); err != nil {
|
|
||||||
return fmt.Errorf("bridgevm: parse genesis: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vm.chain = chain.New[*Block](vmInit.DB, nil)
|
vm.chain = chain.New[*Block](vmInit.DB, nil)
|
||||||
|
|
||||||
genesisBlock := &Block{
|
genesisBlock := &Block{
|
||||||
@@ -461,7 +472,7 @@ func (vm *VM) BuildBlock(ctx context.Context) (vmchain.Block, error) {
|
|||||||
if len(requests) == maxRequestsPerBlock {
|
if len(requests) == maxRequestsPerBlock {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := state.admit(&vm.config, day, req); err != nil {
|
if err := state.admit(&vm.limits, day, req); err != nil {
|
||||||
vm.log.Debug("bridgevm: transfer not carried",
|
vm.log.Debug("bridgevm: transfer not carried",
|
||||||
log.Stringer("requestID", req.ID), log.String("reason", err.Error()))
|
log.Stringer("requestID", req.ID), log.String("reason", err.Error()))
|
||||||
continue
|
continue
|
||||||
@@ -655,6 +666,9 @@ func (vm *VM) WaitForEvent(ctx context.Context) (vmcore.Message, error) {
|
|||||||
// Genesis represents the genesis state
|
// Genesis represents the genesis state
|
||||||
type Genesis struct {
|
type Genesis struct {
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
// Limits are inline so a network declares its caps beside the rest of the
|
||||||
|
// chain's genesis, in one file, and every validator reads the same numbers.
|
||||||
|
Limits
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -720,12 +734,12 @@ func (vm *VM) RegisterValidator(input *RegisterValidatorInput) (*RegisterValidat
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("bridgevm: bond amount %q is not a number", input.BondAmount)
|
return nil, fmt.Errorf("bridgevm: bond amount %q is not a number", input.BondAmount)
|
||||||
}
|
}
|
||||||
if bondAmount < vm.config.RequireValidatorBond {
|
if bondAmount < vm.limits.RequireValidatorBond {
|
||||||
return nil, fmt.Errorf("bridgevm: bond %d is below the required %d",
|
return nil, fmt.Errorf("bridgevm: bond %d is below the required %d",
|
||||||
bondAmount, vm.config.RequireValidatorBond)
|
bondAmount, vm.limits.RequireValidatorBond)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !vm.signerSet.SetFrozen && len(vm.signerSet.Signers) < vm.config.MaxSigners {
|
if !vm.signerSet.SetFrozen && len(vm.signerSet.Signers) < vm.limits.MaxSigners {
|
||||||
signerInfo := &SignerInfo{
|
signerInfo := &SignerInfo{
|
||||||
NodeID: nodeID,
|
NodeID: nodeID,
|
||||||
PartyID: party.ID(nodeID.String()),
|
PartyID: party.ID(nodeID.String()),
|
||||||
@@ -739,7 +753,7 @@ func (vm *VM) RegisterValidator(input *RegisterValidatorInput) (*RegisterValidat
|
|||||||
|
|
||||||
vm.signerSet.Signers = append(vm.signerSet.Signers, signerInfo)
|
vm.signerSet.Signers = append(vm.signerSet.Signers, signerInfo)
|
||||||
vm.signerSet.renumber()
|
vm.signerSet.renumber()
|
||||||
if len(vm.signerSet.Signers) >= vm.config.MaxSigners {
|
if len(vm.signerSet.Signers) >= vm.limits.MaxSigners {
|
||||||
vm.signerSet.SetFrozen = true
|
vm.signerSet.SetFrozen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,7 +774,7 @@ func (vm *VM) RegisterValidator(input *RegisterValidatorInput) (*RegisterValidat
|
|||||||
Threshold: vm.signerSet.ThresholdT,
|
Threshold: vm.signerSet.ThresholdT,
|
||||||
CurrentEpoch: vm.signerSet.CurrentEpoch,
|
CurrentEpoch: vm.signerSet.CurrentEpoch,
|
||||||
SetFrozen: vm.signerSet.SetFrozen,
|
SetFrozen: vm.signerSet.SetFrozen,
|
||||||
RemainingSlots: vm.config.MaxSigners - len(vm.signerSet.Signers),
|
RemainingSlots: vm.limits.MaxSigners - len(vm.signerSet.Signers),
|
||||||
Message: "registered as bridge signer",
|
Message: "registered as bridge signer",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -795,10 +809,10 @@ func (vm *VM) GetSignerSetInfo() *SignerSetInfo {
|
|||||||
info := &SignerSetInfo{
|
info := &SignerSetInfo{
|
||||||
TotalSigners: len(vm.signerSet.Signers),
|
TotalSigners: len(vm.signerSet.Signers),
|
||||||
Threshold: vm.signerSet.ThresholdT,
|
Threshold: vm.signerSet.ThresholdT,
|
||||||
MaxSigners: vm.config.MaxSigners,
|
MaxSigners: vm.limits.MaxSigners,
|
||||||
CurrentEpoch: vm.signerSet.CurrentEpoch,
|
CurrentEpoch: vm.signerSet.CurrentEpoch,
|
||||||
SetFrozen: vm.signerSet.SetFrozen,
|
SetFrozen: vm.signerSet.SetFrozen,
|
||||||
RemainingSlots: vm.config.MaxSigners - len(vm.signerSet.Signers),
|
RemainingSlots: vm.limits.MaxSigners - len(vm.signerSet.Signers),
|
||||||
WaitlistSize: len(vm.signerSet.Waitlist),
|
WaitlistSize: len(vm.signerSet.Waitlist),
|
||||||
Signers: vm.signerSet.Signers,
|
Signers: vm.signerSet.Signers,
|
||||||
}
|
}
|
||||||
@@ -1045,7 +1059,7 @@ func (vm *VM) SlashSigner(input *SlashSignerInput) (*SlashSignerResult, error) {
|
|||||||
// A signer whose bond no longer meets what the chain requires is not a
|
// A signer whose bond no longer meets what the chain requires is not a
|
||||||
// signer. The requirement is read from the same declaration registration
|
// signer. The requirement is read from the same declaration registration
|
||||||
// applies, so the two cannot drift apart.
|
// applies, so the two cannot drift apart.
|
||||||
if remainingBond < vm.config.RequireValidatorBond {
|
if remainingBond < vm.limits.RequireValidatorBond {
|
||||||
vm.signerSet.Signers = append(vm.signerSet.Signers[:signerIndex], vm.signerSet.Signers[signerIndex+1:]...)
|
vm.signerSet.Signers = append(vm.signerSet.Signers[:signerIndex], vm.signerSet.Signers[signerIndex+1:]...)
|
||||||
vm.signerSet.renumber()
|
vm.signerSet.renumber()
|
||||||
vm.signerSet.CurrentEpoch++
|
vm.signerSet.CurrentEpoch++
|
||||||
|
|||||||
+16
-16
@@ -50,19 +50,19 @@ func TestInitializeRefusesWhatItCannotRunOn(t *testing.T) {
|
|||||||
|
|
||||||
require.ErrorContains(t, (&VM{}).Initialize(ctx, vmcore.Init{}), "no runtime")
|
require.ErrorContains(t, (&VM{}).Initialize(ctx, vmcore.Init{}), "no runtime")
|
||||||
|
|
||||||
noDB := initFor(memdb.New(), testConfig())
|
noDB := initFor(memdb.New(), testLimits())
|
||||||
noDB.DB = nil
|
noDB.DB = nil
|
||||||
require.ErrorContains(t, (&VM{}).Initialize(ctx, noDB), "no database")
|
require.ErrorContains(t, (&VM{}).Initialize(ctx, noDB), "no database")
|
||||||
|
|
||||||
noLog := initFor(memdb.New(), testConfig())
|
noLog := initFor(memdb.New(), testLimits())
|
||||||
noLog.Runtime = &runtime.Runtime{NetworkID: 96369}
|
noLog.Runtime = &runtime.Runtime{NetworkID: 96369}
|
||||||
require.ErrorContains(t, (&VM{}).Initialize(ctx, noLog), "invalid logger")
|
require.ErrorContains(t, (&VM{}).Initialize(ctx, noLog), "invalid logger")
|
||||||
|
|
||||||
badConfig := initFor(memdb.New(), testConfig())
|
badConfig := initFor(memdb.New(), testLimits())
|
||||||
badConfig.Config = []byte(`{"maxBridgeAmount": "not a number"}`)
|
badConfig.Config = []byte(`{"externalChains": "not a list"}`)
|
||||||
require.ErrorContains(t, (&VM{}).Initialize(ctx, badConfig), "parse config")
|
require.ErrorContains(t, (&VM{}).Initialize(ctx, badConfig), "parse config")
|
||||||
|
|
||||||
badGenesis := initFor(memdb.New(), testConfig())
|
badGenesis := initFor(memdb.New(), testLimits())
|
||||||
badGenesis.Genesis = []byte(`{`)
|
badGenesis.Genesis = []byte(`{`)
|
||||||
require.ErrorContains(t, (&VM{}).Initialize(ctx, badGenesis), "parse genesis")
|
require.ErrorContains(t, (&VM{}).Initialize(ctx, badGenesis), "parse genesis")
|
||||||
}
|
}
|
||||||
@@ -73,30 +73,30 @@ func TestInitializeRefusesWhatItCannotRunOn(t *testing.T) {
|
|||||||
func TestABridgeDeclaresItsCaps(t *testing.T) {
|
func TestABridgeDeclaresItsCaps(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for name, mutate := range map[string]func(*BridgeConfig){
|
for name, mutate := range map[string]func(*Limits){
|
||||||
"minConfirmations must be at least 1": func(c *BridgeConfig) { c.MinConfirmations = 0 },
|
"minConfirmations must be at least 1": func(c *Limits) { c.MinConfirmations = 0 },
|
||||||
"maxBridgeAmount must be declared": func(c *BridgeConfig) { c.MaxBridgeAmount = 0 },
|
"maxBridgeAmount must be declared": func(c *Limits) { c.MaxBridgeAmount = 0 },
|
||||||
"is below maxBridgeAmount": func(c *BridgeConfig) { c.DailyBridgeLimit = c.MaxBridgeAmount - 1 },
|
"is below maxBridgeAmount": func(c *Limits) { c.DailyBridgeLimit = c.MaxBridgeAmount - 1 },
|
||||||
"is below the": func(c *BridgeConfig) { c.RequireValidatorBond = minValidatorBond - 1 },
|
"is below the": func(c *Limits) { c.RequireValidatorBond = minValidatorBond - 1 },
|
||||||
} {
|
} {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
mutate(&cfg)
|
mutate(&cfg)
|
||||||
require.ErrorContains(t, (&VM{}).Initialize(ctx, initFor(memdb.New(), cfg)), name)
|
require.ErrorContains(t, (&VM{}).Initialize(ctx, initFor(memdb.New(), cfg)), name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// An empty configuration declares nothing, so it runs nothing.
|
// A genesis that declares no limits declares nothing, so it runs nothing.
|
||||||
empty := initFor(memdb.New(), testConfig())
|
empty := initFor(memdb.New(), testLimits())
|
||||||
empty.Config = nil
|
empty.Genesis = nil
|
||||||
require.Error(t, (&VM{}).Initialize(ctx, empty))
|
require.Error(t, (&VM{}).Initialize(ctx, empty))
|
||||||
}
|
}
|
||||||
|
|
||||||
// The signer set freezes at a size, and that size has a default because it is a
|
// The signer set freezes at a size, and that size has a default because it is a
|
||||||
// capacity rather than a risk decision.
|
// capacity rather than a risk decision.
|
||||||
func TestTheSignerSetSizeDefaults(t *testing.T) {
|
func TestTheSignerSetSizeDefaults(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxSigners = 0
|
cfg.MaxSigners = 0
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
require.Equal(t, 100, vm.config.MaxSigners)
|
require.Equal(t, 100, vm.limits.MaxSigners)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTheGenesisTimestampIsTheChainsFirstTime.
|
// TestTheGenesisTimestampIsTheChainsFirstTime.
|
||||||
|
|||||||
+2
-2
@@ -132,7 +132,7 @@ func (w *watcher) pass() {
|
|||||||
// chain requiring 100 permanently ineligible: the depth was never revisited and
|
// chain requiring 100 permanently ineligible: the depth was never revisited and
|
||||||
// the cursor had already moved past the lock.
|
// the cursor had already moved past the lock.
|
||||||
func (w *watcher) lag() uint64 {
|
func (w *watcher) lag() uint64 {
|
||||||
if conf := uint64(w.vm.config.MinConfirmations); conf > minWatchLag {
|
if conf := uint64(w.vm.limits.MinConfirmations); conf > minWatchLag {
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
return minWatchLag
|
return minWatchLag
|
||||||
@@ -199,7 +199,7 @@ func (w *watcher) enqueue(l lock) {
|
|||||||
// A lock this chain can never carry is not held: it would be proposed,
|
// A lock this chain can never carry is not held: it would be proposed,
|
||||||
// refused, and proposed again for as long as the node runs. What can be
|
// refused, and proposed again for as long as the node runs. What can be
|
||||||
// decided without the chain's state is decided here, once.
|
// decided without the chain's state is decided here, once.
|
||||||
if err := admissible(&w.vm.config, req); err != nil {
|
if err := admissible(&w.vm.limits, req); err != nil {
|
||||||
w.vm.log.Warn("bridgevm: lock not admitted",
|
w.vm.log.Warn("bridgevm: lock not admitted",
|
||||||
log.Stringer("requestID", id), log.String("reason", err.Error()))
|
log.Stringer("requestID", id), log.String("reason", err.Error()))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func TestWatchStaysBehindTheHeadAndBoundsAPass(t *testing.T) {
|
|||||||
// ever carried. Reading as deep as the chain asks makes anything read eligible
|
// ever carried. Reading as deep as the chain asks makes anything read eligible
|
||||||
// by construction, and there is no depth left to go stale.
|
// by construction, and there is no depth left to go stale.
|
||||||
func TestWatchReadsAsDeepAsTheChainAsksFor(t *testing.T) {
|
func TestWatchReadsAsDeepAsTheChainAsksFor(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MinConfirmations = 500
|
cfg.MinConfirmations = 500
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
src := &fakeSource{head: 1000}
|
src := &fakeSource{head: 1000}
|
||||||
@@ -239,7 +239,7 @@ func TestAClientThatIsNotASourceIsSkipped(t *testing.T) {
|
|||||||
// lock is read, is the difference between one warning and a proposer that
|
// lock is read, is the difference between one warning and a proposer that
|
||||||
// never makes progress again.
|
// never makes progress again.
|
||||||
func TestALockThatCanNeverBeCarriedIsNotHeld(t *testing.T) {
|
func TestALockThatCanNeverBeCarriedIsNotHeld(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testLimits()
|
||||||
cfg.MaxBridgeAmount = 10
|
cfg.MaxBridgeAmount = 10
|
||||||
vm := bootOn(t, memdb.New(), cfg)
|
vm := bootOn(t, memdb.New(), cfg)
|
||||||
src := &fakeSource{head: 1000}
|
src := &fakeSource{head: 1000}
|
||||||
|
|||||||
Reference in New Issue
Block a user