Files
proto/p/warp/validator_test.go
zeekay 7dfb687462 proto/p: rip luxfi/codec from api/, warp/, warp/message/, warp/payload/
Wave 2A of the codec rip (#101). Eliminates github.com/luxfi/codec imports
from the api/, warp/, warp/message/, and warp/payload/ subtrees of proto/p.

* api/{height,staking}.go now use github.com/luxfi/node/utils/json instead
  of github.com/luxfi/codec/jsonrpc — same byte format, no transitive
  codec dependency.

* warp/codec.go, warp/payload/codec.go, warp/message/codec.go each define
  a local Codec / Registry interface (structurally identical to
  codec.Manager / codec.Registry) and a RegisterTypes(r) function. The
  package-level Codec singleton is gone — every Parse*, New*, *.Initialize,
  TeleportMessage.{ToWarpMessage,DecryptPayload}, TeleportTransferPayload.
  Bytes and ParseTransferPayload now take a Codec parameter and the
  caller threads it through.

* proto/internal/pcodectest is the canonical test wiring (mirrors
  proto/internal/xcodectest from Wave 1A). It exposes NewPayloadCodec,
  NewMessageCodec, and NewWarpCodec — each constructs a linearcodec-
  backed codec.Manager with the appropriate types pre-registered.

* In-package warp tests converted to `package warp_test` to avoid the
  cycle pcodectest → warp → warp/*_test.go → pcodectest. warp's prod
  `test_helpers.go` was renamed to `_test.go` (it was already test-only
  in spirit) and unexported helpers stay unexported in `warp_test`. The
  dead `test_signer_test.go` (//go:build test, unused) was removed.

* No backwards-compat shim. No deprecated alias. proto/p/api/warp/* is
  now byte-compatible with the legacy wire format but constructs its
  codec dependency externally.

Cascade breaks (Wave 2D territory, not fixed here):
  sdk/wallet/chain/p/wallet/backend_visitor.go — warp.ParseMessage,
    payload.ParseAddressedCall now require Codec
  sdk/validatormanager/root.go — payload.* calls need Codec
  vm/rpc/{,dag/,chain/}warp_adapter.go — warp.* calls need Codec

Grep zero confirmation under the subtrees migrated:
  proto/p/api/ — no codec import
  proto/p/warp/, proto/p/warp/message/, proto/p/warp/payload/ — no
  codec import (signertest subpackage also clean)
2026-06-06 00:47:45 -07:00

537 lines
15 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package warp_test
import (
"context"
"math"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/metric"
"github.com/luxfi/proto/p/warp"
"github.com/luxfi/upgrade"
validators "github.com/luxfi/validators"
"github.com/luxfi/validators/validatorsmock"
)
var (
chainID = ids.GenerateTestID()
)
// testValidatorStateAdapter wraps validators.State to implement ValidatorState
// converting GetValidatorOutput to ValidatorData
type testValidatorStateAdapter struct {
validators.State
}
func (t *testValidatorStateAdapter) GetValidatorSet(ctx context.Context, height uint64, chainID ids.ID) (map[ids.NodeID]*warp.ValidatorData, error) {
validatorSet, err := t.State.GetValidatorSet(ctx, height, chainID)
if err != nil {
return nil, err
}
result := make(map[ids.NodeID]*warp.ValidatorData, len(validatorSet))
for nodeID, validator := range validatorSet {
result[nodeID] = &warp.ValidatorData{
NodeID: validator.NodeID,
PublicKey: validator.PublicKey,
Weight: validator.Weight,
}
}
return result, nil
}
func TestGetCanonicalValidatorSet(t *testing.T) {
type test struct {
name string
stateF func(*gomock.Controller) validators.State
expectedVdrs []*warp.Validator
expectedWeight uint64
expectedErr error
}
tests := []test{
{
name: "can't get validator set",
stateF: func(ctrl *gomock.Controller) validators.State {
state := validatorsmock.NewState(ctrl)
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, chainID).Return(nil, errTest)
return state
},
expectedErr: errTest,
},
{
name: "all validators have public keys; no duplicate pub keys",
stateF: func(ctrl *gomock.Controller) validators.State {
state := validatorsmock.NewState(ctrl)
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, chainID).Return(
map[ids.NodeID]*validators.GetValidatorOutput{
testVdrs[0].nodeID: {
NodeID: testVdrs[0].nodeID,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[0].vdr.PublicKey),
Weight: testVdrs[0].vdr.Weight,
},
testVdrs[1].nodeID: {
NodeID: testVdrs[1].nodeID,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[1].vdr.PublicKey),
Weight: testVdrs[1].vdr.Weight,
},
},
nil,
)
return state
},
expectedVdrs: []*warp.Validator{testVdrs[0].vdr, testVdrs[1].vdr},
expectedWeight: 6,
expectedErr: nil,
},
{
name: "all validators have public keys; duplicate pub keys",
stateF: func(ctrl *gomock.Controller) validators.State {
state := validatorsmock.NewState(ctrl)
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, chainID).Return(
map[ids.NodeID]*validators.GetValidatorOutput{
testVdrs[0].nodeID: {
NodeID: testVdrs[0].nodeID,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[0].vdr.PublicKey),
Weight: testVdrs[0].vdr.Weight,
},
testVdrs[1].nodeID: {
NodeID: testVdrs[1].nodeID,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[1].vdr.PublicKey),
Weight: testVdrs[1].vdr.Weight,
},
testVdrs[2].nodeID: {
NodeID: testVdrs[2].nodeID,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[0].vdr.PublicKey),
Weight: testVdrs[0].vdr.Weight,
},
},
nil,
)
return state
},
expectedVdrs: []*warp.Validator{
{
PublicKey: testVdrs[0].vdr.PublicKey,
PublicKeyBytes: testVdrs[0].vdr.PublicKeyBytes,
Weight: testVdrs[0].vdr.Weight * 2,
NodeIDs: []ids.NodeID{
testVdrs[0].nodeID,
testVdrs[2].nodeID,
},
},
testVdrs[1].vdr,
},
expectedWeight: 9,
expectedErr: nil,
},
{
name: "validator without public key; no duplicate pub keys",
stateF: func(ctrl *gomock.Controller) validators.State {
state := validatorsmock.NewState(ctrl)
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, chainID).Return(
map[ids.NodeID]*validators.GetValidatorOutput{
testVdrs[0].nodeID: {
NodeID: testVdrs[0].nodeID,
PublicKey: nil,
Weight: testVdrs[0].vdr.Weight,
},
testVdrs[1].nodeID: {
NodeID: testVdrs[1].nodeID,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[1].vdr.PublicKey),
Weight: testVdrs[1].vdr.Weight,
},
},
nil,
)
return state
},
expectedVdrs: []*warp.Validator{testVdrs[1].vdr},
expectedWeight: 6,
expectedErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
state := tt.stateF(ctrl)
// Wrap validators.State to implement ValidatorState
wrappedState := &testValidatorStateAdapter{
State: state,
}
validators, err := warp.GetCanonicalValidatorSetFromSubchainID(t.Context(), wrappedState, pChainHeight, chainID)
require.ErrorIs(err, tt.expectedErr)
if err != nil {
return
}
require.Equal(tt.expectedWeight, validators.TotalWeight)
// These are pointers so have to test equality like this
require.Len(validators.Validators, len(tt.expectedVdrs))
for i, expectedVdr := range tt.expectedVdrs {
gotVdr := validators.Validators[i]
expectedPKBytes := bls.PublicKeyToUncompressedBytes(expectedVdr.PublicKey)
gotPKBytes := bls.PublicKeyToUncompressedBytes(gotVdr.PublicKey)
require.Equal(expectedPKBytes, gotPKBytes)
require.Equal(expectedVdr.PublicKeyBytes, gotVdr.PublicKeyBytes)
require.Equal(expectedVdr.Weight, gotVdr.Weight)
require.ElementsMatch(expectedVdr.NodeIDs, gotVdr.NodeIDs)
}
})
}
}
func TestFilterValidators(t *testing.T) {
sk0, err := localsigner.New()
require.NoError(t, err)
pk0 := sk0.PublicKey()
vdr0 := &warp.Validator{
PublicKey: pk0,
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(pk0),
Weight: 1,
}
sk1, err := localsigner.New()
require.NoError(t, err)
pk1 := sk1.PublicKey()
vdr1 := &warp.Validator{
PublicKey: pk1,
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(pk1),
Weight: 2,
}
type test struct {
name string
indices set.Bits
vdrs []*warp.Validator
expectedVdrs []*warp.Validator
expectedErr error
}
tests := []test{
{
name: "empty",
indices: set.NewBits(),
vdrs: []*warp.Validator{},
expectedVdrs: []*warp.Validator{},
expectedErr: nil,
},
{
name: "unknown validator",
indices: set.NewBits(2),
vdrs: []*warp.Validator{vdr0, vdr1},
expectedErr: warp.ErrUnknownValidator,
},
{
name: "two filtered out",
indices: set.NewBits(),
vdrs: []*warp.Validator{
vdr0,
vdr1,
},
expectedVdrs: []*warp.Validator{},
expectedErr: nil,
},
{
name: "one filtered out",
indices: set.NewBits(1),
vdrs: []*warp.Validator{
vdr0,
vdr1,
},
expectedVdrs: []*warp.Validator{
vdr1,
},
expectedErr: nil,
},
{
name: "none filtered out",
indices: set.NewBits(0, 1),
vdrs: []*warp.Validator{
vdr0,
vdr1,
},
expectedVdrs: []*warp.Validator{
vdr0,
vdr1,
},
expectedErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
vdrs, err := warp.FilterValidators(tt.indices, tt.vdrs)
require.ErrorIs(err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
require.Equal(tt.expectedVdrs, vdrs)
})
}
}
func TestSumWeight(t *testing.T) {
vdr0 := &warp.Validator{
Weight: 1,
}
vdr1 := &warp.Validator{
Weight: 2,
}
vdr2 := &warp.Validator{
Weight: math.MaxUint64,
}
type test struct {
name string
vdrs []*warp.Validator
expectedSum uint64
expectedErr error
}
tests := []test{
{
name: "empty",
vdrs: []*warp.Validator{},
expectedSum: 0,
},
{
name: "one",
vdrs: []*warp.Validator{vdr0},
expectedSum: 1,
},
{
name: "two",
vdrs: []*warp.Validator{vdr0, vdr1},
expectedSum: 3,
},
{
name: "overflow",
vdrs: []*warp.Validator{vdr0, vdr2},
expectedErr: warp.ErrWeightOverflow,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
sum, err := warp.SumWeight(tt.vdrs)
require.ErrorIs(err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
require.Equal(tt.expectedSum, sum)
})
}
}
func BenchmarkGetCanonicalValidatorSet(b *testing.B) {
pChainHeight := uint64(1)
chainID := ids.GenerateTestID()
numNodes := 10_000
getValidatorOutputs := make([]*validators.GetValidatorOutput, 0, numNodes)
for i := 0; i < numNodes; i++ {
nodeID := ids.GenerateTestNodeID()
blsPrivateKey, err := localsigner.New()
require.NoError(b, err)
blsPublicKey := blsPrivateKey.PublicKey()
getValidatorOutputs = append(getValidatorOutputs, &validators.GetValidatorOutput{
NodeID: nodeID,
PublicKey: bls.PublicKeyToUncompressedBytes(blsPublicKey),
Weight: 20,
})
}
for _, size := range []int{0, 1, 10, 100, 1_000, 10_000} {
getValidatorsOutput := make(map[ids.NodeID]*validators.GetValidatorOutput)
for i := 0; i < size; i++ {
validator := getValidatorOutputs[i]
getValidatorsOutput[validator.NodeID] = validator
}
// Create a simple validator state for benchmarking
wrappedState := newMockValidatorState(
func() map[ids.NodeID]*warp.ValidatorData {
result := make(map[ids.NodeID]*warp.ValidatorData, len(getValidatorsOutput))
for nodeID, vdr := range getValidatorsOutput {
result[nodeID] = &warp.ValidatorData{
NodeID: vdr.NodeID,
PublicKey: vdr.PublicKey,
Weight: vdr.Weight,
}
}
return result
}(),
nil,
)
b.Run(strconv.Itoa(size), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := warp.GetCanonicalValidatorSetFromSubchainID(b.Context(), wrappedState, pChainHeight, chainID)
require.NoError(b, err)
}
})
}
}
// mockValidatorState is a test mock that tracks call counts
type mockValidatorState struct {
callCount int
data map[ids.NodeID]*warp.ValidatorData
err error
}
func (m *mockValidatorState) GetValidatorSet(ctx context.Context, height uint64, chainID ids.ID) (map[ids.NodeID]*warp.ValidatorData, error) {
m.callCount++
return m.data, m.err
}
func newMockValidatorState(data map[ids.NodeID]*warp.ValidatorData, err error) *mockValidatorState {
return &mockValidatorState{data: data, err: err}
}
func TestCachedValidatorState(t *testing.T) {
ctx := context.Background()
height := uint64(100)
chain1 := ids.GenerateTestID()
chain2 := ids.GenerateTestID()
// Create test validator data
nodeID1 := ids.GenerateTestNodeID()
nodeID2 := ids.GenerateTestNodeID()
testData := map[ids.NodeID]*warp.ValidatorData{
nodeID1: {
NodeID: nodeID1,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[0].vdr.PublicKey),
Weight: 100,
},
nodeID2: {
NodeID: nodeID2,
PublicKey: bls.PublicKeyToUncompressedBytes(testVdrs[1].vdr.PublicKey),
Weight: 200,
},
}
type test struct {
name string
state *mockValidatorState
upgradeConfig *upgrade.Config
networkID uint32
expectedCallCount int
operations func(*testing.T, *warp.CachedValidatorState)
}
tests := []test{
{
name: "pre-Granite no caching",
state: newMockValidatorState(testData, nil),
upgradeConfig: &upgrade.Config{GraniteTime: time.Now().Add(1 * time.Hour)},
networkID: constants.MainnetID,
expectedCallCount: 2, // Should call underlying state twice (no caching)
operations: func(t *testing.T, cached *warp.CachedValidatorState) {
vdrs1, err := cached.GetValidatorSet(ctx, height, chain1)
require.NoError(t, err)
require.Equal(t, testData, vdrs1)
vdrs2, err := cached.GetValidatorSet(ctx, height, chain1)
require.NoError(t, err)
require.Equal(t, testData, vdrs2)
},
},
{
name: "post-Granite with caching",
state: newMockValidatorState(testData, nil),
upgradeConfig: &upgrade.Config{GraniteTime: time.Now().Add(-1 * time.Hour)},
networkID: constants.MainnetID,
expectedCallCount: 1, // Should call underlying state once, then use cache
operations: func(t *testing.T, cached *warp.CachedValidatorState) {
vdrs1, err := cached.GetValidatorSet(ctx, height, chain1)
require.NoError(t, err)
require.Equal(t, testData, vdrs1)
vdrs2, err := cached.GetValidatorSet(ctx, height, chain1)
require.NoError(t, err)
require.Equal(t, testData, vdrs2)
},
},
{
name: "different heights cached separately",
state: newMockValidatorState(testData, nil),
upgradeConfig: &upgrade.Config{GraniteTime: time.Now().Add(-1 * time.Hour)},
networkID: constants.MainnetID,
expectedCallCount: 2, // Two different heights = two calls
operations: func(t *testing.T, cached *warp.CachedValidatorState) {
vdrs1, err := cached.GetValidatorSet(ctx, height, chain1)
require.NoError(t, err)
require.Equal(t, testData, vdrs1)
vdrs2, err := cached.GetValidatorSet(ctx, height+1, chain1)
require.NoError(t, err)
require.Equal(t, testData, vdrs2)
},
},
{
name: "different chains cached separately",
state: newMockValidatorState(testData, nil),
upgradeConfig: &upgrade.Config{GraniteTime: time.Now().Add(-1 * time.Hour)},
networkID: constants.MainnetID,
expectedCallCount: 2, // Two different chains = two calls
operations: func(t *testing.T, cached *warp.CachedValidatorState) {
vdrs1, err := cached.GetValidatorSet(ctx, height, chain1)
require.NoError(t, err)
require.Equal(t, testData, vdrs1)
vdrs2, err := cached.GetValidatorSet(ctx, height, chain2)
require.NoError(t, err)
require.Equal(t, testData, vdrs2)
},
},
{
name: "error propagates without caching",
state: newMockValidatorState(nil, errTest),
upgradeConfig: &upgrade.Config{GraniteTime: time.Now().Add(-1 * time.Hour)},
networkID: constants.MainnetID,
expectedCallCount: 1,
operations: func(t *testing.T, cached *warp.CachedValidatorState) {
_, err := cached.GetValidatorSet(ctx, height, chain1)
require.ErrorIs(t, err, errTest)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
registerer := metric.NewRegistry()
cached, err := warp.NewCachedValidatorState(tt.state, tt.upgradeConfig, tt.networkID, registerer)
require.NoError(err)
require.NotNil(cached)
// Run test operations
tt.operations(t, cached)
// Verify call count
require.Equal(tt.expectedCallCount, tt.state.callCount, "unexpected number of calls to underlying state")
})
}
}