Files
bft/msm/approvals_test.go
zeekayandHanzo Dev db33c93d2a Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:46:54 -07:00

435 lines
20 KiB
Go

// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package metadata
import (
"errors"
"math"
"testing"
"github.com/luxfi/bft/common"
"github.com/luxfi/bft/host"
"github.com/luxfi/bft/testutil"
"github.com/stretchr/testify/require"
)
func makeNodeID(seed byte) host.NodeID {
var n host.NodeID
n[0] = seed
return n
}
func makeValidators(n int) NodeBLSMappings {
vdrs := make(NodeBLSMappings, n)
for i := range n {
vdrs[i] = NodeBLSMapping{
NodeID: makeNodeID(byte(i + 1)),
BLSKey: []byte{byte(i + 1)},
Weight: 1,
}
}
return vdrs
}
func TestApprovalStoreHandleApproval(t *testing.T) {
for _, tc := range []struct {
name string
validators int
sigErr error
// approvals is the sequence of approvals (and their timestamps) to hand to the
// store, in order. Each one is expected to be accepted without error. Node IDs are
// referenced via makeNodeID(i+1), matching the i-th validator from makeValidators.
approvals []approvalAndTimestamp
// verify asserts on the store after all approvals have been handled. sent is the
// approvals slice above, so cases can reference the values they submitted.
verify func(t *testing.T, as *ApprovalStore, sent []approvalAndTimestamp)
}{
{
// Verifies that an approval from a node that is not in the validator set is silently dropped.
name: "approval from unknown node is dropped",
validators: 3,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(99), PChainHeight: 1}, 1},
},
verify: func(t *testing.T, as *ApprovalStore, _ []approvalAndTimestamp) {
require.Empty(t, as.Approvals())
require.Equal(t, 0, as.storedCount)
},
},
{
// Verifies that an approval from a known validator whose signature fails verification is dropped without being stored.
name: "approval with invalid signature is dropped",
validators: 3,
sigErr: errors.New("bad sig"),
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 1},
},
verify: func(t *testing.T, as *ApprovalStore, _ []approvalAndTimestamp) {
require.Empty(t, as.Approvals())
require.Equal(t, 0, as.storedCount)
},
},
{
// happy path: an approval from a known validator with a valid signature is stored and
// is retrievable via Approvals() and also increases storedCount.
name: "valid approval is stored",
validators: 3,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100},
},
verify: func(t *testing.T, as *ApprovalStore, sent []approvalAndTimestamp) {
got := as.Approvals()
require.Len(t, got, 1)
require.Equal(t, sent[0].ValidatorSetApproval, got[0])
require.Equal(t, 1, as.storedCount)
},
},
{
// Verifies that handing the same (NodeID, PChainHeight, Timestamp) twice is a no-op.
// The store keeps exactly one copy and storedCount does not double-count.
name: "duplicate approval with same timestamp is a no-op",
validators: 3,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100},
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100},
},
verify: func(t *testing.T, as *ApprovalStore, _ []approvalAndTimestamp) {
require.Len(t, as.Approvals(), 1)
require.Equal(t, 1, as.storedCount)
},
},
{
// Verifies that when a newer approval is already stored for a (NodeID, PChainHeight), a subsequent
// approval with an older Timestamp is dropped and does not overwrite it.
name: "older timestamp is ignored",
validators: 3,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 200}, // newer, stored first
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100}, // older, dropped
},
verify: func(t *testing.T, as *ApprovalStore, sent []approvalAndTimestamp) {
got := as.Approvals()
require.Len(t, got, 1)
require.Equal(t, sent[0].ValidatorSetApproval, got[0]) // the newer approval is kept
require.Equal(t, 1, as.storedCount)
},
},
{
// Verifies that a newer approval at the same (NodeID, PChainHeight) replaces the previously stored
// one in place without changing storedCount.
name: "newer timestamp replaces",
validators: 3,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100}, // older, stored first
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 200}, // newer, replaces
},
verify: func(t *testing.T, as *ApprovalStore, sent []approvalAndTimestamp) {
got := as.Approvals()
require.Len(t, got, 1)
require.Equal(t, sent[1].ValidatorSetApproval, got[0]) // the newer approval replaces the older one
require.Equal(t, 1, as.storedCount)
},
},
{
// Verifies that the store keeps independent entries per (NodeID, PChainHeight) tuple.
// Different validators and different heights all coexist.
name: "multiple nodes and heights coexist",
validators: 3,
// 3 validators x 2 heights, with timestamp == i*10 + h and Signature == {i} per validator.
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 1},
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 2, Signature: signApproval(2, [32]byte{})}, 2},
{common.ValidatorSetApproval{NodeID: makeNodeID(2), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 11},
{common.ValidatorSetApproval{NodeID: makeNodeID(2), PChainHeight: 2, Signature: signApproval(2, [32]byte{})}, 12},
{common.ValidatorSetApproval{NodeID: makeNodeID(3), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 21},
{common.ValidatorSetApproval{NodeID: makeNodeID(3), PChainHeight: 2, Signature: signApproval(2, [32]byte{})}, 22},
},
verify: func(t *testing.T, as *ApprovalStore, sent []approvalAndTimestamp) {
require.Len(t, as.Approvals(), len(sent))
require.Equal(t, len(sent), as.storedCount)
},
},
{
// Verifies that once a node accumulates more approvals than len(validators),
// the entry with the oldest Timestamp is evicted and storedCount stays in sync.
name: "prunes oldest when over cap",
validators: 2,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 10},
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 2, Signature: signApproval(2, [32]byte{})}, 20},
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 3, Signature: signApproval(3, [32]byte{})}, 30},
},
verify: func(t *testing.T, as *ApprovalStore, _ []approvalAndTimestamp) {
got := as.Approvals()
require.Len(t, got, 2, "store should be pruned down to cap=len(validators)=2")
require.Equal(t, 2, as.storedCount)
// Approvals() no longer carries the timestamp, so the pruned entry is identified by
// its PChainHeight: height 1 had the oldest timestamp (ts=10) and should be gone.
heights := map[uint64]bool{}
for _, a := range got {
heights[a.PChainHeight] = true
}
require.False(t, heights[1], "oldest (height=1, ts=10) should have been pruned")
require.True(t, heights[2])
require.True(t, heights[3])
},
},
{
// Verifies that the store keys on (NodeID, PChainHeight, AuxInfoDigest):
// two approvals from the same node at the same P-chain height but with different
// auxiliary info digests are kept as independent entries.
name: "same height different aux info digest coexist",
validators: 3,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, AuxInfoDigest: [32]byte{0xAA}, Signature: signApproval(7, [32]byte{0xAA})}, 100},
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, AuxInfoDigest: [32]byte{0xBB}, Signature: signApproval(7, [32]byte{0xBB})}, 100},
},
verify: func(t *testing.T, as *ApprovalStore, sent []approvalAndTimestamp) {
got := as.Approvals()
require.Len(t, got, 2)
require.Equal(t, 2, as.storedCount)
require.ElementsMatch(t, []common.ValidatorSetApproval{sent[0].ValidatorSetApproval, sent[1].ValidatorSetApproval}, got)
},
},
{
// Verifies that an approval with the maximum uint64 timestamp is stored,
// and that a subsequent approval at the same (NodeID, PChainHeight) with any
// smaller timestamp is treated as older and does not replace it.
name: "max uint64 timestamp is kept over any smaller timestamp",
validators: 3,
approvals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, math.MaxUint64}, // maxTS
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, math.MaxUint64 - 1}, // older
},
verify: func(t *testing.T, as *ApprovalStore, sent []approvalAndTimestamp) {
got := as.Approvals()
require.Len(t, got, 1)
require.Equal(t, sent[0].ValidatorSetApproval, got[0]) // the max-timestamp approval is kept
require.Equal(t, 1, as.storedCount)
},
},
} {
t.Run(tc.name, func(t *testing.T) {
vdrs := makeValidators(tc.validators)
as := NewApprovalStore(&signatureVerifier{err: tc.sigErr}, vdrs, testutil.MakeLogger(t))
for _, a := range tc.approvals {
require.NoError(t, as.HandleApproval(&a.ValidatorSetApproval, a.Timestamp))
}
tc.verify(t, as, tc.approvals)
})
}
}
func TestApprovalStorePutApprovals(t *testing.T) {
// findApproval locates the approval for a given (NodeID, PChainHeight) in a slice returned by
// Approvals(). It is used to assert which of two competing approvals (source vs. destination)
// survived a merge, since Approvals() does not carry the timestamp but does carry the Signature,
// and signApproval produces a distinct signature per call.
findApproval := func(got ValidatorSetApprovals, node host.NodeID, height uint64) (common.ValidatorSetApproval, bool) {
for _, a := range got {
if a.NodeID == node && a.PChainHeight == height {
return a, true
}
}
return common.ValidatorSetApproval{}, false
}
for _, tc := range []struct {
name string
// srcValidators/dstValidators size the two stores via makeValidators; NodeIDs are makeNodeID(i+1).
srcValidators int
dstValidators int
// srcApprovals are loaded into the source store; dstApprovals are pre-loaded into the destination
// store before PutApprovals is called.
srcApprovals []approvalAndTimestamp
dstApprovals []approvalAndTimestamp
// verify asserts on the destination (and source) store after src.PutApprovals(dst).
verify func(t *testing.T, dst, src *ApprovalStore, srcSent, dstSent []approvalAndTimestamp)
}{
{
// Copies every source approval into an empty destination that shares the same validator set,
// and leaves the source store untouched (PutApprovals copies, it does not move).
name: "copies all approvals into empty destination",
srcValidators: 3,
dstValidators: 3,
srcApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100},
{common.ValidatorSetApproval{NodeID: makeNodeID(2), PChainHeight: 8, Signature: signApproval(8, [32]byte{})}, 100},
},
verify: func(t *testing.T, dst, src *ApprovalStore, srcSent, _ []approvalAndTimestamp) {
got := dst.Approvals()
require.Len(t, got, 2)
require.Equal(t, 2, dst.storedCount)
require.ElementsMatch(t, []common.ValidatorSetApproval{srcSent[0].ValidatorSetApproval, srcSent[1].ValidatorSetApproval}, got)
// The source store is unchanged.
require.Len(t, src.Approvals(), 2)
require.Equal(t, 2, src.storedCount)
},
},
{
// Approvals from nodes absent from the destination's (smaller) validator set are dropped on
// carry-over. This is the epoch-change case: a validator that is not part of the new set does
// not have its approval carried into the new store.
name: "drops approvals from nodes not in destination validator set",
srcValidators: 3,
dstValidators: 2,
srcApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 10},
{common.ValidatorSetApproval{NodeID: makeNodeID(2), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 10},
{common.ValidatorSetApproval{NodeID: makeNodeID(3), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 10},
},
verify: func(t *testing.T, dst, _ *ApprovalStore, _, _ []approvalAndTimestamp) {
got := dst.Approvals()
require.Len(t, got, 2, "only approvals from nodes in the destination set carry over")
require.Equal(t, 2, dst.storedCount)
_, ok := findApproval(got, makeNodeID(3), 1)
require.False(t, ok, "node 3 is not in the destination validator set")
},
},
{
// Merges into a non-empty destination: a pre-existing destination approval with a newer
// timestamp is kept over an older one carried from the source, while a brand-new node's
// approval from the source is added.
name: "keeps newer destination approval and adds new node",
srcValidators: 2,
dstValidators: 2,
dstApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 200}, // newer, already present
},
srcApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100}, // older, must not overwrite
{common.ValidatorSetApproval{NodeID: makeNodeID(2), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100}, // new node, added
},
verify: func(t *testing.T, dst, _ *ApprovalStore, _, dstSent []approvalAndTimestamp) {
got := dst.Approvals()
require.Len(t, got, 2)
require.Equal(t, 2, dst.storedCount)
kept, ok := findApproval(got, makeNodeID(1), 7)
require.True(t, ok)
require.Equal(t, dstSent[0].ValidatorSetApproval, kept, "the newer destination approval is retained")
_, ok = findApproval(got, makeNodeID(2), 7)
require.True(t, ok, "the new node's approval is carried over")
},
},
{
// A source approval with a newer timestamp replaces the stale destination approval at the
// same (NodeID, PChainHeight).
name: "newer source approval replaces stale destination approval",
srcValidators: 2,
dstValidators: 2,
dstApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100}, // older, present
},
srcApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 200}, // newer, replaces
},
verify: func(t *testing.T, dst, _ *ApprovalStore, srcSent, _ []approvalAndTimestamp) {
got := dst.Approvals()
require.Len(t, got, 1)
require.Equal(t, 1, dst.storedCount)
require.Equal(t, srcSent[0].ValidatorSetApproval, got[0], "the newer source approval replaces the stale one")
},
},
{
// An empty source store is a no-op: the destination is left exactly as it was.
name: "empty source is a no-op",
srcValidators: 2,
dstValidators: 2,
dstApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 10},
},
verify: func(t *testing.T, dst, _ *ApprovalStore, _, dstSent []approvalAndTimestamp) {
got := dst.Approvals()
require.Len(t, got, 1)
require.Equal(t, 1, dst.storedCount)
require.Equal(t, dstSent[0].ValidatorSetApproval, got[0])
},
},
{
// A source approval with the same timestamp as an existing destination approval at the same
// (NodeID, PChainHeight) does not overwrite it: the >= tie in approvalExistsAndUpToDate goes
// to the current destination.
name: "equal-timestamp source does not overwrite destination",
srcValidators: 2,
dstValidators: 2,
dstApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100},
},
srcApprovals: []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: makeNodeID(1), PChainHeight: 7, Signature: signApproval(7, [32]byte{})}, 100},
},
verify: func(t *testing.T, dst, _ *ApprovalStore, _, dstSent []approvalAndTimestamp) {
got := dst.Approvals()
require.Len(t, got, 1)
require.Equal(t, 1, dst.storedCount)
require.Equal(t, dstSent[0].ValidatorSetApproval, got[0], "the destination approval is retained on a timestamp tie")
},
},
} {
t.Run(tc.name, func(t *testing.T) {
src := NewApprovalStore(&signatureVerifier{}, makeValidators(tc.srcValidators), testutil.MakeLogger(t))
dst := NewApprovalStore(&signatureVerifier{}, makeValidators(tc.dstValidators), testutil.MakeLogger(t))
for _, a := range tc.srcApprovals {
require.NoError(t, src.HandleApproval(&a.ValidatorSetApproval, a.Timestamp))
}
for _, a := range tc.dstApprovals {
require.NoError(t, dst.HandleApproval(&a.ValidatorSetApproval, a.Timestamp))
}
src.PutApprovals(dst)
// storedCount must always stay in sync with the number of retrievable approvals.
require.Len(t, dst.Approvals(), dst.storedCount)
tc.verify(t, dst, src, tc.srcApprovals, tc.dstApprovals)
})
}
}
func TestApprovalStoreHandleApprovalStoredCountStaysConsistent(t *testing.T) {
// Runs a mixed workload (insert, duplicate, replace, new height, prune)
// and asserts that storedCount equals len(Approvals()) after every step.
vdrs := makeValidators(2)
as := NewApprovalStore(&signatureVerifier{}, vdrs, testutil.MakeLogger(t))
node := vdrs[0].NodeID
for _, a := range []approvalAndTimestamp{
{common.ValidatorSetApproval{NodeID: node, PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 10},
{common.ValidatorSetApproval{NodeID: node, PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 10}, // duplicate
{common.ValidatorSetApproval{NodeID: node, PChainHeight: 1, Signature: signApproval(1, [32]byte{})}, 20}, // replaces
{common.ValidatorSetApproval{NodeID: node, PChainHeight: 2, Signature: signApproval(2, [32]byte{})}, 30}, // new height
{common.ValidatorSetApproval{NodeID: node, PChainHeight: 3, Signature: signApproval(3, [32]byte{})}, 40}, // triggers prune
} {
require.NoError(t, as.HandleApproval(&a.ValidatorSetApproval, a.Timestamp))
require.Len(t, as.Approvals(), as.storedCount)
}
require.Equal(t, 2, len(as.Approvals()))
}
func TestApprovalStoreHandleApprovalPruningIsPerNode(t *testing.T) {
// Verifies that the cap is applied per-NodeID, not globally: filling one node up to its cap does not
// affect another node's approvals.
vdrs := makeValidators(2)
as := NewApprovalStore(&signatureVerifier{}, vdrs, testutil.MakeLogger(t))
require.NoError(t, as.HandleApproval(&common.ValidatorSetApproval{
NodeID: vdrs[1].NodeID,
PChainHeight: 1,
Signature: signApproval(1, [32]byte{}),
}, 100))
for h := uint64(1); h <= 10; h++ {
require.NoError(t, as.HandleApproval(&common.ValidatorSetApproval{
NodeID: vdrs[0].NodeID,
PChainHeight: h,
Signature: signApproval(h, [32]byte{}),
}, h))
}
require.Len(t, as.Approvals().UniqueByNodeID(), 2)
}