github.com/MetalBlockchain/metalgo@v1.11.9/genesis/genesis.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package genesis 5 6 import ( 7 "errors" 8 "fmt" 9 "time" 10 11 "github.com/MetalBlockchain/metalgo/ids" 12 "github.com/MetalBlockchain/metalgo/utils" 13 "github.com/MetalBlockchain/metalgo/utils/constants" 14 "github.com/MetalBlockchain/metalgo/utils/formatting" 15 "github.com/MetalBlockchain/metalgo/utils/formatting/address" 16 "github.com/MetalBlockchain/metalgo/utils/json" 17 "github.com/MetalBlockchain/metalgo/utils/set" 18 "github.com/MetalBlockchain/metalgo/vms/avm" 19 "github.com/MetalBlockchain/metalgo/vms/avm/fxs" 20 "github.com/MetalBlockchain/metalgo/vms/nftfx" 21 "github.com/MetalBlockchain/metalgo/vms/platformvm/api" 22 "github.com/MetalBlockchain/metalgo/vms/platformvm/genesis" 23 "github.com/MetalBlockchain/metalgo/vms/propertyfx" 24 "github.com/MetalBlockchain/metalgo/vms/secp256k1fx" 25 26 xchaintxs "github.com/MetalBlockchain/metalgo/vms/avm/txs" 27 pchaintxs "github.com/MetalBlockchain/metalgo/vms/platformvm/txs" 28 ) 29 30 const ( 31 defaultEncoding = formatting.Hex 32 configChainIDAlias = "X" 33 ) 34 35 var ( 36 errStakeDurationTooHigh = errors.New("initial stake duration larger than maximum configured") 37 errNoInitiallyStakedFunds = errors.New("initial staked funds cannot be empty") 38 errNoSupply = errors.New("initial supply must be > 0") 39 errNoStakeDuration = errors.New("initial stake duration must be > 0") 40 errNoStakers = errors.New("initial stakers must be > 0") 41 errNoCChainGenesis = errors.New("C-Chain genesis cannot be empty") 42 errNoTxs = errors.New("genesis creates no transactions") 43 errNoAllocationToStake = errors.New("no allocation to stake") 44 errDuplicateInitiallyStakedAddress = errors.New("duplicate initially staked address") 45 errConflictingNetworkIDs = errors.New("conflicting networkIDs") 46 errFutureStartTime = errors.New("startTime cannot be in the future") 47 errInitialStakeDurationTooLow = errors.New("initial stake duration is too low") 48 errOverridesStandardNetworkConfig = errors.New("overrides standard network genesis config") 49 ) 50 51 // validateInitialStakedFunds ensures all staked 52 // funds have allocations and that all staked 53 // funds are unique. 54 // 55 // This function assumes that NetworkID in *Config has already 56 // been checked for correctness. 57 func validateInitialStakedFunds(config *Config) error { 58 if len(config.InitialStakedFunds) == 0 { 59 return errNoInitiallyStakedFunds 60 } 61 62 allocationSet := set.Set[ids.ShortID]{} 63 initialStakedFundsSet := set.Set[ids.ShortID]{} 64 for _, allocation := range config.Allocations { 65 // It is ok to have duplicates as different 66 // ethAddrs could claim to the same avaxAddr. 67 allocationSet.Add(allocation.AVAXAddr) 68 } 69 70 for _, staker := range config.InitialStakedFunds { 71 if initialStakedFundsSet.Contains(staker) { 72 avaxAddr, err := address.Format( 73 configChainIDAlias, 74 constants.GetHRP(config.NetworkID), 75 staker.Bytes(), 76 ) 77 if err != nil { 78 return fmt.Errorf( 79 "unable to format address from %s", 80 staker.String(), 81 ) 82 } 83 84 return fmt.Errorf( 85 "%w: %s", 86 errDuplicateInitiallyStakedAddress, 87 avaxAddr, 88 ) 89 } 90 initialStakedFundsSet.Add(staker) 91 92 if !allocationSet.Contains(staker) { 93 avaxAddr, err := address.Format( 94 configChainIDAlias, 95 constants.GetHRP(config.NetworkID), 96 staker.Bytes(), 97 ) 98 if err != nil { 99 return fmt.Errorf( 100 "unable to format address from %s", 101 staker.String(), 102 ) 103 } 104 105 return fmt.Errorf( 106 "%w in address %s", 107 errNoAllocationToStake, 108 avaxAddr, 109 ) 110 } 111 } 112 113 return nil 114 } 115 116 // validateConfig returns an error if the provided 117 // *Config is not considered valid. 118 func validateConfig(networkID uint32, config *Config, stakingCfg *StakingConfig) error { 119 if networkID != config.NetworkID { 120 return fmt.Errorf( 121 "%w: expected %d but config contains %d", 122 errConflictingNetworkIDs, 123 networkID, 124 config.NetworkID, 125 ) 126 } 127 128 initialSupply, err := config.InitialSupply() 129 switch { 130 case err != nil: 131 return fmt.Errorf("unable to calculate initial supply: %w", err) 132 case initialSupply == 0: 133 return errNoSupply 134 } 135 136 startTime := time.Unix(int64(config.StartTime), 0) 137 if time.Since(startTime) < 0 { 138 return fmt.Errorf( 139 "%w: %s", 140 errFutureStartTime, 141 startTime, 142 ) 143 } 144 145 // We don't impose any restrictions on the minimum 146 // stake duration to enable complex testing configurations 147 // but recommend setting a minimum duration of at least 148 // 15 minutes. 149 if config.InitialStakeDuration == 0 { 150 return errNoStakeDuration 151 } 152 153 // Initial stake duration of genesis validators must be 154 // not larger than maximal stake duration specified for any validator. 155 if config.InitialStakeDuration > uint64(stakingCfg.MaxStakeDuration.Seconds()) { 156 return errStakeDurationTooHigh 157 } 158 159 if len(config.InitialStakers) == 0 { 160 return errNoStakers 161 } 162 163 offsetTimeRequired := config.InitialStakeDurationOffset * uint64(len(config.InitialStakers)-1) 164 if offsetTimeRequired > config.InitialStakeDuration { 165 return fmt.Errorf( 166 "%w must be at least %d", 167 errInitialStakeDurationTooLow, 168 offsetTimeRequired, 169 ) 170 } 171 172 if err := validateInitialStakedFunds(config); err != nil { 173 return fmt.Errorf("initial staked funds validation failed: %w", err) 174 } 175 176 if len(config.CChainGenesis) == 0 { 177 return errNoCChainGenesis 178 } 179 180 return nil 181 } 182 183 // FromFile returns the genesis data of the Platform Chain. 184 // 185 // Since an Avalanche network has exactly one Platform Chain, and the Platform 186 // Chain defines the genesis state of the network (who is staking, which chains 187 // exist, etc.), defining the genesis state of the Platform Chain is the same as 188 // defining the genesis state of the network. 189 // 190 // FromFile accepts: 191 // 1) The ID of the new network. [networkID] 192 // 2) The location of a custom genesis config to load. [filepath] 193 // 194 // If [filepath] is empty or the given network ID is Mainnet, Testnet, or Local, returns error. 195 // If [filepath] is non-empty and networkID isn't Mainnet, Testnet, or Local, 196 // loads the network genesis data from the config at [filepath]. 197 // 198 // FromFile returns: 199 // 200 // 1. The byte representation of the genesis state of the platform chain 201 // (ie the genesis state of the network) 202 // 2. The asset ID of AVAX 203 func FromFile(networkID uint32, filepath string, stakingCfg *StakingConfig) ([]byte, ids.ID, error) { 204 switch networkID { 205 case constants.MainnetID, constants.TahoeID, constants.LocalID: 206 return nil, ids.Empty, fmt.Errorf( 207 "%w: %s", 208 errOverridesStandardNetworkConfig, 209 constants.NetworkName(networkID), 210 ) 211 } 212 213 config, err := GetConfigFile(filepath) 214 if err != nil { 215 return nil, ids.Empty, fmt.Errorf("unable to load provided genesis config at %s: %w", filepath, err) 216 } 217 218 if err := validateConfig(networkID, config, stakingCfg); err != nil { 219 return nil, ids.Empty, fmt.Errorf("genesis config validation failed: %w", err) 220 } 221 222 return FromConfig(config) 223 } 224 225 // FromFlag returns the genesis data of the Platform Chain. 226 // 227 // Since an Avalanche network has exactly one Platform Chain, and the Platform 228 // Chain defines the genesis state of the network (who is staking, which chains 229 // exist, etc.), defining the genesis state of the Platform Chain is the same as 230 // defining the genesis state of the network. 231 // 232 // FromFlag accepts: 233 // 1) The ID of the new network. [networkID] 234 // 2) The content of a custom genesis config to load. [genesisContent] 235 // 236 // If [genesisContent] is empty or the given network ID is Mainnet, Testnet, or Local, returns error. 237 // If [genesisContent] is non-empty and networkID isn't Mainnet, Testnet, or Local, 238 // loads the network genesis data from [genesisContent]. 239 // 240 // FromFlag returns: 241 // 242 // 1. The byte representation of the genesis state of the platform chain 243 // (ie the genesis state of the network) 244 // 2. The asset ID of AVAX 245 func FromFlag(networkID uint32, genesisContent string, stakingCfg *StakingConfig) ([]byte, ids.ID, error) { 246 switch networkID { 247 case constants.MainnetID, constants.TahoeID, constants.LocalID: 248 return nil, ids.Empty, fmt.Errorf( 249 "%w: %s", 250 errOverridesStandardNetworkConfig, 251 constants.NetworkName(networkID), 252 ) 253 } 254 255 customConfig, err := GetConfigContent(genesisContent) 256 if err != nil { 257 return nil, ids.Empty, fmt.Errorf("unable to load genesis content from flag: %w", err) 258 } 259 260 if err := validateConfig(networkID, customConfig, stakingCfg); err != nil { 261 return nil, ids.Empty, fmt.Errorf("genesis config validation failed: %w", err) 262 } 263 264 return FromConfig(customConfig) 265 } 266 267 // FromConfig returns: 268 // 269 // 1. The byte representation of the genesis state of the platform chain 270 // (ie the genesis state of the network) 271 // 2. The asset ID of AVAX 272 func FromConfig(config *Config) ([]byte, ids.ID, error) { 273 hrp := constants.GetHRP(config.NetworkID) 274 275 amount := uint64(0) 276 277 // Specify the genesis state of the AVM 278 avmArgs := avm.BuildGenesisArgs{ 279 NetworkID: json.Uint32(config.NetworkID), 280 Encoding: defaultEncoding, 281 } 282 { 283 avax := avm.AssetDefinition{ 284 Name: "Metal", 285 Symbol: "METAL", 286 Denomination: 9, 287 InitialState: map[string][]interface{}{}, 288 } 289 memoBytes := []byte{} 290 xAllocations := []Allocation(nil) 291 for _, allocation := range config.Allocations { 292 if allocation.InitialAmount > 0 { 293 xAllocations = append(xAllocations, allocation) 294 } 295 } 296 utils.Sort(xAllocations) 297 298 for _, allocation := range xAllocations { 299 addr, err := address.FormatBech32(hrp, allocation.AVAXAddr.Bytes()) 300 if err != nil { 301 return nil, ids.Empty, err 302 } 303 304 avax.InitialState["fixedCap"] = append(avax.InitialState["fixedCap"], avm.Holder{ 305 Amount: json.Uint64(allocation.InitialAmount), 306 Address: addr, 307 }) 308 memoBytes = append(memoBytes, allocation.ETHAddr.Bytes()...) 309 amount += allocation.InitialAmount 310 } 311 312 var err error 313 avax.Memo, err = formatting.Encode(defaultEncoding, memoBytes) 314 if err != nil { 315 return nil, ids.Empty, fmt.Errorf("couldn't parse memo bytes to string: %w", err) 316 } 317 avmArgs.GenesisData = map[string]avm.AssetDefinition{ 318 "METAL": avax, // The AVM starts out with one asset: AVAX 319 } 320 } 321 avmReply := avm.BuildGenesisReply{} 322 323 avmSS := avm.CreateStaticService() 324 err := avmSS.BuildGenesis(nil, &avmArgs, &avmReply) 325 if err != nil { 326 return nil, ids.Empty, err 327 } 328 329 bytes, err := formatting.Decode(defaultEncoding, avmReply.Bytes) 330 if err != nil { 331 return nil, ids.Empty, fmt.Errorf("couldn't parse avm genesis reply: %w", err) 332 } 333 avaxAssetID, err := AVAXAssetID(bytes) 334 if err != nil { 335 return nil, ids.Empty, fmt.Errorf("couldn't generate AVAX asset ID: %w", err) 336 } 337 338 genesisTime := time.Unix(int64(config.StartTime), 0) 339 initialSupply, err := config.InitialSupply() 340 if err != nil { 341 return nil, ids.Empty, fmt.Errorf("couldn't calculate the initial supply: %w", err) 342 } 343 344 initiallyStaked := set.Of(config.InitialStakedFunds...) 345 skippedAllocations := []Allocation(nil) 346 347 // Specify the initial state of the Platform Chain 348 platformvmArgs := api.BuildGenesisArgs{ 349 AvaxAssetID: avaxAssetID, 350 NetworkID: json.Uint32(config.NetworkID), 351 Time: json.Uint64(config.StartTime), 352 InitialSupply: json.Uint64(initialSupply), 353 Message: config.Message, 354 Encoding: defaultEncoding, 355 } 356 for _, allocation := range config.Allocations { 357 if initiallyStaked.Contains(allocation.AVAXAddr) { 358 skippedAllocations = append(skippedAllocations, allocation) 359 continue 360 } 361 addr, err := address.FormatBech32(hrp, allocation.AVAXAddr.Bytes()) 362 if err != nil { 363 return nil, ids.Empty, err 364 } 365 for _, unlock := range allocation.UnlockSchedule { 366 if unlock.Amount > 0 { 367 msgStr, err := formatting.Encode(defaultEncoding, allocation.ETHAddr.Bytes()) 368 if err != nil { 369 return nil, ids.Empty, fmt.Errorf("couldn't encode message: %w", err) 370 } 371 platformvmArgs.UTXOs = append(platformvmArgs.UTXOs, 372 api.UTXO{ 373 Locktime: json.Uint64(unlock.Locktime), 374 Amount: json.Uint64(unlock.Amount), 375 Address: addr, 376 Message: msgStr, 377 }, 378 ) 379 amount += unlock.Amount 380 } 381 } 382 } 383 384 allNodeAllocations := splitAllocations(skippedAllocations, len(config.InitialStakers)) 385 endStakingTime := genesisTime.Add(time.Duration(config.InitialStakeDuration) * time.Second) 386 stakingOffset := time.Duration(0) 387 for i, staker := range config.InitialStakers { 388 nodeAllocations := allNodeAllocations[i] 389 endStakingTime := endStakingTime.Add(-stakingOffset) 390 stakingOffset += time.Duration(config.InitialStakeDurationOffset) * time.Second 391 392 destAddrStr, err := address.FormatBech32(hrp, staker.RewardAddress.Bytes()) 393 if err != nil { 394 return nil, ids.Empty, err 395 } 396 397 utxos := []api.UTXO(nil) 398 for _, allocation := range nodeAllocations { 399 addr, err := address.FormatBech32(hrp, allocation.AVAXAddr.Bytes()) 400 if err != nil { 401 return nil, ids.Empty, err 402 } 403 for _, unlock := range allocation.UnlockSchedule { 404 msgStr, err := formatting.Encode(defaultEncoding, allocation.ETHAddr.Bytes()) 405 if err != nil { 406 return nil, ids.Empty, fmt.Errorf("couldn't encode message: %w", err) 407 } 408 utxos = append(utxos, api.UTXO{ 409 Locktime: json.Uint64(unlock.Locktime), 410 Amount: json.Uint64(unlock.Amount), 411 Address: addr, 412 Message: msgStr, 413 }) 414 amount += unlock.Amount 415 } 416 } 417 418 delegationFee := json.Uint32(staker.DelegationFee) 419 420 platformvmArgs.Validators = append(platformvmArgs.Validators, 421 api.GenesisPermissionlessValidator{ 422 GenesisValidator: api.GenesisValidator{ 423 StartTime: json.Uint64(genesisTime.Unix()), 424 EndTime: json.Uint64(endStakingTime.Unix()), 425 NodeID: staker.NodeID, 426 }, 427 RewardOwner: &api.Owner{ 428 Threshold: 1, 429 Addresses: []string{destAddrStr}, 430 }, 431 Staked: utxos, 432 ExactDelegationFee: &delegationFee, 433 Signer: staker.Signer, 434 }, 435 ) 436 } 437 438 // Specify the chains that exist upon this network's creation 439 genesisStr, err := formatting.Encode(defaultEncoding, []byte(config.CChainGenesis)) 440 if err != nil { 441 return nil, ids.Empty, fmt.Errorf("couldn't encode message: %w", err) 442 } 443 platformvmArgs.Chains = []api.Chain{ 444 { 445 GenesisData: avmReply.Bytes, 446 SubnetID: constants.PrimaryNetworkID, 447 VMID: constants.AVMID, 448 FxIDs: []ids.ID{ 449 secp256k1fx.ID, 450 nftfx.ID, 451 propertyfx.ID, 452 }, 453 Name: "X-Chain", 454 }, 455 { 456 GenesisData: genesisStr, 457 SubnetID: constants.PrimaryNetworkID, 458 VMID: constants.EVMID, 459 Name: "C-Chain", 460 }, 461 } 462 463 platformvmReply := api.BuildGenesisReply{} 464 platformvmSS := api.StaticService{} 465 if err := platformvmSS.BuildGenesis(nil, &platformvmArgs, &platformvmReply); err != nil { 466 return nil, ids.Empty, fmt.Errorf("problem while building platform chain's genesis state: %w", err) 467 } 468 469 genesisBytes, err := formatting.Decode(platformvmReply.Encoding, platformvmReply.Bytes) 470 if err != nil { 471 return nil, ids.Empty, fmt.Errorf("problem parsing platformvm genesis bytes: %w", err) 472 } 473 474 return genesisBytes, avaxAssetID, nil 475 } 476 477 func splitAllocations(allocations []Allocation, numSplits int) [][]Allocation { 478 totalAmount := uint64(0) 479 for _, allocation := range allocations { 480 for _, unlock := range allocation.UnlockSchedule { 481 totalAmount += unlock.Amount 482 } 483 } 484 485 nodeWeight := totalAmount / uint64(numSplits) 486 allNodeAllocations := make([][]Allocation, 0, numSplits) 487 488 currentNodeAllocation := []Allocation(nil) 489 currentNodeAmount := uint64(0) 490 for _, allocation := range allocations { 491 currentAllocation := allocation 492 // Already added to the X-chain 493 currentAllocation.InitialAmount = 0 494 // Going to be added until the correct amount is reached 495 currentAllocation.UnlockSchedule = nil 496 497 for _, unlock := range allocation.UnlockSchedule { 498 unlock := unlock 499 for currentNodeAmount+unlock.Amount > nodeWeight && len(allNodeAllocations) < numSplits-1 { 500 amountToAdd := nodeWeight - currentNodeAmount 501 currentAllocation.UnlockSchedule = append(currentAllocation.UnlockSchedule, LockedAmount{ 502 Amount: amountToAdd, 503 Locktime: unlock.Locktime, 504 }) 505 unlock.Amount -= amountToAdd 506 507 currentNodeAllocation = append(currentNodeAllocation, currentAllocation) 508 509 allNodeAllocations = append(allNodeAllocations, currentNodeAllocation) 510 511 currentNodeAllocation = nil 512 currentNodeAmount = 0 513 514 currentAllocation = allocation 515 // Already added to the X-chain 516 currentAllocation.InitialAmount = 0 517 // Going to be added until the correct amount is reached 518 currentAllocation.UnlockSchedule = nil 519 } 520 521 if unlock.Amount == 0 { 522 continue 523 } 524 525 currentAllocation.UnlockSchedule = append(currentAllocation.UnlockSchedule, LockedAmount{ 526 Amount: unlock.Amount, 527 Locktime: unlock.Locktime, 528 }) 529 currentNodeAmount += unlock.Amount 530 } 531 532 if len(currentAllocation.UnlockSchedule) > 0 { 533 currentNodeAllocation = append(currentNodeAllocation, currentAllocation) 534 } 535 } 536 537 return append(allNodeAllocations, currentNodeAllocation) 538 } 539 540 func VMGenesis(genesisBytes []byte, vmID ids.ID) (*pchaintxs.Tx, error) { 541 genesis, err := genesis.Parse(genesisBytes) 542 if err != nil { 543 return nil, fmt.Errorf("failed to parse genesis: %w", err) 544 } 545 for _, chain := range genesis.Chains { 546 uChain := chain.Unsigned.(*pchaintxs.CreateChainTx) 547 if uChain.VMID == vmID { 548 return chain, nil 549 } 550 } 551 return nil, fmt.Errorf("couldn't find blockchain with VM ID %s", vmID) 552 } 553 554 func AVAXAssetID(avmGenesisBytes []byte) (ids.ID, error) { 555 parser, err := xchaintxs.NewParser( 556 []fxs.Fx{ 557 &secp256k1fx.Fx{}, 558 }, 559 ) 560 if err != nil { 561 return ids.Empty, err 562 } 563 564 genesisCodec := parser.GenesisCodec() 565 genesis := avm.Genesis{} 566 if _, err := genesisCodec.Unmarshal(avmGenesisBytes, &genesis); err != nil { 567 return ids.Empty, err 568 } 569 570 if len(genesis.Txs) == 0 { 571 return ids.Empty, errNoTxs 572 } 573 genesisTx := genesis.Txs[0] 574 575 tx := xchaintxs.Tx{Unsigned: &genesisTx.CreateAssetTx} 576 if err := tx.Initialize(genesisCodec); err != nil { 577 return ids.Empty, err 578 } 579 return tx.ID(), nil 580 }