Files

519 lines
16 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package blockchain
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"time"
"github.com/luxfi/cli/v2/sdk/interchain"
"github.com/luxfi/cli/v2/sdk/evm"
"github.com/luxfi/cli/v2/sdk/multisig"
"github.com/luxfi/cli/v2/sdk/network"
utilsSDK "github.com/luxfi/cli/v2/sdk/utils"
"github.com/luxfi/cli/v2/sdk/validatormanager"
"github.com/luxfi/cli/v2/sdk/vm"
"github.com/luxfi/cli/v2/sdk/wallet"
"github.com/luxfi/ids"
"github.com/luxfi/node/v2/v2/utils/logging"
"github.com/luxfi/node/v2/v2/vms/platformvm/txs"
walletoptions "github.com/luxfi/node/v2/v2/wallet"
"github.com/luxfi/evm/v2/commontype"
"github.com/luxfi/evm/v2/core"
"github.com/luxfi/evm/v2/params"
"github.com/ethereum/go-ethereum/common"
"go.uber.org/zap"
)
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
// if GenesisFilePath value is set
//
// See https://docs.lux.network/build/subnet/upgrade/customize-a-subnet#genesis for
// information on Genesis
GenesisFilePath string
// Subnet-EVM parameters to use
// Do not set SubnetEVM value if you are using Custom VM
SubnetEVM *SubnetEVMParams
// Name is alias for the Subnet, it is used to derive VM ID, which is required
// during for createBlockchainTx
Name string
}
type SubnetEVMParams struct {
// ChainID identifies the current chain and is used for replay protection
ChainID *big.Int
// FeeConfig sets the configuration for the dynamic fee algorithm
FeeConfig commontype.FeeConfig
// Allocation specifies the initial state that is part of the genesis block.
Allocation core.GenesisAlloc
// Ethereum uses Precompiles to efficiently implement cryptographic primitives within the EVM
// instead of re-implementing the same primitives in Solidity.
//
// Precompiles are a shortcut to execute a function implemented by the EVM itself,
// rather than an actual contract. A precompile is associated with a fixed address defined in
// the EVM. There is no byte code associated with that address.
//
// For more information regarding Precompiles, head to https://docs.lux.network/build/vm/evm/intro.
Precompiles params.Precompiles
// Timestamp
// TODO: add description what timestamp is
Timestamp *uint64
}
type CustomVMParams struct {
// File path of the Custom VM binary to use
VMFilePath string
// Git Repo URL to be used to build Custom VM
// Only set CustomVMRepoURL value when VMFilePath value is not set
CustomVMRepoURL string
// Git branch or commit to be used to build Custom VM
// Only set CustomVMBranch value when VMFilePath value is not set
CustomVMBranch string
// Filepath of the script to be used to build Custom VM
// Only set CustomVMBuildScript value when VMFilePath value is not set
CustomVMBuildScript string
}
type Subnet struct {
// Name is alias for the Subnet
Name string
// Genesis is the initial state of a blockchain when it is first created. Each Virtual Machine
// defines the format and semantics of its genesis data.
//
// For more information regarding Genesis, head to https://docs.lux.network/build/subnet/upgrade/customize-a-subnet#genesis
Genesis []byte
// SubnetID is the transaction ID from an issued CreateSubnetTX and is used to identify
// 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 ConvertSubnetToL1Tx call
// that made Subnet a sovereign L1
BootstrapValidators []*txs.ConvertSubnetToL1Validator
}
func (c *Subnet) SetParams(controlKeys []ids.ShortID, subnetAuthKeys []ids.ShortID, threshold uint32) {
c.DeployInfo = DeployParams{
ControlKeys: controlKeys,
SubnetAuthKeys: subnetAuthKeys,
Threshold: threshold,
}
}
// SetSubnetControlParams sets:
// - control keys, which are keys that are allowed to make changes to a Subnet
// - threshold, which is the number of keys that need to sign a transaction that changes
// a Subnet
func (c *Subnet) SetSubnetControlParams(controlKeys []ids.ShortID, threshold uint32) {
c.DeployInfo.ControlKeys = controlKeys
c.DeployInfo.Threshold = threshold
}
// SetSubnetAuthKeys sets subnetAuthKeys, which are keys that are being used to sign a transaction
// that changes a Subnet
func (c *Subnet) SetSubnetAuthKeys(subnetAuthKeys []ids.ShortID) {
c.DeployInfo.SubnetAuthKeys = subnetAuthKeys
}
type DeployParams struct {
// ControlKeys is a list of P-Chain addresses that are authorized to create new chains and add
// new validators to the Subnet
ControlKeys []ids.ShortID
// SubnetAuthKeys is a list of P-Chain addresses that will be used to sign transactions that
// will modify the Subnet.
//
// SubnetAuthKeys has to be a subset of ControlKeys
SubnetAuthKeys []ids.ShortID
// Threshold is the minimum number of signatures needed before a transaction can be issued
// Number of addresses in SubnetAuthKeys has to be more than or equal to Threshold number
Threshold uint32
}
// New takes SubnetParams as input and creates Subnet as an output
//
// The created Subnet object can be used to :
// - Create the Subnet on a specified network (Testnet / Mainnet)
// - Create Blockchain(s) in the Subnet
// - Add Validator(s) into the Subnet
func New(subnetParams *SubnetParams) (*Subnet, error) {
if subnetParams.GenesisFilePath != "" && subnetParams.SubnetEVM != nil {
return nil, fmt.Errorf("genesis file path cannot be non-empty if SubnetEVM params is not empty")
}
if subnetParams.GenesisFilePath == "" && subnetParams.SubnetEVM == nil {
return nil, fmt.Errorf("genesis file path and SubnetEVM params params cannot all be empty")
}
if subnetParams.Name == "" {
return nil, fmt.Errorf("SubnetEVM name cannot be empty")
}
var genesisBytes []byte
var err error
switch {
case subnetParams.GenesisFilePath != "":
genesisBytes, err = os.ReadFile(subnetParams.GenesisFilePath)
case subnetParams.SubnetEVM != nil:
genesisBytes, err = createEvmGenesis(subnetParams.SubnetEVM)
default:
}
if err != nil {
return nil, err
}
vmID, err := vmID(subnetParams.Name)
if err != nil {
return nil, fmt.Errorf("failed to create VM ID from %s: %w", subnetParams.Name, err)
}
subnet := Subnet{
Name: subnetParams.Name,
VMID: vmID,
Genesis: genesisBytes,
}
return &subnet, nil
}
func (c *Subnet) SetSubnetID(subnetID ids.ID) {
c.SubnetID = subnetID
}
func createEvmGenesis(
subnetEVMParams *SubnetEVMParams,
) ([]byte, error) {
genesis := core.Genesis{}
genesis.Timestamp = *subnetEVMParams.Timestamp
conf := params.SubnetEVMDefaultChainConfig
conf.NetworkUpgrades = params.NetworkUpgrades{}
var err error
if subnetEVMParams.ChainID == nil {
return nil, fmt.Errorf("genesis params chain ID cannot be empty")
}
if subnetEVMParams.FeeConfig == commontype.EmptyFeeConfig {
return nil, fmt.Errorf("genesis params fee config cannot be empty")
}
if subnetEVMParams.Allocation == nil {
return nil, fmt.Errorf("genesis params allocation cannot be empty")
}
allocation := subnetEVMParams.Allocation
if subnetEVMParams.Precompiles == nil {
return nil, fmt.Errorf("genesis params precompiles cannot be empty")
}
conf.FeeConfig = subnetEVMParams.FeeConfig
conf.GenesisPrecompiles = subnetEVMParams.Precompiles
conf.ChainID = subnetEVMParams.ChainID
genesis.Alloc = allocation
genesis.Config = conf
genesis.Difficulty = vm.Difficulty
genesis.GasLimit = conf.FeeConfig.GasLimit.Uint64()
jsonBytes, err := genesis.MarshalJSON()
if err != nil {
return nil, err
}
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, jsonBytes, "", " ")
if err != nil {
return nil, err
}
return prettyJSON.Bytes(), nil
}
func vmID(vmName string) (ids.ID, error) {
if len(vmName) > 32 {
return ids.Empty, fmt.Errorf("VM name must be <= 32 bytes, found %d", len(vmName))
}
b := make([]byte, 32)
copy(b, []byte(vmName))
return ids.ToID(b)
}
func (c *Subnet) Commit(ms multisig.Multisig, wallet wallet.Wallet, waitForTxAcceptance bool) (ids.ID, error) {
if ms.Undefined() {
return ids.Empty, multisig.ErrUndefinedTx
}
isReady, err := ms.IsReadyToCommit()
if err != nil {
return ids.Empty, err
}
if !isReady {
return ids.Empty, errors.New("tx is not fully signed so can't be committed")
}
tx, err := ms.GetWrappedPChainTx()
if err != nil {
return ids.Empty, err
}
const (
repeats = 3
sleepBetweenRepeats = 2 * time.Second
)
var issueTxErr error
if err != nil {
return ids.Empty, err
}
for i := 0; i < repeats; i++ {
ctx, cancel := utilsSDK.GetAPILargeContext()
defer cancel()
options := []walletoptions.Option{walletoptions.WithContext(ctx)}
if !waitForTxAcceptance {
options = append(options, walletoptions.WithAssumeDecided())
}
// TODO: split error checking and recovery between issuing and waiting for status
issueTxErr = wallet.P().IssueTx(tx, options...)
if issueTxErr == nil {
break
}
if ctx.Err() != nil {
issueTxErr = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), issueTxErr)
} else {
issueTxErr = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), issueTxErr)
}
time.Sleep(sleepBetweenRepeats)
}
if issueTxErr != nil {
return ids.Empty, fmt.Errorf("issue tx error %w", issueTxErr)
}
if _, ok := ms.PChainTx.Unsigned.(*txs.CreateSubnetTx); ok {
c.SubnetID = tx.ID()
}
return tx.ID(), issueTxErr
}
// InitializeProofOfAuthority setups PoA manager after a successful execution of
// ConvertSubnetToL1Tx 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(
log logging.Logger,
network network.Network,
privateKey string,
aggregatorLogger logging.Logger,
validatorManagerAddressStr string,
useACP99 bool,
signatureAggregatorEndpoint string,
) 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 client, err := evm.GetClient(c.RPC); err != nil {
log.Error("failure connecting to L1 to setup proposer VM", zap.Error(err))
} else {
if err := client.SetupProposerVM(privateKey); err != nil {
log.Error("failure setting proposer VM on L1", zap.Error(err))
}
client.Close()
}
managerAddress := common.HexToAddress(validatorManagerAddressStr)
tx, _, err := validatormanager.PoAValidatorManagerInitialize(
c.RPC,
managerAddress,
privateKey,
c.SubnetID,
*c.OwnerAddress,
useACP99,
)
if err != nil {
if !errors.Is(err, validatormanager.ErrAlreadyInitialized) {
return evm.TransactionError(tx, err, "failure initializing poa validator manager")
}
log.Info("the PoA contract is already initialized, skipping initializing Proof of Authority contract")
}
subnetConversionUnsignedMessage, err := validatormanager.GetPChainSubnetToL1ConversionUnsignedMessage(
network,
c.SubnetID,
c.BlockchainID,
managerAddress,
c.BootstrapValidators,
)
if err != nil {
return fmt.Errorf("failure signing subnet conversion warp message: %w", err)
}
chainIDHexStr := hex.EncodeToString(c.SubnetID[:])
messageHexStr := hex.EncodeToString(subnetConversionUnsignedMessage.Bytes())
signedMessage, err := interchain.SignMessage(aggregatorLogger, signatureAggregatorEndpoint, messageHexStr, chainIDHexStr, c.SubnetID.String(), 0)
if err != nil {
return fmt.Errorf("failed to get signed message: %w", err)
}
tx, _, err = validatormanager.InitializeValidatorsSet(
c.RPC,
managerAddress,
privateKey,
c.SubnetID,
c.BlockchainID,
c.BootstrapValidators,
signedMessage,
)
if err != nil {
return evm.TransactionError(tx, err, "failure initializing validators set on poa manager")
}
return nil
}
func (c *Subnet) InitializeProofOfStake(
log logging.Logger,
network network.Network,
privateKey string,
aggregatorLogger logging.Logger,
posParams validatormanager.PoSParams,
managerAddress string,
specializedManagerAddress string,
managerOwnerPrivateKey string,
useACP99 bool,
signatureAggregatorEndpoint string,
) error {
if client, err := evm.GetClient(c.RPC); err != nil {
log.Error("failure connecting to L1 to setup proposer VM", zap.Error(err))
} else {
if err := client.SetupProposerVM(privateKey); err != nil {
log.Error("failure setting proposer VM on L1", zap.Error(err))
}
client.Close()
}
if useACP99 {
managerOwnerAddress, err := evm.PrivateKeyToAddress(managerOwnerPrivateKey)
if err != nil {
return fmt.Errorf("could not generate manager owner address from manager owner private key: %w", err)
}
tx, _, err := validatormanager.PoAValidatorManagerInitialize(
c.RPC,
common.HexToAddress(managerAddress),
privateKey,
c.SubnetID,
managerOwnerAddress,
useACP99,
)
if err != nil {
if !errors.Is(err, validatormanager.ErrAlreadyInitialized) {
return evm.TransactionError(tx, err, "failure initializing validator manager")
}
log.Info("the Validator Manager contract is already initialized, skipping initializing it")
}
}
tx, _, err := validatormanager.PoSValidatorManagerInitialize(
c.RPC,
common.HexToAddress(managerAddress),
common.HexToAddress(specializedManagerAddress),
managerOwnerPrivateKey,
privateKey,
c.SubnetID,
posParams,
useACP99,
)
if err != nil {
if !errors.Is(err, validatormanager.ErrAlreadyInitialized) {
return evm.TransactionError(tx, err, "failure initializing native PoS validator manager")
}
log.Info("the PoS contract is already initialized, skipping initializing Proof of Stake contract")
}
subnetConversionUnsignedMessage, err := validatormanager.GetPChainSubnetToL1ConversionUnsignedMessage(
network,
c.SubnetID,
c.BlockchainID,
common.HexToAddress(managerAddress),
c.BootstrapValidators,
)
if err != nil {
return fmt.Errorf("failure signing subnet conversion warp message: %w", err)
}
chainIDHexStr := hex.EncodeToString(c.SubnetID[:])
messageHexStr := hex.EncodeToString(subnetConversionUnsignedMessage.Bytes())
signedMessage, err := interchain.SignMessage(aggregatorLogger, signatureAggregatorEndpoint, messageHexStr, chainIDHexStr, c.SubnetID.String(), 0)
if err != nil {
return fmt.Errorf("failed to get signed message: %w", err)
}
tx, _, err = validatormanager.InitializeValidatorsSet(
c.RPC,
common.HexToAddress(managerAddress),
privateKey,
c.SubnetID,
c.BlockchainID,
c.BootstrapValidators,
signedMessage,
)
if err != nil {
return evm.TransactionError(tx, err, "failure initializing validators set on pos manager")
}
return nil
}