Subnet Testing
Test subnet creation, validators, and cross-subnet interactions with tmpnet
This guide covers advanced subnet testing scenarios using tmpnet, including subnet creation, validator management, and testing cross-subnet functionality.
Overview
tmpnet supports comprehensive subnet testing:
- Create subnets with specific validators
- Test validator operations (add/remove)
- Configure subnet parameters
- Test cross-subnet messaging with Warp
- Validate L1 conversions
Creating a Subnet with Specific Validators
Basic Example
Create a subnet validated by specific nodes:
import (
"context"
"os"
"time"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/tests/fixture/tmpnet"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/logging"
)
// Create 5-node network
network := &tmpnet.Network{
Nodes: tmpnet.NewNodesOrPanic(5),
DefaultRuntimeConfig: tmpnet.NodeRuntimeConfig{
Process: &tmpnet.ProcessRuntimeConfig{
AvalancheGoPath: os.Getenv("AVALANCHEGO_PATH"),
PluginDir: os.Getenv("AVAGO_PLUGIN_DIR"),
},
},
}
// Subnet validated by first 3 nodes only
subnet := &tmpnet.Subnet{
Name: "my-subnet",
ValidatorIDs: []ids.NodeID{
network.Nodes[0].NodeID,
network.Nodes[1].NodeID,
network.Nodes[2].NodeID,
},
Chains: []*tmpnet.Chain{{
VMID: constants.XSVMID,
Genesis: genesisBytes,
}},
}
network.Subnets = []*tmpnet.Subnet{subnet}
// Bootstrap
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
err := tmpnet.BootstrapNewNetwork(ctx, logging.NoLog{}, network, "")
if err != nil {
panic(err)
}
println("Subnet ID:", subnet.SubnetID.String())Subnet Configuration
Customize subnet parameters:
subnet.Config = tmpnet.ConfigMap{
"proposerNumHistoricalBlocks": 50000, // Historical ProposerVM blocks to index
}Testing Multiple Subnets
Create overlapping and isolated subnets:
nodes := tmpnet.NewNodesOrPanic(7)
// Subnet A: nodes 0-2
subnetA := &tmpnet.Subnet{
Name: "subnet-a",
ValidatorIDs: []ids.NodeID{nodes[0].NodeID, nodes[1].NodeID, nodes[2].NodeID},
Chains: []*tmpnet.Chain{chainA},
}
// Subnet B: nodes 2-4 (node 2 validates both A and B)
subnetB := &tmpnet.Subnet{
Name: "subnet-b",
ValidatorIDs: []ids.NodeID{nodes[2].NodeID, nodes[3].NodeID, nodes[4].NodeID},
Chains: []*tmpnet.Chain{chainB},
}
// Subnet C: nodes 5-6 (isolated)
subnetC := &tmpnet.Subnet{
Name: "subnet-c",
ValidatorIDs: []ids.NodeID{nodes[5].NodeID, nodes[6].NodeID},
Chains: []*tmpnet.Chain{chainC},
}
network := &tmpnet.Network{
Nodes: nodes,
Subnets: []*tmpnet.Subnet{subnetA, subnetB, subnetC},
}This lets you test:
- Shared validators (node 2 validates both A and B)
- Isolated subnets (subnet C)
- Cross-subnet messaging via shared validators
Adding Validators to a Running Subnet
Test adding validators dynamically to an existing subnet:
func addValidatorToSubnet(network *tmpnet.Network, subnet *tmpnet.Subnet) error {
// Create a new ephemeral node
newNode := tmpnet.NewEphemeralNode(tmpnet.FlagsMap{
config.TrackSubnetsKey: subnet.SubnetID.String(),
})
// Start the node
err := network.StartNode(context.Background(), newNode)
if err != nil {
return err
}
// Add as subnet validator using the subnet wallet.
// AddSubnetValidatorTx requires the node to be a Primary Network validator.
// The subnet validation period must be inside its Primary Network validation period.
// (Implementation details depend on your wallet setup)
err = addSubnetValidator(subnet, newNode.NodeID)
if err != nil {
return err
}
// Wait for the validator to become active.
// WaitForActiveValidators waits for all IDs in subnet.ValidatorIDs.
subnet.ValidatorIDs = append(subnet.ValidatorIDs, newNode.NodeID)
pChainClient := platformvm.NewClient(newNode.URI)
return tmpnet.WaitForActiveValidators(context.Background(), logging.NoLog{}, pChainClient, subnet)
}Testing Subnet-to-L1 Conversion
Test converting a subnet to an L1 blockchain:
func testL1Conversion(t *testing.T) {
// Create initial subnet
network := createNetworkWithSubnet()
defer network.Stop(context.Background())
subnet := network.Subnets[0]
// Perform L1 conversion operations
// 1. Convert the subnet to an L1. ConvertSubnetToL1Tx sets the initial validators.
initialNodes, additionalNodes := network.Nodes[:3], network.Nodes[3:]
err := convertSubnetToL1(subnet, initialNodes)
require.NoError(t, err)
// 2. Register more L1 validators. RegisterL1ValidatorTx is valid only after the conversion.
for _, node := range additionalNodes {
err := registerL1Validator(subnet, node)
require.NoError(t, err)
}
// 3. Wait for validators to activate
err = waitForL1Validators(subnet)
require.NoError(t, err)
// 4. Verify L1 functionality
verifyL1Behavior(t, subnet)
}Cross-Subnet Messaging
Test Avalanche Warp Messaging between subnets:
func testWarpMessaging(t *testing.T) {
// Create network with two subnets
network := createMultiSubnetNetwork()
defer network.Stop(context.Background())
sourceSubnet := network.Subnets[0]
destSubnet := network.Subnets[1]
// Send a Warp message from source to destination
message := createWarpMessage(sourceSubnet)
// Get signatures from source subnet validators
signatures := collectWarpSignatures(sourceSubnet, message)
// Submit message to destination subnet
err := submitWarpMessage(destSubnet, message, signatures)
require.NoError(t, err)
// Verify message was received and processed
verifyWarpMessage(t, destSubnet, message)
}Subnet Validator Lifecycle Testing
Test the complete validator lifecycle on a subnet:
func TestSubnetValidatorLifecycle(t *testing.T) {
network := setupNetwork(t)
defer network.Stop(context.Background())
subnet := network.Subnets[0]
// Create a new node to add as validator
node := tmpnet.NewEphemeralNode(tmpnet.FlagsMap{
config.TrackSubnetsKey: subnet.SubnetID.String(),
})
// Start the node
err := network.StartNode(context.Background(), node)
require.NoError(t, err)
// Add as validator. Since Durango, the validation starts when the P-Chain accepts the tx.
// The P-Chain ignores startTime.
t.Run("AddValidator", func(t *testing.T) {
err := addSubnetValidator(subnet, node.NodeID, startTime, endTime, weight)
require.NoError(t, err)
})
// Wait for the P-Chain to list the validator as current
t.Run("WaitForActive", func(t *testing.T) {
subnet.ValidatorIDs = append(subnet.ValidatorIDs, node.NodeID)
pChainClient := platformvm.NewClient(node.URI)
err := tmpnet.WaitForActiveValidators(context.Background(), logging.NoLog{}, pChainClient, subnet)
require.NoError(t, err)
})
// Verify validator is active
t.Run("VerifyActive", func(t *testing.T) {
active := isValidatorActive(subnet, node.NodeID)
require.True(t, active)
})
// Remove validator
t.Run("RemoveValidator", func(t *testing.T) {
err := removeSubnetValidator(subnet, node.NodeID)
require.NoError(t, err)
})
// Verify validator is removed
t.Run("VerifyRemoved", func(t *testing.T) {
active := isValidatorActive(subnet, node.NodeID)
require.False(t, active)
})
}Testing Subnet Configuration Changes
Test how subnet configuration changes affect behavior:
func testSubnetConfigUpdate(t *testing.T) {
// Initial configuration
subnet := &tmpnet.Subnet{
Name: "configurable-subnet",
Config: tmpnet.ConfigMap{
"validatorOnly": false,
},
Chains: []*tmpnet.Chain{chain},
ValidatorIDs: validatorIDs,
}
network := &tmpnet.Network{
Nodes: nodes,
Subnets: []*tmpnet.Subnet{subnet},
DefaultRuntimeConfig: tmpnet.NodeRuntimeConfig{
Process: &tmpnet.ProcessRuntimeConfig{
AvalancheGoPath: avalanchegoPath,
PluginDir: pluginDir,
},
},
}
// Bootstrap network (the second argument is a logging.Logger)
require.NoError(t, tmpnet.BootstrapNewNetwork(ctx, logging.NoLog{}, network, ""))
// Test behavior with initial config: a non-validator can sync the chain
verifyNonValidatorSync(t, network, subnet, true)
// Update configuration: only subnet validators get chain messages
subnet.Config["validatorOnly"] = true
// Restart the running nodes to apply the new configuration
require.NoError(t, network.Restart(context.Background()))
// Test behavior with updated config
verifyNonValidatorSync(t, network, subnet, false)
}Tracking Specific Subnets
Configure nodes to track specific subnets for testing:
// Configure network to track subnet
network.DefaultFlags = tmpnet.FlagsMap{
config.TrackSubnetsKey: subnetID.String(),
}
// Or configure individual nodes
node.Flags = tmpnet.FlagsMap{
config.TrackSubnetsKey: fmt.Sprintf("%s,%s", subnet1.String(), subnet2.String()),
}Testing Subnet Validator Weights
Test different validator weight distributions:
func testValidatorWeights(t *testing.T) {
network := setupNetwork(t)
// Add validators with different weights
validators := []struct {
nodeID ids.NodeID
weight uint64
}{
{network.Nodes[0].NodeID, 100}, // 50% of total weight
{network.Nodes[1].NodeID, 50}, // 25% of total weight
{network.Nodes[2].NodeID, 30}, // 15% of total weight
{network.Nodes[3].NodeID, 20}, // 10% of total weight
}
for _, v := range validators {
err := addSubnetValidator(subnet, v.nodeID, startTime, endTime, v.weight)
require.NoError(t, err)
}
// Test consensus with weighted validators
testConsensusWithWeights(t, subnet, validators)
}Ephemeral Subnet Validators
Add temporary validators for specific test scenarios:
func addEphemeralValidator(network *tmpnet.Network, subnet *tmpnet.Subnet) (*tmpnet.Node, error) {
// Create ephemeral node that tracks the subnet
ephemeralNode := tmpnet.NewEphemeralNode(tmpnet.FlagsMap{
config.TrackSubnetsKey: subnet.SubnetID.String(),
})
// Add to network (tmpnet.Network has no AddEphemeralNode method)
err := network.StartNode(context.Background(), ephemeralNode)
if err != nil {
return nil, err
}
// Make the node a Primary Network validator first.
// AddSubnetValidatorTx requires it.
// ...
// Add as subnet validator with short duration
shortDuration := 5 * time.Minute
err = addSubnetValidator(
subnet,
ephemeralNode.NodeID,
time.Now(),
time.Now().Add(shortDuration),
20, // weight
)
return ephemeralNode, err
}Common Testing Patterns
Testing Subnet Bootstrap
Verify that nodes can bootstrap from a subnet:
func testSubnetBootstrap(t *testing.T) {
// Create and bootstrap network with subnet
network := createNetworkWithSubnet()
defer network.Stop(context.Background())
// Create a new node
newNode := tmpnet.NewNode()
newNode.Flags = tmpnet.FlagsMap{
config.TrackSubnetsKey: subnet.SubnetID.String(),
}
// Start the node
err := network.StartNode(context.Background(), newNode)
require.NoError(t, err)
// Verify the node bootstrapped the subnet
verifySubnetBootstrap(t, newNode, subnet)
}Testing Subnet Chain Upgrades
Test deploying chain upgrades on a subnet:
func testChainUpgrade(t *testing.T) {
network := setupNetworkWithSubnet(t)
// Deploy initial chain version
// ... operate chain ...
// Stop network
network.Stop(context.Background())
// Update chain configuration or VM binary
updateChainConfig(network.Subnets[0])
// Start the stopped nodes again. Network.Restart restarts only running nodes.
err := network.StartNodes(context.Background(), logging.NoLog{}, network.Nodes...)
require.NoError(t, err)
// Verify upgrade succeeded
verifyChainUpgrade(t, network.Subnets[0])
}Troubleshooting
Subnet Creation Fails
Check:
- Sufficient nodes are specified as validators (minimum 1)
- Nodes have generated staking keys
- Bootstrap node has sufficient funds for transactions
Debug:
# Check subnet creation logs
# NETWORK_DIR is network.Dir. BootstrapNewNetwork does not update the latest symlink.
grep -i "subnet" "$NETWORK_DIR"/NodeID-*/logs/main.logValidators Not Becoming Active
Check:
- The node is a Primary Network validator, and its Primary Network validation period covers the validation period in the
AddSubnetValidatorTx - Nodes are tracking the subnet
- Subnet validators were added correctly
Debug:
# Check if node is tracking subnet
jq '.["track-subnets"]' "$NETWORK_DIR"/NodeID-*/flags.jsonCross-Subnet Messaging Issues
Check:
- Both subnets have active validators
- Nodes validating both subnets have proper connectivity
- Warp messaging is enabled
Next Steps
Runtime Environments
Choose between local and Kubernetes runtimes
Monitoring
Monitor subnet behavior with metrics
Configuration Reference
Detailed configuration options
Additional Resources
Is this guide helpful?