Files
evm/x/warp/predicate_test.go
2024-01-03 09:50:41 +01:00

695 lines
20 KiB
Go

// (c) 2023-2024, Lux Partners Limited. All rights reserved.
// See the file LICENSE for licensing terms.
package warp
import (
"context"
"errors"
"fmt"
"testing"
"github.com/luxdefi/node/ids"
"github.com/luxdefi/node/snow"
"github.com/luxdefi/node/snow/engine/snowman/block"
"github.com/luxdefi/node/snow/validators"
agoUtils "github.com/luxdefi/node/utils"
"github.com/luxdefi/node/utils/constants"
"github.com/luxdefi/node/utils/crypto/bls"
"github.com/luxdefi/node/utils/set"
luxWarp "github.com/luxdefi/node/vms/platformvm/warp"
"github.com/luxdefi/node/vms/platformvm/warp/payload"
"github.com/luxdefi/evm/params"
"github.com/luxdefi/evm/precompile/precompileconfig"
"github.com/luxdefi/evm/precompile/testutils"
"github.com/luxdefi/evm/predicate"
<<<<<<< HEAD
"github.com/luxdefi/evm/utils"
=======
subnetEVMUtils "github.com/luxdefi/evm/utils"
>>>>>>> fd08c47 (Update import path)
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
const pChainHeight uint64 = 1337
var (
_ agoUtils.Sortable[*testValidator] = (*testValidator)(nil)
errTest = errors.New("non-nil error")
networkID = uint32(54321)
sourceChainID = ids.GenerateTestID()
sourceSubnetID = ids.GenerateTestID()
// valid unsigned warp message used throughout testing
unsignedMsg *luxWarp.UnsignedMessage
// valid addressed payload
addressedPayload *payload.AddressedCall
addressedPayloadBytes []byte
// blsSignatures of [unsignedMsg] from each of [testVdrs]
blsSignatures []*bls.Signature
numTestVdrs = 10_000
testVdrs []*testValidator
vdrs map[ids.NodeID]*validators.GetValidatorOutput
tests []signatureTest
predicateTests = make(map[string]testutils.PredicateTest)
)
func init() {
testVdrs = make([]*testValidator, 0, numTestVdrs)
for i := 0; i < numTestVdrs; i++ {
testVdrs = append(testVdrs, newTestValidator())
}
agoUtils.Sort(testVdrs)
vdrs = map[ids.NodeID]*validators.GetValidatorOutput{
testVdrs[0].nodeID: {
NodeID: testVdrs[0].nodeID,
PublicKey: testVdrs[0].vdr.PublicKey,
Weight: testVdrs[0].vdr.Weight,
},
testVdrs[1].nodeID: {
NodeID: testVdrs[1].nodeID,
PublicKey: testVdrs[1].vdr.PublicKey,
Weight: testVdrs[1].vdr.Weight,
},
testVdrs[2].nodeID: {
NodeID: testVdrs[2].nodeID,
PublicKey: testVdrs[2].vdr.PublicKey,
Weight: testVdrs[2].vdr.Weight,
},
}
var err error
addr := ids.GenerateTestShortID()
addressedPayload, err = payload.NewAddressedCall(
addr[:],
[]byte{1, 2, 3},
)
if err != nil {
panic(err)
}
addressedPayloadBytes = addressedPayload.Bytes()
unsignedMsg, err = luxWarp.NewUnsignedMessage(networkID, sourceChainID, addressedPayload.Bytes())
if err != nil {
panic(err)
}
for _, testVdr := range testVdrs {
blsSignature := bls.Sign(testVdr.sk, unsignedMsg.Bytes())
blsSignatures = append(blsSignatures, blsSignature)
}
initWarpPredicateTests()
}
type testValidator struct {
nodeID ids.NodeID
sk *bls.SecretKey
vdr *luxWarp.Validator
}
func (v *testValidator) Less(o *testValidator) bool {
return v.vdr.Less(o.vdr)
}
func newTestValidator() *testValidator {
sk, err := bls.NewSecretKey()
if err != nil {
panic(err)
}
nodeID := ids.GenerateTestNodeID()
pk := bls.PublicFromSecretKey(sk)
return &testValidator{
nodeID: nodeID,
sk: sk,
vdr: &luxWarp.Validator{
PublicKey: pk,
PublicKeyBytes: pk.Serialize(),
Weight: 3,
NodeIDs: []ids.NodeID{nodeID},
},
}
}
type signatureTest struct {
name string
stateF func(*gomock.Controller) validators.State
quorumNum uint64
quorumDen uint64
msgF func(*require.Assertions) *luxWarp.Message
err error
}
// createWarpMessage constructs a signed warp message using the global variable [unsignedMsg]
// and the first [numKeys] signatures from [blsSignatures]
func createWarpMessage(numKeys int) *luxWarp.Message {
aggregateSignature, err := bls.AggregateSignatures(blsSignatures[0:numKeys])
if err != nil {
panic(err)
}
bitSet := set.NewBits()
for i := 0; i < numKeys; i++ {
bitSet.Add(i)
}
warpSignature := &luxWarp.BitSetSignature{
Signers: bitSet.Bytes(),
}
copy(warpSignature.Signature[:], bls.SignatureToBytes(aggregateSignature))
warpMsg, err := luxWarp.NewMessage(unsignedMsg, warpSignature)
if err != nil {
panic(err)
}
return warpMsg
}
// createPredicate constructs a warp message using createWarpMessage with numKeys signers
// and packs it into predicate encoding.
func createPredicate(numKeys int) []byte {
warpMsg := createWarpMessage(numKeys)
predicateBytes := predicate.PackPredicate(warpMsg.Bytes())
return predicateBytes
}
// validatorRange specifies a range of validators to include from [start, end), a staking weight
// to specify for each validator in that range, and whether or not to include the public key.
type validatorRange struct {
start int
end int
weight uint64
publicKey bool
}
// createSnowCtx creates a snow.Context instance with a validator state specified by the given validatorRanges
func createSnowCtx(validatorRanges []validatorRange) *snow.Context {
getValidatorsOutput := make(map[ids.NodeID]*validators.GetValidatorOutput)
for _, validatorRange := range validatorRanges {
for i := validatorRange.start; i < validatorRange.end; i++ {
validatorOutput := &validators.GetValidatorOutput{
NodeID: testVdrs[i].nodeID,
Weight: validatorRange.weight,
}
if validatorRange.publicKey {
validatorOutput.PublicKey = testVdrs[i].vdr.PublicKey
}
getValidatorsOutput[testVdrs[i].nodeID] = validatorOutput
}
}
snowCtx := utils.TestSnowContext()
state := &validators.TestState{
GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) {
return sourceSubnetID, nil
},
GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return getValidatorsOutput, nil
},
}
snowCtx.ValidatorState = state
snowCtx.NetworkID = networkID
return snowCtx
}
func createValidPredicateTest(snowCtx *snow.Context, numKeys uint64, predicateBytes []byte) testutils.PredicateTest {
return testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + numKeys*GasCostPerWarpSigner,
GasErr: nil,
ExpectedErr: nil,
}
}
func TestWarpMessageFromPrimaryNetwork(t *testing.T) {
require := require.New(t)
numKeys := 10
cChainID := ids.GenerateTestID()
addressedCall, err := payload.NewAddressedCall(agoUtils.RandomBytes(20), agoUtils.RandomBytes(100))
require.NoError(err)
unsignedMsg, err := luxWarp.NewUnsignedMessage(networkID, cChainID, addressedCall.Bytes())
require.NoError(err)
getValidatorsOutput := make(map[ids.NodeID]*validators.GetValidatorOutput)
blsSignatures := make([]*bls.Signature, 0, numKeys)
for i := 0; i < numKeys; i++ {
validatorOutput := &validators.GetValidatorOutput{
NodeID: testVdrs[i].nodeID,
Weight: 20,
PublicKey: testVdrs[i].vdr.PublicKey,
}
getValidatorsOutput[testVdrs[i].nodeID] = validatorOutput
blsSignatures = append(blsSignatures, bls.Sign(testVdrs[i].sk, unsignedMsg.Bytes()))
}
aggregateSignature, err := bls.AggregateSignatures(blsSignatures)
require.NoError(err)
bitSet := set.NewBits()
for i := 0; i < numKeys; i++ {
bitSet.Add(i)
}
warpSignature := &luxWarp.BitSetSignature{
Signers: bitSet.Bytes(),
}
copy(warpSignature.Signature[:], bls.SignatureToBytes(aggregateSignature))
warpMsg, err := luxWarp.NewMessage(unsignedMsg, warpSignature)
require.NoError(err)
predicateBytes := predicate.PackPredicate(warpMsg.Bytes())
snowCtx := utils.TestSnowContext()
snowCtx.SubnetID = ids.GenerateTestID()
snowCtx.ChainID = ids.GenerateTestID()
snowCtx.CChainID = cChainID
snowCtx.NetworkID = networkID
snowCtx.ValidatorState = &validators.TestState{
GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) {
require.Equal(chainID, cChainID)
return constants.PrimaryNetworkID, nil // Return Primary Network SubnetID
},
GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
require.Equal(snowCtx.SubnetID, subnetID)
return getValidatorsOutput, nil
},
}
test := testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner,
GasErr: nil,
ExpectedErr: nil,
}
test.Run(t)
}
func TestInvalidPredicatePacking(t *testing.T) {
numKeys := 1
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: numKeys,
weight: 20,
publicKey: true,
},
})
predicateBytes := createPredicate(numKeys)
predicateBytes = append(predicateBytes, byte(0x01)) // Invalidate the predicate byte packing
test := testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner,
GasErr: errInvalidPredicateBytes,
}
test.Run(t)
}
func TestInvalidWarpMessage(t *testing.T) {
numKeys := 1
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: numKeys,
weight: 20,
publicKey: true,
},
})
warpMsg := createWarpMessage(1)
warpMsgBytes := warpMsg.Bytes()
warpMsgBytes = append(warpMsgBytes, byte(0x01)) // Invalidate warp message packing
predicateBytes := predicate.PackPredicate(warpMsgBytes)
test := testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner,
GasErr: errInvalidWarpMsg,
}
test.Run(t)
}
func TestInvalidAddressedPayload(t *testing.T) {
numKeys := 1
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: numKeys,
weight: 20,
publicKey: true,
},
})
aggregateSignature, err := bls.AggregateSignatures(blsSignatures[0:numKeys])
require.NoError(t, err)
bitSet := set.NewBits()
for i := 0; i < numKeys; i++ {
bitSet.Add(i)
}
warpSignature := &luxWarp.BitSetSignature{
Signers: bitSet.Bytes(),
}
copy(warpSignature.Signature[:], bls.SignatureToBytes(aggregateSignature))
// Create an unsigned message with an invalid addressed payload
unsignedMsg, err := luxWarp.NewUnsignedMessage(networkID, sourceChainID, []byte{1, 2, 3})
require.NoError(t, err)
warpMsg, err := luxWarp.NewMessage(unsignedMsg, warpSignature)
require.NoError(t, err)
warpMsgBytes := warpMsg.Bytes()
predicateBytes := predicate.PackPredicate(warpMsgBytes)
test := testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner,
GasErr: errInvalidWarpMsgPayload,
}
test.Run(t)
}
func TestInvalidBitSet(t *testing.T) {
addressedCall, err := payload.NewAddressedCall(agoUtils.RandomBytes(20), agoUtils.RandomBytes(100))
require.NoError(t, err)
unsignedMsg, err := luxWarp.NewUnsignedMessage(
networkID,
sourceChainID,
addressedCall.Bytes(),
)
require.NoError(t, err)
msg, err := luxWarp.NewMessage(
unsignedMsg,
&luxWarp.BitSetSignature{
Signers: make([]byte, 1),
Signature: [bls.SignatureLen]byte{},
},
)
require.NoError(t, err)
numKeys := 1
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: numKeys,
weight: 20,
publicKey: true,
},
})
predicateBytes := predicate.PackPredicate(msg.Bytes())
test := testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numKeys)*GasCostPerWarpSigner,
GasErr: errCannotGetNumSigners,
}
test.Run(t)
}
func TestWarpSignatureWeightsDefaultQuorumNumerator(t *testing.T) {
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: 100,
weight: 20,
publicKey: true,
},
})
tests := make(map[string]testutils.PredicateTest)
for _, numSigners := range []int{
1,
int(params.WarpDefaultQuorumNumerator) - 1,
int(params.WarpDefaultQuorumNumerator),
int(params.WarpDefaultQuorumNumerator) + 1,
int(params.WarpQuorumDenominator) - 1,
int(params.WarpQuorumDenominator),
int(params.WarpQuorumDenominator) + 1,
} {
predicateBytes := createPredicate(numSigners)
// The predicate is valid iff the number of signers is >= the required numerator and does not exceed the denominator.
var expectedErr error
if numSigners >= int(params.WarpDefaultQuorumNumerator) && numSigners <= int(params.WarpQuorumDenominator) {
expectedErr = nil
} else {
expectedErr = errFailedVerification
}
tests[fmt.Sprintf("default quorum %d signature(s)", numSigners)] = testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numSigners)*GasCostPerWarpSigner,
GasErr: nil,
ExpectedErr: expectedErr,
}
}
testutils.RunPredicateTests(t, tests)
}
// multiple messages all correct, multiple messages all incorrect, mixed bag
func TestWarpMultiplePredicates(t *testing.T) {
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: 100,
weight: 20,
publicKey: true,
},
})
tests := make(map[string]testutils.PredicateTest)
for _, validMessageIndices := range [][]bool{
{},
{true, false},
{false, true},
{false, false},
{true, true},
} {
var (
numSigners = int(params.WarpQuorumDenominator)
invalidPredicateBytes = createPredicate(1)
validPredicateBytes = createPredicate(numSigners)
)
for _, valid := range validMessageIndices {
var (
predicate []byte
expectedGas uint64
expectedErr error
)
if valid {
predicate = validPredicateBytes
expectedGas = GasCostPerSignatureVerification + uint64(len(validPredicateBytes))*GasCostPerWarpMessageBytes + uint64(numSigners)*GasCostPerWarpSigner
expectedErr = nil
} else {
expectedGas = GasCostPerSignatureVerification + uint64(len(invalidPredicateBytes))*GasCostPerWarpMessageBytes + uint64(1)*GasCostPerWarpSigner
predicate = invalidPredicateBytes
expectedErr = errFailedVerification
}
tests[fmt.Sprintf("multiple predicates %v", validMessageIndices)] = testutils.PredicateTest{
Config: NewDefaultConfig(utils.NewUint64(0)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicate,
Gas: expectedGas,
GasErr: nil,
ExpectedErr: expectedErr,
}
}
}
testutils.RunPredicateTests(t, tests)
}
func TestWarpSignatureWeightsNonDefaultQuorumNumerator(t *testing.T) {
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: 100,
weight: 20,
publicKey: true,
},
})
tests := make(map[string]testutils.PredicateTest)
nonDefaultQuorumNumerator := 50
// Ensure this test fails if the DefaultQuroumNumerator is changed to an unexpected value during development
require.NotEqual(t, nonDefaultQuorumNumerator, int(params.WarpDefaultQuorumNumerator))
// Add cases with default quorum
for _, numSigners := range []int{nonDefaultQuorumNumerator, nonDefaultQuorumNumerator + 1, 99, 100, 101} {
predicateBytes := createPredicate(numSigners)
// The predicate is valid iff the number of signers is >= the required numerator and does not exceed the denominator.
var expectedErr error
if numSigners >= nonDefaultQuorumNumerator && numSigners <= int(params.WarpQuorumDenominator) {
expectedErr = nil
} else {
expectedErr = errFailedVerification
}
name := fmt.Sprintf("non-default quorum %d signature(s)", numSigners)
tests[name] = testutils.PredicateTest{
Config: NewConfig(utils.NewUint64(0), uint64(nonDefaultQuorumNumerator)),
PredicateContext: &precompileconfig.PredicateContext{
SnowCtx: snowCtx,
ProposerVMBlockCtx: &block.Context{
PChainHeight: 1,
},
},
PredicateBytes: predicateBytes,
Gas: GasCostPerSignatureVerification + uint64(len(predicateBytes))*GasCostPerWarpMessageBytes + uint64(numSigners)*GasCostPerWarpSigner,
GasErr: nil,
ExpectedErr: expectedErr,
}
}
testutils.RunPredicateTests(t, tests)
}
func initWarpPredicateTests() {
for _, totalNodes := range []int{10, 100, 1_000, 10_000} {
testName := fmt.Sprintf("%d signers/%d validators", totalNodes, totalNodes)
predicateBytes := createPredicate(totalNodes)
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: totalNodes,
weight: 20,
publicKey: true,
},
})
predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(totalNodes), predicateBytes)
}
numSigners := 10
for _, totalNodes := range []int{100, 1_000, 10_000} {
testName := fmt.Sprintf("%d signers (heavily weighted)/%d validators", numSigners, totalNodes)
predicateBytes := createPredicate(numSigners)
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: numSigners,
weight: 10_000_000,
publicKey: true,
},
{
start: numSigners,
end: totalNodes,
weight: 20,
publicKey: true,
},
})
predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(numSigners), predicateBytes)
}
for _, totalNodes := range []int{100, 1_000, 10_000} {
testName := fmt.Sprintf("%d signers (heavily weighted)/%d validators (non-signers without registered PublicKey)", numSigners, totalNodes)
predicateBytes := createPredicate(numSigners)
snowCtx := createSnowCtx([]validatorRange{
{
start: 0,
end: numSigners,
weight: 10_000_000,
publicKey: true,
},
{
start: numSigners,
end: totalNodes,
weight: 20,
publicKey: false,
},
})
predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(numSigners), predicateBytes)
}
for _, totalNodes := range []int{100, 1_000, 10_000} {
testName := fmt.Sprintf("%d validators w/ %d signers/repeated PublicKeys", totalNodes, numSigners)
predicateBytes := createPredicate(numSigners)
getValidatorsOutput := make(map[ids.NodeID]*validators.GetValidatorOutput, totalNodes)
for i := 0; i < totalNodes; i++ {
getValidatorsOutput[testVdrs[i].nodeID] = &validators.GetValidatorOutput{
NodeID: testVdrs[i].nodeID,
Weight: 20,
PublicKey: testVdrs[i%numSigners].vdr.PublicKey,
}
}
snowCtx := utils.TestSnowContext()
snowCtx.NetworkID = networkID
state := &validators.TestState{
GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) {
return sourceSubnetID, nil
},
GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return getValidatorsOutput, nil
},
}
snowCtx.ValidatorState = state
predicateTests[testName] = createValidPredicateTest(snowCtx, uint64(numSigners), predicateBytes)
}
}
func TestWarpPredicate(t *testing.T) {
testutils.RunPredicateTests(t, predicateTests)
}
func BenchmarkWarpPredicate(b *testing.B) {
testutils.RunPredicateBenchmarks(b, predicateTests)
}