github.com/MetalBlockchain/metalgo@v1.11.9/tests/fixture/tmpnet/subnet.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package tmpnet 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "time" 14 15 "github.com/MetalBlockchain/metalgo/ids" 16 "github.com/MetalBlockchain/metalgo/utils/constants" 17 "github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1" 18 "github.com/MetalBlockchain/metalgo/utils/perms" 19 "github.com/MetalBlockchain/metalgo/utils/set" 20 "github.com/MetalBlockchain/metalgo/utils/units" 21 "github.com/MetalBlockchain/metalgo/vms/platformvm" 22 "github.com/MetalBlockchain/metalgo/vms/platformvm/txs" 23 "github.com/MetalBlockchain/metalgo/vms/secp256k1fx" 24 "github.com/MetalBlockchain/metalgo/wallet/subnet/primary" 25 "github.com/MetalBlockchain/metalgo/wallet/subnet/primary/common" 26 ) 27 28 const defaultSubnetDirName = "subnets" 29 30 type Chain struct { 31 // Set statically 32 VMID ids.ID 33 Config string 34 Genesis []byte 35 36 // Set at runtime 37 ChainID ids.ID 38 PreFundedKey *secp256k1.PrivateKey 39 } 40 41 // Write the chain configuration to the specified directory. 42 func (c *Chain) WriteConfig(chainDir string) error { 43 // TODO(marun) Ensure removal of an existing file if no configuration should be provided 44 if len(c.Config) == 0 { 45 return nil 46 } 47 48 chainConfigDir := filepath.Join(chainDir, c.ChainID.String()) 49 if err := os.MkdirAll(chainConfigDir, perms.ReadWriteExecute); err != nil { 50 return fmt.Errorf("failed to create chain config dir: %w", err) 51 } 52 53 path := filepath.Join(chainConfigDir, defaultConfigFilename) 54 if err := os.WriteFile(path, []byte(c.Config), perms.ReadWrite); err != nil { 55 return fmt.Errorf("failed to write chain config: %w", err) 56 } 57 58 return nil 59 } 60 61 type Subnet struct { 62 // A unique string that can be used to refer to the subnet across different temporary 63 // networks (since the SubnetID will be different every time the subnet is created) 64 Name string 65 66 Config FlagsMap 67 68 // The ID of the transaction that created the subnet 69 SubnetID ids.ID 70 71 // The private key that owns the subnet 72 OwningKey *secp256k1.PrivateKey 73 74 // IDs of the nodes responsible for validating the subnet 75 ValidatorIDs []ids.NodeID 76 77 Chains []*Chain 78 } 79 80 // Retrieves a wallet configured for use with the subnet 81 func (s *Subnet) GetWallet(ctx context.Context, uri string) (primary.Wallet, error) { 82 keychain := secp256k1fx.NewKeychain(s.OwningKey) 83 84 // Only fetch the subnet transaction if a subnet ID is present. This won't be true when 85 // the wallet is first used to create the subnet. 86 txIDs := set.Set[ids.ID]{} 87 if s.SubnetID != ids.Empty { 88 txIDs.Add(s.SubnetID) 89 } 90 91 return primary.MakeWallet(ctx, &primary.WalletConfig{ 92 URI: uri, 93 AVAXKeychain: keychain, 94 EthKeychain: keychain, 95 PChainTxsToFetch: txIDs, 96 }) 97 } 98 99 // Issues the subnet creation transaction and retains the result. The URI of a node is 100 // required to issue the transaction. 101 func (s *Subnet) Create(ctx context.Context, uri string) error { 102 wallet, err := s.GetWallet(ctx, uri) 103 if err != nil { 104 return err 105 } 106 pWallet := wallet.P() 107 108 subnetTx, err := pWallet.IssueCreateSubnetTx( 109 &secp256k1fx.OutputOwners{ 110 Threshold: 1, 111 Addrs: []ids.ShortID{ 112 s.OwningKey.Address(), 113 }, 114 }, 115 common.WithContext(ctx), 116 ) 117 if err != nil { 118 return fmt.Errorf("failed to create subnet %s: %w", s.Name, err) 119 } 120 s.SubnetID = subnetTx.ID() 121 122 return nil 123 } 124 125 func (s *Subnet) CreateChains(ctx context.Context, w io.Writer, uri string) error { 126 wallet, err := s.GetWallet(ctx, uri) 127 if err != nil { 128 return err 129 } 130 pWallet := wallet.P() 131 132 if _, err := fmt.Fprintf(w, "Creating chains for subnet %q\n", s.Name); err != nil { 133 return err 134 } 135 136 for _, chain := range s.Chains { 137 createChainTx, err := pWallet.IssueCreateChainTx( 138 s.SubnetID, 139 chain.Genesis, 140 chain.VMID, 141 nil, 142 "", 143 common.WithContext(ctx), 144 ) 145 if err != nil { 146 return fmt.Errorf("failed to create chain: %w", err) 147 } 148 chain.ChainID = createChainTx.ID() 149 150 if _, err := fmt.Fprintf(w, " created chain %q for VM %q on subnet %q\n", chain.ChainID, chain.VMID, s.Name); err != nil { 151 return err 152 } 153 } 154 return nil 155 } 156 157 // Add validators to the subnet 158 func (s *Subnet) AddValidators(ctx context.Context, w io.Writer, apiURI string, nodes ...*Node) error { 159 wallet, err := s.GetWallet(ctx, apiURI) 160 if err != nil { 161 return err 162 } 163 pWallet := wallet.P() 164 165 // Collect the end times for current validators to reuse for subnet validators 166 pvmClient := platformvm.NewClient(apiURI) 167 validators, err := pvmClient.GetCurrentValidators(ctx, constants.PrimaryNetworkID, nil) 168 if err != nil { 169 return err 170 } 171 endTimes := make(map[ids.NodeID]uint64) 172 for _, validator := range validators { 173 endTimes[validator.NodeID] = validator.EndTime 174 } 175 176 startTime := time.Now().Add(DefaultValidatorStartTimeDiff) 177 for _, node := range nodes { 178 endTime, ok := endTimes[node.NodeID] 179 if !ok { 180 return fmt.Errorf("failed to find end time for %s", node.NodeID) 181 } 182 183 _, err := pWallet.IssueAddSubnetValidatorTx( 184 &txs.SubnetValidator{ 185 Validator: txs.Validator{ 186 NodeID: node.NodeID, 187 Start: uint64(startTime.Unix()), 188 End: endTime, 189 Wght: units.Schmeckle, 190 }, 191 Subnet: s.SubnetID, 192 }, 193 common.WithContext(ctx), 194 ) 195 if err != nil { 196 return err 197 } 198 199 if _, err := fmt.Fprintf(w, " added %s as validator for subnet `%s`\n", node.NodeID, s.Name); err != nil { 200 return err 201 } 202 } 203 204 return nil 205 } 206 207 // Write the subnet configuration to disk 208 func (s *Subnet) Write(subnetDir string, chainDir string) error { 209 if err := os.MkdirAll(subnetDir, perms.ReadWriteExecute); err != nil { 210 return fmt.Errorf("failed to create subnet dir: %w", err) 211 } 212 tmpnetConfigPath := filepath.Join(subnetDir, s.Name+".json") 213 214 // Since subnets are expected to be serialized for the first time 215 // without their chains having been created (i.e. chains will have 216 // empty IDs), use the absence of chain IDs as a prompt for a 217 // subnet name uniqueness check. 218 if len(s.Chains) > 0 && s.Chains[0].ChainID == ids.Empty { 219 _, err := os.Stat(tmpnetConfigPath) 220 if err != nil && !os.IsNotExist(err) { 221 return err 222 } 223 if err == nil { 224 return fmt.Errorf("a subnet with name %s already exists", s.Name) 225 } 226 } 227 228 // Write subnet configuration for tmpnet 229 bytes, err := DefaultJSONMarshal(s) 230 if err != nil { 231 return fmt.Errorf("failed to marshal tmpnet subnet %s: %w", s.Name, err) 232 } 233 if err := os.WriteFile(tmpnetConfigPath, bytes, perms.ReadWrite); err != nil { 234 return fmt.Errorf("failed to write tmpnet subnet config %s: %w", s.Name, err) 235 } 236 237 // The subnet and chain configurations for avalanchego can only be written once 238 // they have been created since the id of the creating transaction must be 239 // included in the path. 240 if s.SubnetID == ids.Empty { 241 return nil 242 } 243 244 // TODO(marun) Ensure removal of an existing file if no configuration should be provided 245 if len(s.Config) > 0 { 246 // Write subnet configuration for avalanchego 247 bytes, err = DefaultJSONMarshal(s.Config) 248 if err != nil { 249 return fmt.Errorf("failed to marshal avalanchego subnet config %s: %w", s.Name, err) 250 } 251 252 avgoConfigDir := filepath.Join(subnetDir, s.SubnetID.String()) 253 if err := os.MkdirAll(avgoConfigDir, perms.ReadWriteExecute); err != nil { 254 return fmt.Errorf("failed to create avalanchego subnet config dir: %w", err) 255 } 256 257 avgoConfigPath := filepath.Join(avgoConfigDir, defaultConfigFilename) 258 if err := os.WriteFile(avgoConfigPath, bytes, perms.ReadWrite); err != nil { 259 return fmt.Errorf("failed to write avalanchego subnet config %s: %w", s.Name, err) 260 } 261 } 262 263 for _, chain := range s.Chains { 264 if err := chain.WriteConfig(chainDir); err != nil { 265 return err 266 } 267 } 268 269 return nil 270 } 271 272 // HasChainConfig indicates whether at least one of the subnet's 273 // chains have explicit configuration. This can be used to determine 274 // whether validator restart is required after chain creation to 275 // ensure that chains are configured correctly. 276 func (s *Subnet) HasChainConfig() bool { 277 for _, chain := range s.Chains { 278 if len(chain.Config) > 0 { 279 return true 280 } 281 } 282 return false 283 } 284 285 func waitForActiveValidators( 286 ctx context.Context, 287 w io.Writer, 288 pChainClient platformvm.Client, 289 subnet *Subnet, 290 ) error { 291 ticker := time.NewTicker(DefaultPollingInterval) 292 defer ticker.Stop() 293 294 if _, err := fmt.Fprintf(w, "Waiting for validators of subnet %q to become active\n", subnet.Name); err != nil { 295 return err 296 } 297 298 if _, err := fmt.Fprint(w, " "); err != nil { 299 return err 300 } 301 302 for { 303 if _, err := fmt.Fprint(w, "."); err != nil { 304 return err 305 } 306 validators, err := pChainClient.GetCurrentValidators(ctx, subnet.SubnetID, nil) 307 if err != nil { 308 return err 309 } 310 validatorSet := set.NewSet[ids.NodeID](len(validators)) 311 for _, validator := range validators { 312 validatorSet.Add(validator.NodeID) 313 } 314 allActive := true 315 for _, validatorID := range subnet.ValidatorIDs { 316 if !validatorSet.Contains(validatorID) { 317 allActive = false 318 } 319 } 320 if allActive { 321 if _, err := fmt.Fprintf(w, "\n saw the expected active validators of subnet %q\n", subnet.Name); err != nil { 322 return err 323 } 324 return nil 325 } 326 327 select { 328 case <-ctx.Done(): 329 return fmt.Errorf("failed to see the expected active validators of subnet %q before timeout", subnet.Name) 330 case <-ticker.C: 331 } 332 } 333 } 334 335 // Reads subnets from [network dir]/subnets/[subnet name].json 336 func readSubnets(subnetDir string) ([]*Subnet, error) { 337 if _, err := os.Stat(subnetDir); os.IsNotExist(err) { 338 return nil, nil 339 } else if err != nil { 340 return nil, err 341 } 342 343 entries, err := os.ReadDir(subnetDir) 344 if err != nil { 345 return nil, fmt.Errorf("failed to read subnet dir: %w", err) 346 } 347 348 subnets := []*Subnet{} 349 for _, entry := range entries { 350 if entry.IsDir() { 351 // Looking only for files 352 continue 353 } 354 if filepath.Ext(entry.Name()) != ".json" { 355 // Subnet files should have a .json extension 356 continue 357 } 358 359 subnetPath := filepath.Join(subnetDir, entry.Name()) 360 bytes, err := os.ReadFile(subnetPath) 361 if err != nil { 362 return nil, fmt.Errorf("failed to read subnet file %s: %w", subnetPath, err) 363 } 364 subnet := &Subnet{} 365 if err := json.Unmarshal(bytes, subnet); err != nil { 366 return nil, fmt.Errorf("failed to unmarshal subnet from %s: %w", subnetPath, err) 367 } 368 subnets = append(subnets, subnet) 369 } 370 371 return subnets, nil 372 }