diff --git a/cmd/blockchaincmd/deploy.go b/cmd/blockchaincmd/deploy.go index d0d8925da..e23765f18 100644 --- a/cmd/blockchaincmd/deploy.go +++ b/cmd/blockchaincmd/deploy.go @@ -12,6 +12,8 @@ import ( "strings" "time" + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" + "github.com/ava-labs/avalanchego/api/info" "github.com/ava-labs/avalanchego/network/peer" @@ -484,7 +486,7 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { } if sidecar.Sovereign { - if !convertOnly && !generateNodeID { + if !generateNodeID { clusterName := fmt.Sprintf("%s-local-node", blockchainName) if globalNetworkFlags.ClusterName != "" { clusterName = globalNetworkFlags.ClusterName @@ -873,17 +875,35 @@ func deployBlockchain(cmd *cobra.Command, args []string) error { return err } ux.Logger.PrintToUser("Initializing Proof of Authority Validator Manager contract on blockchain %s ...", blockchainName) - if err := validatormanager.SetupPoA( + subnetID, err := contract.GetSubnetID( app, network, - rpcURL, chainSpec, - genesisPrivateKey, - common.HexToAddress(sidecar.PoAValidatorManagerOwner), - avaGoBootstrapValidators, - extraAggregatorPeers, - aggregatorLogLevel, - ); err != nil { + ) + if err != nil { + return err + } + blockchainID, err := contract.GetBlockchainID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + ownerAddress := common.HexToAddress(sidecar.PoAValidatorManagerOwner) + subnetSDK := blockchainSDK.Subnet{ + SubnetID: subnetID, + BlockchainID: blockchainID, + OwnerAddress: &ownerAddress, + RPC: rpcURL, + BootstrapValidators: avaGoBootstrapValidators, + } + logLvl, err := logging.ToLevel(aggregatorLogLevel) + if err != nil { + logLvl = logging.Off + } + if err := subnetSDK.InitializeProofOfAuthority(network, genesisPrivateKey, extraAggregatorPeers, logLvl); err != nil { return err } ux.Logger.GreenCheckmarkToUser("Proof of Authority Validator Manager contract successfully initialized on blockchain %s", blockchainName) @@ -1214,17 +1234,7 @@ func UrisToPeers(uris []string) ([]info.Peer, error) { return peers, nil } -func GetAggregatorExtraPeers( - network models.Network, - extraURIs []string, -) ([]info.Peer, error) { - uris, err := GetAggregatorNetworkUris(network) - if err != nil { - return nil, err - } - uris = append(uris, extraURIs...) - urisSet := set.Of(uris...) - uris = urisSet.List() +func ConvertURIToPeers(uris []string) ([]info.Peer, error) { aggregatorPeers, err := UrisToPeers(uris) if err != nil { return nil, err @@ -1251,6 +1261,20 @@ func GetAggregatorExtraPeers( return aggregatorPeers, nil } +func GetAggregatorExtraPeers( + network models.Network, + extraURIs []string, +) ([]info.Peer, error) { + uris, err := GetAggregatorNetworkUris(network) + if err != nil { + return nil, err + } + uris = append(uris, extraURIs...) + urisSet := set.Of(uris...) + uris = urisSet.List() + return ConvertURIToPeers(uris) +} + func GetAggregatorNetworkUris(network models.Network) ([]string, error) { aggregatorExtraPeerEndpointsUris := []string{} if network.ClusterName != "" { diff --git a/cmd/contractcmd/init_poa_validator_manager.go b/cmd/contractcmd/init_poa_validator_manager.go index 026d32d29..90bd7c36f 100644 --- a/cmd/contractcmd/init_poa_validator_manager.go +++ b/cmd/contractcmd/init_poa_validator_manager.go @@ -12,10 +12,10 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/prompts" "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanche-cli/pkg/validatormanager" + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ethereum/go-ethereum/common" - "github.com/spf13/cobra" ) @@ -124,14 +124,34 @@ func initPOAManager(_ *cobra.Command, args []string) error { if err != nil { return err } - if err := validatormanager.SetupPoA( + subnetID, err := contract.GetSubnetID( + app, + network, + chainSpec, + ) + if err != nil { + return err + } + blockchainID, err := contract.GetBlockchainID( app, network, - initPOAManagerFlags.rpcEndpoint, chainSpec, + ) + if err != nil { + return err + } + ownerAddress := common.HexToAddress(sc.PoAValidatorManagerOwner) + subnetSDK := blockchainSDK.Subnet{ + SubnetID: subnetID, + BlockchainID: blockchainID, + BootstrapValidators: avaGoBootstrapValidators, + OwnerAddress: &ownerAddress, + RPC: initPOAManagerFlags.rpcEndpoint, + } + if err := validatormanager.SetupPoA( + subnetSDK, + network, privateKey, - common.HexToAddress(sc.PoAValidatorManagerOwner), - avaGoBootstrapValidators, extraAggregatorPeers, initPOAManagerFlags.aggregatorLogLevel, ); err != nil { diff --git a/pkg/ux/spinner.go b/pkg/ux/spinner.go index 5c5e28a34..bd35b11f6 100644 --- a/pkg/ux/spinner.go +++ b/pkg/ux/spinner.go @@ -44,7 +44,9 @@ func (us *UserSpinner) Stop() { func (us *UserSpinner) SpinToUser(msg string, args ...interface{}) *ysmrr.Spinner { formattedMsg := fmt.Sprintf(msg, args...) - Logger.log.Info(formattedMsg + " [Spinner Start]") + if Logger != nil { + Logger.log.Info(formattedMsg + " [Spinner Start]") + } sp := us.spinner.AddSpinner(formattedMsg) us.mutex.Lock() if !us.started { @@ -71,5 +73,7 @@ func SpinComplete(s *ysmrr.Spinner) { return } s.Complete() - Logger.log.Info(s.GetMessage() + " [Spinner Complete]") + if Logger != nil { + Logger.log.Info(s.GetMessage() + " [Spinner Complete]") + } } diff --git a/pkg/validatormanager/validatormanager.go b/pkg/validatormanager/validatormanager.go index 19cc40013..4fb25c103 100644 --- a/pkg/validatormanager/validatormanager.go +++ b/pkg/validatormanager/validatormanager.go @@ -9,22 +9,11 @@ import ( "math/big" "strings" - "github.com/ava-labs/avalanche-cli/pkg/application" - "github.com/ava-labs/avalanche-cli/pkg/contract" - "github.com/ava-labs/avalanche-cli/pkg/evm" "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/ux" - "github.com/ava-labs/avalanche-cli/sdk/interchain" + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/ids" - avagoconstants "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" - warp "github.com/ava-labs/avalanchego/vms/platformvm/warp" - warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" - warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/subnet-evm/core" - "github.com/ava-labs/subnet-evm/core/types" "github.com/ethereum/go-ethereum/common" ) @@ -84,241 +73,21 @@ func AddPoAValidatorManagerContractToAllocations( } } -// initializes contract [managerAddress] at [rpcURL], to -// manage validators on [subnetID], with -// owner given by [ownerAddress] -func PoAValidatorManagerInitialize( - rpcURL string, - managerAddress common.Address, - privateKey string, - subnetID ids.ID, - ownerAddress common.Address, -) (*types.Transaction, *types.Receipt, error) { - const ( - defaultChurnPeriodSeconds = uint64(0) - defaultMaximumChurnPercentage = uint8(20) - ) - type Params struct { - SubnetID [32]byte - ChurnPeriodSeconds uint64 - MaximumChurnPercentage uint8 - } - params := Params{ - SubnetID: subnetID, - ChurnPeriodSeconds: defaultChurnPeriodSeconds, - MaximumChurnPercentage: defaultMaximumChurnPercentage, - } - return contract.TxToMethod( - rpcURL, - privateKey, - managerAddress, - nil, - "initialize PoA manager", - errorSignatureToError, - "initialize((bytes32,uint64,uint8),address)", - params, - ownerAddress, - ) -} - -// constructs p-chain-validated (signed) subnet conversion warp -// message, to be sent to the validators manager when -// initializing validators set -// the message specifies [subnetID] that is being converted -// together with the validator's manager [managerBlockchainID], -// [managerAddress], and the initial list of [validators] -func PoaValidatorManagerGetPChainSubnetConversionWarpMessage( - network models.Network, - aggregatorLogLevel logging.Level, - aggregatorQuorumPercentage uint64, - aggregatorExtraPeerEndpoints []info.Peer, - subnetID ids.ID, - managerBlockchainID ids.ID, - managerAddress common.Address, - convertSubnetValidators []*txs.ConvertSubnetValidator, -) (*warp.Message, error) { - validators := []warpMessage.SubnetConversionValidatorData{} - for _, convertSubnetValidator := range convertSubnetValidators { - validators = append(validators, warpMessage.SubnetConversionValidatorData{ - NodeID: convertSubnetValidator.NodeID[:], - BLSPublicKey: convertSubnetValidator.Signer.PublicKey, - Weight: convertSubnetValidator.Weight, - }) - } - subnetConversionData := warpMessage.SubnetConversionData{ - SubnetID: subnetID, - ManagerChainID: managerBlockchainID, - ManagerAddress: managerAddress.Bytes(), - Validators: validators, - } - subnetConversionID, err := warpMessage.SubnetConversionID(subnetConversionData) - if err != nil { - return nil, err - } - addressedCallPayload, err := warpMessage.NewSubnetConversion(subnetConversionID) - if err != nil { - return nil, err - } - subnetConversionAddressedCall, err := warpPayload.NewAddressedCall( - nil, - addressedCallPayload.Bytes(), - ) - if err != nil { - return nil, err - } - subnetConversionUnsignedMessage, err := warp.NewUnsignedMessage( - network.ID, - avagoconstants.PlatformChainID, - subnetConversionAddressedCall.Bytes(), - ) - if err != nil { - return nil, err - } - signatureAggregator, err := interchain.NewSignatureAggregator( - network, - aggregatorLogLevel, - subnetID, - aggregatorQuorumPercentage, - aggregatorExtraPeerEndpoints, - ) - if err != nil { - return nil, err - } - return signatureAggregator.Sign(subnetConversionUnsignedMessage, subnetID[:]) -} - -// calls poa manager validators set init method, -// passing to it the p-chain signed [subnetConversionSignedMessage] -// so as to verify p-chain already proceesed the associated -// ConvertSubnetTx -func PoAValidatorManagerInitializeValidatorsSet( - rpcURL string, - managerAddress common.Address, - privateKey string, - subnetID ids.ID, - managerBlockchainID ids.ID, - convertSubnetValidators []*txs.ConvertSubnetValidator, - subnetConversionSignedMessage *warp.Message, -) (*types.Transaction, *types.Receipt, error) { - type InitialValidator struct { - NodeID []byte - BlsPublicKey []byte - Weight uint64 - } - type SubnetConversionData struct { - SubnetID [32]byte - ValidatorManagerBlockchainID [32]byte - ValidatorManagerAddress common.Address - InitialValidators []InitialValidator - } - validators := []InitialValidator{} - for _, convertSubnetValidator := range convertSubnetValidators { - validators = append(validators, InitialValidator{ - NodeID: convertSubnetValidator.NodeID[:], - BlsPublicKey: convertSubnetValidator.Signer.PublicKey[:], - Weight: convertSubnetValidator.Weight, - }) - } - subnetConversionData := SubnetConversionData{ - SubnetID: subnetID, - ValidatorManagerBlockchainID: managerBlockchainID, - ValidatorManagerAddress: managerAddress, - InitialValidators: validators, - } - return contract.TxToMethodWithWarpMessage( - rpcURL, - privateKey, - managerAddress, - subnetConversionSignedMessage, - big.NewInt(0), - "initialize validator set", - errorSignatureToError, - "initializeValidatorSet((bytes32,bytes32,address,[(bytes,bytes,uint64)]),uint32)", - subnetConversionData, - uint32(0), - ) -} - // setups PoA manager after a successful execution of // ConvertSubnetTx on P-Chain // needs the list of validators for that tx, // [convertSubnetValidators], together with an evm [ownerAddress] // to set as the owner of the PoA manager func SetupPoA( - app *application.Avalanche, + subnet blockchainSDK.Subnet, network models.Network, - rpcURL string, - chainSpec contract.ChainSpec, privateKey string, - ownerAddress common.Address, - convertSubnetValidators []*txs.ConvertSubnetValidator, aggregatorExtraPeerEndpoints []info.Peer, aggregatorLogLevelStr string, ) error { - if err := evm.SetupProposerVM( - rpcURL, - privateKey, - ); err != nil { - return err - } - subnetID, err := contract.GetSubnetID( - app, - network, - chainSpec, - ) - if err != nil { - return err - } - blockchainID, err := contract.GetBlockchainID( - app, - network, - chainSpec, - ) - if err != nil { - return err - } - managerAddress := common.HexToAddress(ValidatorContractAddress) - tx, _, err := PoAValidatorManagerInitialize( - rpcURL, - managerAddress, - privateKey, - subnetID, - ownerAddress, - ) - if err != nil { - if !errors.Is(err, errAlreadyInitialized) { - return evm.TransactionError(tx, err, "failure initializing poa validator manager") - } - ux.Logger.PrintToUser("Warning: the PoA contract is already initialized.") - } aggregatorLogLevel, err := logging.ToLevel(aggregatorLogLevelStr) if err != nil { aggregatorLogLevel = defaultAggregatorLogLevel } - subnetConversionSignedMessage, err := PoaValidatorManagerGetPChainSubnetConversionWarpMessage( - network, - aggregatorLogLevel, - 0, - aggregatorExtraPeerEndpoints, - subnetID, - blockchainID, - managerAddress, - convertSubnetValidators, - ) - if err != nil { - return fmt.Errorf("failure signing subnet conversion warp message: %w", err) - } - tx, _, err = PoAValidatorManagerInitializeValidatorsSet( - rpcURL, - managerAddress, - privateKey, - subnetID, - blockchainID, - convertSubnetValidators, - subnetConversionSignedMessage, - ) - if err != nil { - return evm.TransactionError(tx, err, "failure initializing validators set on poa manager") - } - return nil + return subnet.InitializeProofOfAuthority(network, privateKey, aggregatorExtraPeerEndpoints, aggregatorLogLevel) } diff --git a/sdk/blockchain/blockchain.go b/sdk/blockchain/blockchain.go index 64324a1c3..8c699aeb4 100644 --- a/sdk/blockchain/blockchain.go +++ b/sdk/blockchain/blockchain.go @@ -12,6 +12,14 @@ import ( "os" "time" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/sdk/validatormanager" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ethereum/go-ethereum/common" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanche-cli/sdk/multisig" @@ -27,6 +35,14 @@ import ( "github.com/ava-labs/subnet-evm/params" ) +var ( + errMissingSubnetID = fmt.Errorf("missing Subnet ID") + errMissingBlockchainID = fmt.Errorf("missing Blockchain ID") + errMissingRPC = fmt.Errorf("missing RPC URL") + errMissingBootstrapValidators = fmt.Errorf("missing bootstrap validators") + errMissingOwnerAddress = fmt.Errorf("missing Owner Address") +) + type SubnetParams struct { // File path of Genesis to use // Do not set SubnetEVMParams or CustomVMParams @@ -101,11 +117,24 @@ type Subnet struct { // the target Subnet for CreateChainTx and AddValidatorTx SubnetID ids.ID + // BlockchainID is the transaction ID from an issued CreateChainTx + BlockchainID ids.ID + // VMID specifies the vm that the new chain will run when CreateChainTx is called VMID ids.ID // DeployInfo contains all the necessary information for createSubnetTx DeployInfo DeployParams + + // RPC URL that Subnet can be reached at + RPC string + + // OwnerAddress is address of the owner of the Validator Manager Contract + OwnerAddress *common.Address + + // BootstrapValidators are bootstrap validators that are included in the ConvertL1Tx call + // that made Subnet a sovereign blockchain + BootstrapValidators []*txs.ConvertSubnetValidator } func (c *Subnet) SetParams(controlKeys []ids.ShortID, subnetAuthKeys []ids.ShortID, threshold uint32) { @@ -306,3 +335,86 @@ func (c *Subnet) Commit(ms multisig.Multisig, wallet wallet.Wallet, waitForTxAcc } return tx.ID(), issueTxErr } + +// InitializeProofOfAuthority setups PoA manager after a successful execution of +// ConvertSubnetTx on P-Chain +// needs the list of validators for that tx, +// [convertSubnetValidators], together with an evm [ownerAddress] +// to set as the owner of the PoA manager +func (c *Subnet) InitializeProofOfAuthority( + network models.Network, + privateKey string, + aggregatorExtraPeerEndpoints []info.Peer, + aggregatorLogLevel logging.Level, +) error { + if c.SubnetID == ids.Empty { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingSubnetID) + } + + if c.BlockchainID == ids.Empty { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingBlockchainID) + } + + if c.RPC == "" { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingRPC) + } + + if c.OwnerAddress == nil { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingOwnerAddress) + } + + if len(c.BootstrapValidators) == 0 { + return fmt.Errorf("unable to initialize Proof of Authority: %w", errMissingBootstrapValidators) + } + + if err := evm.SetupProposerVM( + c.RPC, + privateKey, + ); err != nil { + return err + } + + managerAddress := common.HexToAddress(validatormanager.ValidatorContractAddress) + tx, _, err := validatormanager.PoAValidatorManagerInitialize( + c.RPC, + managerAddress, + privateKey, + c.SubnetID, + *c.OwnerAddress, + ) + if err != nil { + if !errors.Is(err, validatormanager.ErrAlreadyInitialized) { + return evm.TransactionError(tx, err, "failure initializing poa validator manager") + } + ux.Logger.PrintToUser("Warning: the PoA contract is already initialized.") + } + + subnetConversionSignedMessage, err := validatormanager.GetPoAPChainSubnetConversionWarpMessage( + network, + aggregatorLogLevel, + 0, + aggregatorExtraPeerEndpoints, + c.SubnetID, + c.BlockchainID, + managerAddress, + c.BootstrapValidators, + ) + if err != nil { + return fmt.Errorf("failure signing subnet conversion warp message: %w", err) + } + + tx, _, err = validatormanager.PoAInitializeValidatorsSet( + c.RPC, + managerAddress, + privateKey, + c.SubnetID, + c.BlockchainID, + c.BootstrapValidators, + subnetConversionSignedMessage, + ) + if err != nil { + return evm.TransactionError(tx, err, "failure initializing validators set on poa manager") + } + + return nil +} diff --git a/sdk/validatormanager/validator_manager.go b/sdk/validatormanager/validator_manager.go new file mode 100644 index 000000000..8b481a37d --- /dev/null +++ b/sdk/validatormanager/validator_manager.go @@ -0,0 +1,220 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validatormanager + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/avalanche-cli/pkg/contract" + "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/sdk/interchain" + "github.com/ava-labs/avalanchego/api/info" + avagoconstants "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + warpMessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + warpPayload "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/ava-labs/avalanchego/ids" +) + +const ( + ValidatorContractAddress = "0x5F584C2D56B4c356e7d82EC6129349393dc5df17" +) + +var ( + ErrAlreadyInitialized = errors.New("the contract is already initialized") + errInvalidMaximumChurnPercentage = fmt.Errorf("unvalid churn percentage") + errInvalidValidationID = fmt.Errorf("invalid validation id") + errInvalidValidatorStatus = fmt.Errorf("invalid validator status") + errMaxChurnRateExceeded = fmt.Errorf("max churn rate exceeded") + errInvalidInitializationStatus = fmt.Errorf("validators set already initialized") + errInvalidValidatorManagerBlockchainID = fmt.Errorf("invalid validator manager blockchain ID") + errInvalidValidatorManagerAddress = fmt.Errorf("invalid validator manager address") + errNodeAlreadyRegistered = fmt.Errorf("node already registered") + errInvalidSubnetConversionID = fmt.Errorf("invalid subnet conversion id") + errInvalidRegistrationExpiry = fmt.Errorf("invalid registration expiry") + errInvalidBLSKeyLength = fmt.Errorf("invalid BLS key length") + errInvalidNodeID = fmt.Errorf("invalid node id") + errInvalidWarpMessage = fmt.Errorf("invalid warp message") + errInvalidWarpSourceChainID = fmt.Errorf("invalid wapr source chain ID") + errInvalidWarpOriginSenderAddress = fmt.Errorf("invalid warp origin sender address") + errorSignatureToError = map[string]error{ + "InvalidInitialization()": ErrAlreadyInitialized, + "InvalidMaximumChurnPercentage(uint8)": errInvalidMaximumChurnPercentage, + "InvalidValidationID(bytes32)": errInvalidValidationID, + "InvalidValidatorStatus(uint8)": errInvalidValidatorStatus, + "MaxChurnRateExceeded(uint64)": errMaxChurnRateExceeded, + "InvalidInitializationStatus()": errInvalidInitializationStatus, + "InvalidValidatorManagerBlockchainID(bytes32)": errInvalidValidatorManagerBlockchainID, + "InvalidValidatorManagerAddress(address)": errInvalidValidatorManagerAddress, + "NodeAlreadyRegistered(bytes)": errNodeAlreadyRegistered, + "InvalidSubnetConversionID(bytes32,bytes32)": errInvalidSubnetConversionID, + "InvalidRegistrationExpiry(uint64)": errInvalidRegistrationExpiry, + "InvalidBLSKeyLength(uint256)": errInvalidBLSKeyLength, + "InvalidNodeID(bytes)": errInvalidNodeID, + "InvalidWarpMessage()": errInvalidWarpMessage, + "InvalidWarpSourceChainID(bytes32)": errInvalidWarpSourceChainID, + "InvalidWarpOriginSenderAddress(address)": errInvalidWarpOriginSenderAddress, + } +) + +// PoAValidatorManagerInitialize initializes contract [managerAddress] at [rpcURL], to +// manage validators on [subnetID], with +// owner given by [ownerAddress] +func PoAValidatorManagerInitialize( + rpcURL string, + managerAddress common.Address, + privateKey string, + subnetID ids.ID, + ownerAddress common.Address, +) (*types.Transaction, *types.Receipt, error) { + const ( + defaultChurnPeriodSeconds = uint64(0) + defaultMaximumChurnPercentage = uint8(20) + ) + type Params struct { + SubnetID [32]byte + ChurnPeriodSeconds uint64 + MaximumChurnPercentage uint8 + } + params := Params{ + SubnetID: subnetID, + ChurnPeriodSeconds: defaultChurnPeriodSeconds, + MaximumChurnPercentage: defaultMaximumChurnPercentage, + } + return contract.TxToMethod( + rpcURL, + privateKey, + managerAddress, + nil, + "initialize PoA manager", + errorSignatureToError, + "initialize((bytes32,uint64,uint8),address)", + params, + ownerAddress, + ) +} + +// GetPoAPChainSubnetConversionWarpMessage constructs p-chain-validated (signed) subnet conversion warp +// message, to be sent to the validators manager when +// initializing validators set +// the message specifies [subnetID] that is being converted +// together with the validator's manager [managerBlockchainID], +// [managerAddress], and the initial list of [validators] +func GetPoAPChainSubnetConversionWarpMessage( + network models.Network, + aggregatorLogLevel logging.Level, + aggregatorQuorumPercentage uint64, + aggregatorExtraPeerEndpoints []info.Peer, + subnetID ids.ID, + managerBlockchainID ids.ID, + managerAddress common.Address, + convertSubnetValidators []*txs.ConvertSubnetValidator, +) (*warp.Message, error) { + validators := []warpMessage.SubnetConversionValidatorData{} + for _, convertSubnetValidator := range convertSubnetValidators { + validators = append(validators, warpMessage.SubnetConversionValidatorData{ + NodeID: convertSubnetValidator.NodeID[:], + BLSPublicKey: convertSubnetValidator.Signer.PublicKey, + Weight: convertSubnetValidator.Weight, + }) + } + subnetConversionData := warpMessage.SubnetConversionData{ + SubnetID: subnetID, + ManagerChainID: managerBlockchainID, + ManagerAddress: managerAddress.Bytes(), + Validators: validators, + } + subnetConversionID, err := warpMessage.SubnetConversionID(subnetConversionData) + if err != nil { + return nil, err + } + addressedCallPayload, err := warpMessage.NewSubnetConversion(subnetConversionID) + if err != nil { + return nil, err + } + subnetConversionAddressedCall, err := warpPayload.NewAddressedCall( + nil, + addressedCallPayload.Bytes(), + ) + if err != nil { + return nil, err + } + subnetConversionUnsignedMessage, err := warp.NewUnsignedMessage( + network.ID, + avagoconstants.PlatformChainID, + subnetConversionAddressedCall.Bytes(), + ) + if err != nil { + return nil, err + } + signatureAggregator, err := interchain.NewSignatureAggregator( + network, + aggregatorLogLevel, + subnetID, + aggregatorQuorumPercentage, + aggregatorExtraPeerEndpoints, + ) + if err != nil { + return nil, err + } + return signatureAggregator.Sign(subnetConversionUnsignedMessage, subnetID[:]) +} + +// PoAInitializeValidatorsSet calls poa manager validators set init method, +// passing to it the p-chain signed [subnetConversionSignedMessage] +// to verify p-chain already processed the associated ConvertSubnetTx +func PoAInitializeValidatorsSet( + rpcURL string, + managerAddress common.Address, + privateKey string, + subnetID ids.ID, + managerBlockchainID ids.ID, + convertSubnetValidators []*txs.ConvertSubnetValidator, + subnetConversionSignedMessage *warp.Message, +) (*types.Transaction, *types.Receipt, error) { + type InitialValidator struct { + NodeID []byte + BlsPublicKey []byte + Weight uint64 + } + type SubnetConversionData struct { + SubnetID [32]byte + ValidatorManagerBlockchainID [32]byte + ValidatorManagerAddress common.Address + InitialValidators []InitialValidator + } + validators := []InitialValidator{} + for _, convertSubnetValidator := range convertSubnetValidators { + validators = append(validators, InitialValidator{ + NodeID: convertSubnetValidator.NodeID[:], + BlsPublicKey: convertSubnetValidator.Signer.PublicKey[:], + Weight: convertSubnetValidator.Weight, + }) + } + subnetConversionData := SubnetConversionData{ + SubnetID: subnetID, + ValidatorManagerBlockchainID: managerBlockchainID, + ValidatorManagerAddress: managerAddress, + InitialValidators: validators, + } + return contract.TxToMethodWithWarpMessage( + rpcURL, + privateKey, + managerAddress, + subnetConversionSignedMessage, + big.NewInt(0), + "initialize validator set", + errorSignatureToError, + "initializeValidatorSet((bytes32,bytes32,address,[(bytes,bytes,uint64)]),uint32)", + subnetConversionData, + uint32(0), + ) +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 63317e376..42342d011 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -27,6 +27,7 @@ import ( _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/subnet/sov/public" _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/upgrade/non-sov" _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/upgrade/sov" + _ "github.com/ava-labs/avalanche-cli/tests/e2e/testcases/validatormanager" ginkgo "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "github.com/onsi/gomega/format" diff --git a/tests/e2e/testcases/subnet/non-sov/public/suite.go b/tests/e2e/testcases/subnet/non-sov/public/suite.go index b3258339d..6181b2583 100644 --- a/tests/e2e/testcases/subnet/non-sov/public/suite.go +++ b/tests/e2e/testcases/subnet/non-sov/public/suite.go @@ -39,7 +39,7 @@ const ( func deploySubnetToFujiNonSOV() (string, map[string]utils.NodeInfo) { // deploy s := commands.SimulateFujiDeployNonSOV(subnetName, keyName, controlKeys) - subnetID, err := utils.ParsePublicDeployOutput(s) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() @@ -111,7 +111,7 @@ var _ = ginkgo.Describe("[Public Subnet non SOV]", func() { fmt.Println(logging.LightRed.Wrap("DEPLOYING SUBNET. VERIFY LEDGER ADDRESS HAS CUSTOM HRP BEFORE SIGNING")) s := commands.SimulateMainnetDeployNonSOV(subnetName, 0, false) // deploy - subnetID, err := utils.ParsePublicDeployOutput(s) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() diff --git a/tests/e2e/testcases/subnet/sov/public/suite.go b/tests/e2e/testcases/subnet/sov/public/suite.go index e3f1da4b9..fbc2b91de 100644 --- a/tests/e2e/testcases/subnet/sov/public/suite.go +++ b/tests/e2e/testcases/subnet/sov/public/suite.go @@ -36,7 +36,7 @@ const ( func deploySubnetToFujiSOV() (string, map[string]utils.NodeInfo) { // deploy s := commands.SimulateFujiDeploySOV(subnetName, keyName, controlKeys) - subnetID, err := utils.ParsePublicDeployOutput(s) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() @@ -108,7 +108,7 @@ var _ = ginkgo.Describe("[Public Subnet SOV]", func() { fmt.Println(logging.LightRed.Wrap("DEPLOYING SUBNET. VERIFY LEDGER ADDRESS HAS CUSTOM HRP BEFORE SIGNING")) s := commands.SimulateMainnetDeploySOV(subnetName, 0, false) // deploy - subnetID, err := utils.ParsePublicDeployOutput(s) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() diff --git a/tests/e2e/testcases/upgrade/non-sov/suite.go b/tests/e2e/testcases/upgrade/non-sov/suite.go index b346162d7..0c3a8e4d2 100644 --- a/tests/e2e/testcases/upgrade/non-sov/suite.go +++ b/tests/e2e/testcases/upgrade/non-sov/suite.go @@ -389,7 +389,7 @@ var _ = ginkgo.Describe("[Upgrade local network non SOV]", ginkgo.Ordered, func( // Simulate fuji deployment s := commands.SimulateFujiDeployNonSOV(subnetName, keyName, controlKeys) - subnetID, err := utils.ParsePublicDeployOutput(s) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() diff --git a/tests/e2e/testcases/upgrade/sov/suite.go b/tests/e2e/testcases/upgrade/sov/suite.go index b23f86362..591806076 100644 --- a/tests/e2e/testcases/upgrade/sov/suite.go +++ b/tests/e2e/testcases/upgrade/sov/suite.go @@ -389,7 +389,7 @@ var _ = ginkgo.Describe("[Upgrade local network SOV]", ginkgo.Ordered, func() { // Simulate fuji deployment s := commands.SimulateFujiDeploySOV(subnetName, keyName, controlKeys) - subnetID, err := utils.ParsePublicDeployOutput(s) + subnetID, err := utils.ParsePublicDeployOutput(s, utils.SubnetIDParseType) gomega.Expect(err).Should(gomega.BeNil()) // add validators to subnet nodeInfos, err := utils.GetNodesInfo() diff --git a/tests/e2e/testcases/validatormanager/suite.go b/tests/e2e/testcases/validatormanager/suite.go new file mode 100644 index 000000000..1fe4a35e3 --- /dev/null +++ b/tests/e2e/testcases/validatormanager/suite.go @@ -0,0 +1,217 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package packageman + +import ( + "encoding/hex" + "fmt" + "os" + "os/exec" + "path" + + "github.com/ava-labs/avalanche-cli/cmd/blockchaincmd" + "github.com/ava-labs/avalanche-cli/pkg/constants" + "github.com/ava-labs/avalanche-cli/pkg/evm" + "github.com/ava-labs/avalanche-cli/pkg/key" + "github.com/ava-labs/avalanche-cli/pkg/models" + blockchainSDK "github.com/ava-labs/avalanche-cli/sdk/blockchain" + "github.com/ava-labs/avalanche-cli/tests/e2e/commands" + "github.com/ava-labs/avalanche-cli/tests/e2e/utils" + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ethereum/go-ethereum/common" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +const ( + CLIBinary = "./bin/avalanche" + subnetName = "e2eSubnetTest" + keyName = "ewoq" + avalancheGoPath = "--avalanchego-path" + ewoqEVMAddress = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ewoqPChainAddress = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + testLocalNodeName = "e2eSubnetTest-local-node" +) + +var err error + +func createEtnaSubnetEvmConfig() error { + // Check config does not already exist + _, err = utils.SubnetConfigExists(subnetName) + if err != nil { + return err + } + + // Create config + cmd := exec.Command( + CLIBinary, + "blockchain", + "create", + subnetName, + "--evm", + "--proof-of-authority", + "--poa-manager-owner", + ewoqEVMAddress, + "--production-defaults", + "--evm-chain-id=99999", + "--evm-token=TOK", + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + fmt.Println(string(output)) + return err +} + +func createSovereignSubnet() (string, string, error) { + if err := createEtnaSubnetEvmConfig(); err != nil { + return "", "", err + } + // Deploy subnet on etna devnet with local machine as bootstrap validator + cmd := exec.Command( + CLIBinary, + "blockchain", + "deploy", + subnetName, + "--etna-devnet", + "--use-local-machine", + avalancheGoPath+"="+utils.EtnaAvalancheGoBinaryPath, + "--num-local-nodes=1", + "--ewoq", + "--convert-only", + "--change-owner-address", + ewoqPChainAddress, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + utils.PrintStdErr(err) + } + fmt.Println(string(output)) + subnetID, err := utils.ParsePublicDeployOutput(string(output), utils.SubnetIDParseType) + if err != nil { + return "", "", err + } + blockchainID, err := utils.ParsePublicDeployOutput(string(output), utils.BlockchainIDParseType) + if err != nil { + return "", "", err + } + return subnetID, blockchainID, err +} + +func destroyLocalNode() { + _, err := os.Stat(testLocalNodeName) + if os.IsNotExist(err) { + return + } + cmd := exec.Command( + CLIBinary, + "node", + "local", + "destroy", + testLocalNodeName, + "--"+constants.SkipUpdateFlag, + ) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd.String()) + fmt.Println(string(output)) + utils.PrintStdErr(err) + } +} + +func getBootstrapValidator() ([]*txs.ConvertSubnetValidator, error) { + infoClient := info.NewClient("http://127.0.0.1:9650") + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + nodeID, proofOfPossession, err := infoClient.GetNodeID(ctx) + if err != nil { + return nil, err + } + publicKey := "0x" + hex.EncodeToString(proofOfPossession.PublicKey[:]) + pop := "0x" + hex.EncodeToString(proofOfPossession.ProofOfPossession[:]) + + bootstrapValidator := models.SubnetValidator{ + NodeID: nodeID.String(), + Weight: constants.BootstrapValidatorWeight, + Balance: constants.BootstrapValidatorBalance, + BLSPublicKey: publicKey, + BLSProofOfPossession: pop, + ChangeOwnerAddr: ewoqPChainAddress, + } + avaGoBootstrapValidators, err := blockchaincmd.ConvertToAvalancheGoSubnetValidator([]models.SubnetValidator{bootstrapValidator}) + if err != nil { + return nil, err + } + + return avaGoBootstrapValidators, nil +} + +var _ = ginkgo.Describe("[Validator Manager POA Set Up]", ginkgo.Ordered, func() { + ginkgo.BeforeEach(func() { + // key + _ = utils.DeleteKey(keyName) + output, err := commands.CreateKeyFromPath(keyName, utils.EwoqKeyPath) + if err != nil { + fmt.Println(output) + utils.PrintStdErr(err) + } + gomega.Expect(err).Should(gomega.BeNil()) + // subnet config + _ = utils.DeleteConfigs(subnetName) + destroyLocalNode() + }) + + ginkgo.AfterEach(func() { + destroyLocalNode() + commands.DeleteSubnetConfig(subnetName) + err := utils.DeleteKey(keyName) + gomega.Expect(err).Should(gomega.BeNil()) + commands.CleanNetwork() + }) + ginkgo.It("Set Up POA Validator Manager", func() { + subnetIDStr, blockchainIDStr, err := createSovereignSubnet() + gomega.Expect(err).Should(gomega.BeNil()) + _, err = commands.TrackLocalEtnaSubnet(testLocalNodeName, subnetName) + gomega.Expect(err).Should(gomega.BeNil()) + keyPath := path.Join(utils.GetBaseDir(), constants.KeyDir, fmt.Sprintf("subnet_%s_airdrop", subnetName)+constants.KeySuffix) + k, err := key.LoadSoft(models.NewLocalNetwork().ID, keyPath) + gomega.Expect(err).Should(gomega.BeNil()) + rpcURL := fmt.Sprintf("http://127.0.0.1:9650/ext/bc/%s/rpc", blockchainIDStr) + client, err := evm.GetClient(rpcURL) + gomega.Expect(err).Should(gomega.BeNil()) + evm.WaitForChainID(client) + + network := models.NewNetworkFromCluster(models.NewEtnaDevnetNetwork(), testLocalNodeName) + extraAggregatorPeers, err := blockchaincmd.ConvertURIToPeers([]string{"http://127.0.0.1:9650"}) + gomega.Expect(err).Should(gomega.BeNil()) + + subnetID, err := ids.FromString(subnetIDStr) + gomega.Expect(err).Should(gomega.BeNil()) + + blockchainID, err := ids.FromString(blockchainIDStr) + gomega.Expect(err).Should(gomega.BeNil()) + + avaGoBootstrapValidators, err := getBootstrapValidator() + gomega.Expect(err).Should(gomega.BeNil()) + ownerAddress := common.HexToAddress(ewoqEVMAddress) + subnetSDK := blockchainSDK.Subnet{ + SubnetID: subnetID, + BlockchainID: blockchainID, + OwnerAddress: &ownerAddress, + RPC: rpcURL, + BootstrapValidators: avaGoBootstrapValidators, + } + + err = subnetSDK.InitializeProofOfAuthority(network, k.PrivKeyHex(), extraAggregatorPeers, logging.Off) + gomega.Expect(err).Should(gomega.BeNil()) + }) +}) diff --git a/tests/e2e/utils/constants.go b/tests/e2e/utils/constants.go index d541f53fc..8994ea79e 100644 --- a/tests/e2e/utils/constants.go +++ b/tests/e2e/utils/constants.go @@ -32,6 +32,8 @@ const ( EtnaAvalancheGoBinaryPath = "tests/e2e/assets/avalanchego" PluginDirExt = "plugins" - ledgerSimDir = "./tests/e2e/ledgerSim" - basicLedgerSimScript = "./launchAndApproveTxs.ts" + ledgerSimDir = "./tests/e2e/ledgerSim" + basicLedgerSimScript = "./launchAndApproveTxs.ts" + SubnetIDParseType = "SubnetID" + BlockchainIDParseType = "BlockchainID" ) diff --git a/tests/e2e/utils/helpers.go b/tests/e2e/utils/helpers.go index fd55769ca..717b06041 100644 --- a/tests/e2e/utils/helpers.go +++ b/tests/e2e/utils/helpers.go @@ -605,25 +605,33 @@ func DownloadCustomVMBin(subnetEVMversion string) (string, error) { return subnetEVMBin, nil } -func ParsePublicDeployOutput(output string) (string, error) { +// ParsePublicDeployOutput can parse Subnet ID or Blockchain ID +func ParsePublicDeployOutput(output string, parseType string) (string, error) { lines := strings.Split(output, "\n") - var subnetID string + var targetID string for _, line := range lines { - if !strings.Contains(line, "Subnet ID") && !strings.Contains(line, "RPC URL") { + if !strings.Contains(line, "Subnet ID") && !strings.Contains(line, "RPC URL") && !strings.Contains(line, " Blockchain ID") { continue } words := strings.Split(line, "|") if len(words) != 4 { return "", errors.New("error parsing output: invalid number of words in line") } - if strings.Contains(line, "Subnet ID") { - subnetID = strings.TrimSpace(words[2]) + if parseType == SubnetIDParseType { + if strings.Contains(line, "Subnet ID") { + targetID = strings.TrimSpace(words[2]) + } + } + if parseType == BlockchainIDParseType { + if strings.Contains(line, "Blockchain ID") { + targetID = strings.TrimSpace(words[2]) + } } } - if subnetID == "" { + if targetID == "" { return "", errors.New("information not found in output") } - return subnetID, nil + return targetID, nil } func RestartNodesWithWhitelistedSubnets(whitelistedSubnets string) error { @@ -1059,3 +1067,7 @@ func GetKeyTransferFee(output string) (uint64, error) { } return feeNAvax, nil } + +func GetAPILargeContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), constants.APIRequestLargeTimeout) +}