diff --git a/chains/manager.go b/chains/manager.go index 48f2fee35704..3c016fe7c18f 100644 --- a/chains/manager.go +++ b/chains/manager.go @@ -97,7 +97,6 @@ var ( errUnknownVMType = errors.New("the vm should have type avalanche.DAGVM or snowman.ChainVM") errCreatePlatformVM = errors.New("attempted to create a chain running the PlatformVM") errNotBootstrapped = errors.New("subnets not bootstrapped") - errNoPrimaryNetworkConfig = errors.New("no subnet config for primary network found") errPartialSyncAsAValidator = errors.New("partial sync should not be configured for a validator") fxs = map[ids.ID]fx.Factory{ @@ -233,6 +232,8 @@ type ManagerConfig struct { StateSyncBeacons []ids.NodeID ChainDataDir string + + Subnets *Subnets } type manager struct { @@ -256,11 +257,6 @@ type manager struct { chainCreatorShutdownCh chan struct{} chainCreatorExited sync.WaitGroup - subnetsLock sync.RWMutex - // Key: Subnet's ID - // Value: Subnet description - subnets map[ids.ID]subnets.Subnet - chainsLock sync.Mutex // Key: Chain's ID // Value: The chain @@ -277,7 +273,6 @@ func New(config *ManagerConfig) Manager { ManagerConfig: *config, stakingSigner: config.StakingTLSCert.PrivateKey.(crypto.Signer), stakingCert: staking.CertificateFromX509(config.StakingTLSCert.Leaf), - subnets: make(map[ids.ID]subnets.Subnet), chains: make(map[ids.ID]handler.Handler), chainsQueue: buffer.NewUnboundedBlockingDeque[ChainParameters](initialQueueSize), unblockChainCreatorCh: make(chan struct{}), @@ -288,25 +283,10 @@ func New(config *ManagerConfig) Manager { // QueueChainCreation queues a chain creation request // Invariant: Tracked Subnet must be checked before calling this function func (m *manager) QueueChainCreation(chainParams ChainParameters) { - m.subnetsLock.Lock() - subnetID := chainParams.SubnetID - sb, exists := m.subnets[subnetID] - if !exists { - sbConfig, ok := m.SubnetConfigs[subnetID] - if !ok { - // default to primary subnet config - sbConfig = m.SubnetConfigs[constants.PrimaryNetworkID] - } - sb = subnets.New(m.NodeID, sbConfig) - m.subnets[chainParams.SubnetID] = sb - } - addedChain := sb.AddChain(chainParams.ID) - m.subnetsLock.Unlock() - - if !addedChain { + if sb, _ := m.Subnets.GetOrCreate(chainParams.SubnetID); !sb.AddChain(chainParams.ID) { m.Log.Debug("skipping chain creation", zap.String("reason", "chain already staged"), - zap.Stringer("subnetID", subnetID), + zap.Stringer("subnetID", chainParams.SubnetID), zap.Stringer("chainID", chainParams.ID), zap.Stringer("vmID", chainParams.VMID), ) @@ -316,7 +296,7 @@ func (m *manager) QueueChainCreation(chainParams ChainParameters) { if ok := m.chainsQueue.PushRight(chainParams); !ok { m.Log.Warn("skipping chain creation", zap.String("reason", "couldn't enqueue chain"), - zap.Stringer("subnetID", subnetID), + zap.Stringer("subnetID", chainParams.SubnetID), zap.Stringer("chainID", chainParams.ID), zap.Stringer("vmID", chainParams.VMID), ) @@ -334,9 +314,7 @@ func (m *manager) createChain(chainParams ChainParameters) { zap.Stringer("vmID", chainParams.VMID), ) - m.subnetsLock.RLock() - sb := m.subnets[chainParams.SubnetID] - m.subnetsLock.RUnlock() + sb, _ := m.Subnets.GetOrCreate(chainParams.SubnetID) // Note: buildChain builds all chain's relevant objects (notably engine and handler) // but does not start their operations. Starting of the handler (which could potentially @@ -1307,23 +1285,9 @@ func (m *manager) IsBootstrapped(id ids.ID) bool { return chain.Context().State.Get().State == snow.NormalOp } -func (m *manager) subnetsNotBootstrapped() []ids.ID { - m.subnetsLock.RLock() - defer m.subnetsLock.RUnlock() - - subnetsBootstrapping := make([]ids.ID, 0, len(m.subnets)) - for subnetID, subnet := range m.subnets { - if !subnet.IsBootstrapped() { - subnetsBootstrapping = append(subnetsBootstrapping, subnetID) - } - } - return subnetsBootstrapping -} - func (m *manager) registerBootstrappedHealthChecks() error { bootstrappedCheck := health.CheckerFunc(func(context.Context) (interface{}, error) { - subnetIDs := m.subnetsNotBootstrapped() - if len(subnetIDs) != 0 { + if subnetIDs := m.Subnets.Bootstrapping(); len(subnetIDs) != 0 { return subnetIDs, errNotBootstrapped } return []ids.ID{}, nil @@ -1365,18 +1329,9 @@ func (m *manager) registerBootstrappedHealthChecks() error { // Starts chain creation loop to process queued chains func (m *manager) StartChainCreator(platformParams ChainParameters) error { - // Get the Primary Network's subnet config. If it wasn't registered, then we - // throw a fatal error. - sbConfig, ok := m.SubnetConfigs[constants.PrimaryNetworkID] - if !ok { - return errNoPrimaryNetworkConfig - } - - sb := subnets.New(m.NodeID, sbConfig) - m.subnetsLock.Lock() - m.subnets[platformParams.SubnetID] = sb + // Add the P-Chain to the Primary Network + sb, _ := m.Subnets.GetOrCreate(constants.PrimaryNetworkID) sb.AddChain(platformParams.ID) - m.subnetsLock.Unlock() // The P-chain is created synchronously to ensure that `VM.Initialize` has // finished before returning from this function. This is required because diff --git a/chains/subnets.go b/chains/subnets.go new file mode 100644 index 000000000000..fda66a7eded2 --- /dev/null +++ b/chains/subnets.go @@ -0,0 +1,82 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chains + +import ( + "errors" + "sync" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/subnets" + "github.com/ava-labs/avalanchego/utils/constants" +) + +var ErrNoPrimaryNetworkConfig = errors.New("no subnet config for primary network found") + +// Subnets holds the currently running subnets on this node +type Subnets struct { + nodeID ids.NodeID + configs map[ids.ID]subnets.Config + + lock sync.RWMutex + subnets map[ids.ID]subnets.Subnet +} + +// GetOrCreate returns a subnet running on this node, or creates one if it was +// not running before. Returns the subnet and if the subnet was created. +func (s *Subnets) GetOrCreate(subnetID ids.ID) (subnets.Subnet, bool) { + s.lock.Lock() + defer s.lock.Unlock() + + if subnet, ok := s.subnets[subnetID]; ok { + return subnet, false + } + + // Default to the primary network config if a subnet config was not + // specified + config, ok := s.configs[subnetID] + if !ok { + config = s.configs[constants.PrimaryNetworkID] + } + + subnet := subnets.New(s.nodeID, config) + s.subnets[subnetID] = subnet + + return subnet, true +} + +// Bootstrapping returns the subnetIDs of any chains that are still +// bootstrapping. +func (s *Subnets) Bootstrapping() []ids.ID { + s.lock.RLock() + defer s.lock.RUnlock() + + subnetsBootstrapping := make([]ids.ID, 0, len(s.subnets)) + for subnetID, subnet := range s.subnets { + if !subnet.IsBootstrapped() { + subnetsBootstrapping = append(subnetsBootstrapping, subnetID) + } + } + + return subnetsBootstrapping +} + +// NewSubnets returns an instance of Subnets +func NewSubnets( + nodeID ids.NodeID, + configs map[ids.ID]subnets.Config, +) (*Subnets, error) { + if _, ok := configs[constants.PrimaryNetworkID]; !ok { + return nil, ErrNoPrimaryNetworkConfig + } + + s := &Subnets{ + nodeID: nodeID, + configs: configs, + subnets: make(map[ids.ID]subnets.Subnet), + } + + _, _ = s.GetOrCreate(constants.PrimaryNetworkID) + return s, nil +} diff --git a/chains/subnets_test.go b/chains/subnets_test.go new file mode 100644 index 000000000000..231a8f970a15 --- /dev/null +++ b/chains/subnets_test.go @@ -0,0 +1,173 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package chains + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/subnets" + "github.com/ava-labs/avalanchego/utils/constants" +) + +func TestNewSubnets(t *testing.T) { + require := require.New(t) + config := map[ids.ID]subnets.Config{ + constants.PrimaryNetworkID: {}, + } + + subnets, err := NewSubnets(ids.EmptyNodeID, config) + require.NoError(err) + + subnet, ok := subnets.GetOrCreate(constants.PrimaryNetworkID) + require.False(ok) + require.Equal(config[constants.PrimaryNetworkID], subnet.Config()) +} + +func TestNewSubnetsNoPrimaryNetworkConfig(t *testing.T) { + require := require.New(t) + config := map[ids.ID]subnets.Config{} + + _, err := NewSubnets(ids.EmptyNodeID, config) + require.ErrorIs(err, ErrNoPrimaryNetworkConfig) +} + +func TestSubnetsGetOrCreate(t *testing.T) { + testSubnetID := ids.GenerateTestID() + + type args struct { + subnetID ids.ID + want bool + } + + tests := []struct { + name string + args []args + }{ + { + name: "adding duplicate subnet is a noop", + args: []args{ + { + subnetID: testSubnetID, + want: true, + }, + { + subnetID: testSubnetID, + }, + }, + }, + { + name: "adding unique subnets succeeds", + args: []args{ + { + subnetID: ids.GenerateTestID(), + want: true, + }, + { + subnetID: ids.GenerateTestID(), + want: true, + }, + { + subnetID: ids.GenerateTestID(), + want: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + config := map[ids.ID]subnets.Config{ + constants.PrimaryNetworkID: {}, + } + subnets, err := NewSubnets(ids.EmptyNodeID, config) + require.NoError(err) + + for _, arg := range tt.args { + _, got := subnets.GetOrCreate(arg.subnetID) + require.Equal(arg.want, got) + } + }) + } +} + +func TestSubnetConfigs(t *testing.T) { + testSubnetID := ids.GenerateTestID() + + tests := []struct { + name string + config map[ids.ID]subnets.Config + subnetID ids.ID + want subnets.Config + }{ + { + name: "default to primary network config", + config: map[ids.ID]subnets.Config{ + constants.PrimaryNetworkID: {}, + }, + subnetID: testSubnetID, + want: subnets.Config{}, + }, + { + name: "use subnet config", + config: map[ids.ID]subnets.Config{ + constants.PrimaryNetworkID: {}, + testSubnetID: { + GossipConfig: subnets.GossipConfig{ + AcceptedFrontierValidatorSize: 123456789, + }, + }, + }, + subnetID: testSubnetID, + want: subnets.Config{ + GossipConfig: subnets.GossipConfig{ + AcceptedFrontierValidatorSize: 123456789, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + subnets, err := NewSubnets(ids.EmptyNodeID, tt.config) + require.NoError(err) + + subnet, ok := subnets.GetOrCreate(tt.subnetID) + require.True(ok) + + require.Equal(tt.want, subnet.Config()) + }) + } +} + +func TestSubnetsBootstrapping(t *testing.T) { + require := require.New(t) + + config := map[ids.ID]subnets.Config{ + constants.PrimaryNetworkID: {}, + } + + subnets, err := NewSubnets(ids.EmptyNodeID, config) + require.NoError(err) + + subnetID := ids.GenerateTestID() + chainID := ids.GenerateTestID() + + subnet, ok := subnets.GetOrCreate(subnetID) + require.True(ok) + + // Start bootstrapping + subnet.AddChain(chainID) + bootstrapping := subnets.Bootstrapping() + require.Contains(bootstrapping, subnetID) + + // Finish bootstrapping + subnet.Bootstrapped(chainID) + require.Empty(subnets.Bootstrapping()) +} diff --git a/node/node.go b/node/node.go index d7ef070fd434..437f505a5e0a 100644 --- a/node/node.go +++ b/node/node.go @@ -1117,51 +1117,58 @@ func (n *Node) initChainManager(avaxAssetID ids.ID) error { return fmt.Errorf("couldn't initialize chain router: %w", err) } - n.chainManager = chains.New(&chains.ManagerConfig{ - SybilProtectionEnabled: n.Config.SybilProtectionEnabled, - StakingTLSCert: n.Config.StakingTLSCert, - StakingBLSKey: n.Config.StakingSigningKey, - Log: n.Log, - LogFactory: n.LogFactory, - VMManager: n.VMManager, - BlockAcceptorGroup: n.BlockAcceptorGroup, - TxAcceptorGroup: n.TxAcceptorGroup, - VertexAcceptorGroup: n.VertexAcceptorGroup, - DB: n.DB, - MsgCreator: n.msgCreator, - Router: n.chainRouter, - Net: n.Net, - Validators: n.vdrs, - PartialSyncPrimaryNetwork: n.Config.PartialSyncPrimaryNetwork, - NodeID: n.ID, - NetworkID: n.Config.NetworkID, - Server: n.APIServer, - Keystore: n.keystore, - AtomicMemory: n.sharedMemory, - AVAXAssetID: avaxAssetID, - XChainID: xChainID, - CChainID: cChainID, - CriticalChains: criticalChains, - TimeoutManager: n.timeoutManager, - Health: n.health, - ShutdownNodeFunc: n.Shutdown, - MeterVMEnabled: n.Config.MeterVMEnabled, - Metrics: n.MetricsGatherer, - SubnetConfigs: n.Config.SubnetConfigs, - ChainConfigs: n.Config.ChainConfigs, - FrontierPollFrequency: n.Config.FrontierPollFrequency, - ConsensusAppConcurrency: n.Config.ConsensusAppConcurrency, - BootstrapMaxTimeGetAncestors: n.Config.BootstrapMaxTimeGetAncestors, - BootstrapAncestorsMaxContainersSent: n.Config.BootstrapAncestorsMaxContainersSent, - BootstrapAncestorsMaxContainersReceived: n.Config.BootstrapAncestorsMaxContainersReceived, - ApricotPhase4Time: version.GetApricotPhase4Time(n.Config.NetworkID), - ApricotPhase4MinPChainHeight: version.ApricotPhase4MinPChainHeight[n.Config.NetworkID], - ResourceTracker: n.resourceTracker, - StateSyncBeacons: n.Config.StateSyncIDs, - TracingEnabled: n.Config.TraceConfig.Enabled, - Tracer: n.tracer, - ChainDataDir: n.Config.ChainDataDir, - }) + subnets, err := chains.NewSubnets(n.ID, n.Config.SubnetConfigs) + if err != nil { + return fmt.Errorf("failed to initialize subnets: %w", err) + } + n.chainManager = chains.New( + &chains.ManagerConfig{ + SybilProtectionEnabled: n.Config.SybilProtectionEnabled, + StakingTLSCert: n.Config.StakingTLSCert, + StakingBLSKey: n.Config.StakingSigningKey, + Log: n.Log, + LogFactory: n.LogFactory, + VMManager: n.VMManager, + BlockAcceptorGroup: n.BlockAcceptorGroup, + TxAcceptorGroup: n.TxAcceptorGroup, + VertexAcceptorGroup: n.VertexAcceptorGroup, + DB: n.DB, + MsgCreator: n.msgCreator, + Router: n.chainRouter, + Net: n.Net, + Validators: n.vdrs, + PartialSyncPrimaryNetwork: n.Config.PartialSyncPrimaryNetwork, + NodeID: n.ID, + NetworkID: n.Config.NetworkID, + Server: n.APIServer, + Keystore: n.keystore, + AtomicMemory: n.sharedMemory, + AVAXAssetID: avaxAssetID, + XChainID: xChainID, + CChainID: cChainID, + CriticalChains: criticalChains, + TimeoutManager: n.timeoutManager, + Health: n.health, + ShutdownNodeFunc: n.Shutdown, + MeterVMEnabled: n.Config.MeterVMEnabled, + Metrics: n.MetricsGatherer, + SubnetConfigs: n.Config.SubnetConfigs, + ChainConfigs: n.Config.ChainConfigs, + FrontierPollFrequency: n.Config.FrontierPollFrequency, + ConsensusAppConcurrency: n.Config.ConsensusAppConcurrency, + BootstrapMaxTimeGetAncestors: n.Config.BootstrapMaxTimeGetAncestors, + BootstrapAncestorsMaxContainersSent: n.Config.BootstrapAncestorsMaxContainersSent, + BootstrapAncestorsMaxContainersReceived: n.Config.BootstrapAncestorsMaxContainersReceived, + ApricotPhase4Time: version.GetApricotPhase4Time(n.Config.NetworkID), + ApricotPhase4MinPChainHeight: version.ApricotPhase4MinPChainHeight[n.Config.NetworkID], + ResourceTracker: n.resourceTracker, + StateSyncBeacons: n.Config.StateSyncIDs, + TracingEnabled: n.Config.TraceConfig.Enabled, + Tracer: n.tracer, + ChainDataDir: n.Config.ChainDataDir, + Subnets: subnets, + }, + ) // Notify the API server when new chains are created n.chainManager.AddRegistrant(n.APIServer)