Files
chains/bridgevm/apply_test.go
T
zeekay 6595c4ff2e bridgevm: an empty group is not a key, and the arms nothing can reach are gone
A CMP group with no shares sums to the identity point, which marshals to a
well-formed thirty-three bytes. Handed back as the custody key it made an
empty config look like a finished keygen: the node reported itself ready to
attest and bridge_getInfo agreed, on a chain that could verify nothing.

The rest is subtraction. A settlement record now holds only the fact that
the transfer is settled, because the block that settled it is already named
by the chain. movedToday reports what it read or why it could not, instead
of reporting zero. RemoveSigner returns what happened rather than an error
it never had. Marshal cannot fail, so Bytes has no failure to handle. The
gorilla-rpc registration was a second way to serve one API. The route table
NewHTTPHandler built was a second copy of the one CreateHandlers names. The
logger is set at Initialize, so guarding every log call against its absence
was guarding against ourselves.

How often the watcher polls is data on the watcher, as the releaser's
backoff already is.
2026-08-28 11:59:59 -07:00

227 lines
8.4 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bridgevm
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/chains/chain"
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
)
// TestABlockThatCannotCommitReleasesNothing is the defect this chain had.
// Accept credited the daily volume, completed the request, advanced
// lastAcceptedID and handed the transfer to the release worker — which
// broadcasts on an external chain — and only then wrote the block. A write
// that failed left all of it done, including a release in flight for a
// transfer no block records. lastAcceptedID was never written at all, so the
// tip reset to genesis on restart and the daily limit reset with it.
func TestABlockThatCannotCommitReleasesNothing(t *testing.T) {
vm, db := bootRefusing(t)
vm.releaser = &releaser{vm: vm, queue: make(chan *BridgeRequest, 8), quit: make(chan struct{})}
req := requestFor(1, 7)
pend(vm, req)
blk := build(t, vm)
require.NoError(t, blk.Verify(context.Background()))
tipBefore, heightBefore := vm.chain.Tip()
db.refuse = true
require.ErrorIs(t, blk.Accept(context.Background()), errRefused)
// The irreversible part: nothing was handed to the release worker.
require.Empty(t, vm.releaser.queue,
"an external release must not start for a block that did not commit")
// Nor did any of the durable part happen.
require.Zero(t, moved(t, vm, blk.BlockTimestamp, dstChain),
"none of the amount counts against the daily cap")
settled, err := newSpend(vm.chain.Base()).isSettled(req.ID)
require.NoError(t, err)
require.False(t, settled, "no transfer is settled by a block that did not commit")
vm.mu.RLock()
_, stillPending := vm.pendingBridges[req.ID]
vm.mu.RUnlock()
require.True(t, stillPending, "the transfer is still in flight, so it is not lost")
tip, height := vm.chain.Tip()
require.Equal(t, tipBefore, tip)
require.Equal(t, heightBefore, height)
// The block is not committed. It is still in flight, which is right — the
// engine may put it to a vote again — but nothing about it is on disk.
require.False(t, vm.chain.Accepted(blk.ID()))
found, err := vm.GetBlock(context.Background(), blk.ID())
require.NoError(t, err, "a block in flight is still findable as a parent")
require.Equal(t, blk.ID(), found.ID())
// And it can be applied again once the database will take it.
db.refuse = false
require.NoError(t, blk.Accept(context.Background()))
require.True(t, vm.chain.Accepted(blk.ID()))
require.Len(t, vm.releaser.queue, 1)
}
// TestAnAcceptedBlockIsDurableAndReleased is the other half, and pins the tip
// as durable: it was never written before, so a restart reset the chain to
// genesis and reset the daily bridge limit with it.
func TestAnAcceptedBlockIsDurableAndReleased(t *testing.T) {
vm, db := bootRefusing(t)
vm.releaser = &releaser{vm: vm, queue: make(chan *BridgeRequest, 8), quit: make(chan struct{})}
req := requestFor(1, 7)
pend(vm, req)
blk := buildAndAccept(t, vm)
require.Len(t, vm.releaser.queue, 1)
require.Equal(t, req.ID, (<-vm.releaser.queue).ID)
require.Equal(t, uint64(7), moved(t, vm, blk.BlockTimestamp, dstChain))
tip, height := vm.chain.Tip()
require.Equal(t, blk.ID(), tip)
require.Equal(t, uint64(1), height)
// A second store over the same database is a restart: the tip is where the
// chain left it, not back at genesis.
restarted := chain.New[*Block](db, nil)
at, fresh, err := restarted.Open(vm.genesisBlock, vm.parseBlock)
require.NoError(t, err)
require.False(t, fresh)
require.Equal(t, blk.ID(), at.ID())
}
// TestTheHeightIndexNamesOnlyAcceptedBlocks pins the index to the commit that
// wrote it.
func TestTheHeightIndexNamesOnlyAcceptedBlocks(t *testing.T) {
vm, db := bootRefusing(t)
pend(vm, requestFor(1, 7))
blk := build(t, vm)
require.NoError(t, blk.Verify(context.Background()))
db.refuse = true
require.ErrorIs(t, blk.Accept(context.Background()), errRefused)
_, err := vm.GetBlockIDAtHeight(context.Background(), 1)
require.Error(t, err, "a block that did not commit has no height entry")
db.refuse = false
require.NoError(t, blk.Accept(context.Background()))
at, err := vm.GetBlockIDAtHeight(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, blk.ID(), at)
}
// TestTheDailyCapSurvivesARestart is the defect this file exists for.
//
// The counter the cap is measured against lived in a map on the heap. A
// restart built a fresh one, so the whole day's allowance came back: an
// operator restart — or a crash, or a rolling deploy — was a free reset of
// the number that bounds what this bridge can move.
func TestTheDailyCapSurvivesARestart(t *testing.T) {
db := memdb.New()
cfg := testConfig()
cfg.MaxBridgeAmount = 600
cfg.DailyBridgeLimit = 1000
vm := bootOn(t, db, cfg)
pend(vm, requestFor(1, 600))
first := buildAndAccept(t, vm)
require.Equal(t, uint64(600), moved(t, vm, first.BlockTimestamp, dstChain))
// Restart: a second VM over the same database, exactly as the node does.
restarted := bootOn(t, db, cfg)
require.Equal(t, uint64(600), moved(t, restarted, first.BlockTimestamp, dstChain),
"the day's spend is read back from disk, not started over")
// 600 already moved against a cap of 1000, so a second 600 does not fit.
pend(restarted, requestFor(2, 600))
_, err := restarted.BuildBlock(context.Background())
require.Error(t, err, "the restarted chain must still refuse what the cap no longer allows")
// And what does fit, fits.
pend(restarted, requestFor(3, 400))
blk := build(t, restarted)
require.Len(t, blk.BridgeRequests, 1)
require.Equal(t, uint64(400), blk.BridgeRequests[0].Amount)
require.NoError(t, blk.Verify(context.Background()))
require.NoError(t, blk.Accept(context.Background()))
require.Equal(t, uint64(1000), moved(t, restarted, blk.BlockTimestamp, dstChain))
}
// TestTheCapIsCountedPerDestination keeps one busy route from closing another.
func TestTheCapIsCountedPerDestination(t *testing.T) {
cfg := testConfig()
cfg.MaxBridgeAmount = 100
cfg.DailyBridgeLimit = 100
vm := bootOn(t, memdb.New(), cfg)
toZoo := requestFor(1, 100)
other := transferFor(2, 100)
other.DstChainID = 777
toElsewhere := &BridgeRequest{
ID: ids.ID(other.Digest()), SrcChainID: other.SrcChainID, DstChainID: other.DstChainID,
Nonce: other.Nonce, Asset: ids.ID(other.Asset), Amount: other.Amount,
Recipient: append([]byte(nil), other.Recipient[:]...),
}
pend(vm, toZoo, toElsewhere)
blk := buildAndAccept(t, vm)
require.Len(t, blk.BridgeRequests, 2, "two destinations each have their own cap")
require.Equal(t, uint64(100), moved(t, vm, blk.BlockTimestamp, dstChain))
require.Equal(t, uint64(100), moved(t, vm, blk.BlockTimestamp, 777))
}
// TestTheWindowReopensOnTheNextDay proves the cap is daily rather than
// permanent. The counter it replaced only ever grew, so once total volume
// passed the limit every later block failed verification and the bridge
// stopped for good.
//
// Both blocks are stamped by hand, in the past, because what decides the
// window is the BLOCK's time — the whole point of measuring it there rather
// than off each node's clock.
func TestTheWindowReopensOnTheNextDay(t *testing.T) {
cfg := testConfig()
cfg.MaxBridgeAmount = 100
cfg.DailyBridgeLimit = 100
vm := bootOn(t, memdb.New(), cfg)
genesisTime := vm.genesisBlock.BlockTimestamp
first := blockOn(t, vm, vm.genesisBlock, genesisTime, requestFor(1, 100))
require.NoError(t, first.Verify(context.Background()))
require.NoError(t, first.Accept(context.Background()))
// A third 100 on the same day does not fit.
same := blockOn(t, vm, first, genesisTime, requestFor(2, 100))
require.ErrorContains(t, same.Verify(context.Background()), "daily cap")
// The next day starts the window over.
next := blockOn(t, vm, first, genesisTime+dayLength, requestFor(2, 100))
require.NoError(t, next.Verify(context.Background()))
require.NoError(t, next.Accept(context.Background()))
require.Equal(t, uint64(100), moved(t, vm, genesisTime, dstChain))
require.Equal(t, uint64(100), moved(t, vm, genesisTime+dayLength, dstChain))
}
// blockOn assembles a block on a chosen parent at a chosen time, which is how
// a peer's block arrives: this node did not pick either.
func blockOn(t *testing.T, vm *VM, parent *Block, at int64, reqs ...*BridgeRequest) *Block {
t.Helper()
blk := &Block{
ParentID_: parent.ID(),
BlockHeight: parent.Height() + 1,
BlockTimestamp: at,
BridgeRequests: reqs,
vm: vm,
}
blk.ID_ = blk.computeID()
return blk
}