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  }