diff --git a/cosmos/chain/chain.go b/cosmos/chain/chain.go index 13cdd6f..6f7889c 100644 --- a/cosmos/chain/chain.go +++ b/cosmos/chain/chain.go @@ -18,6 +18,8 @@ import ( "time" ) +// Chain is a logical representation of a Cosmos-based blockchain + type Chain struct { Config petritypes.ChainConfig @@ -33,6 +35,8 @@ type Chain struct { var _ petritypes.ChainI = &Chain{} +// CreateChain creates the Chain object and initializes the node tasks, their backing compute and the validator wallets + func CreateChain(ctx context.Context, logger *zap.Logger, infraProvider provider.Provider, config petritypes.ChainConfig) (*Chain, error) { var chain Chain @@ -96,10 +100,14 @@ func CreateChain(ctx context.Context, logger *zap.Logger, infraProvider provider return &chain, nil } +// GetConfig returns the Chain's configuration + func (c *Chain) GetConfig() petritypes.ChainConfig { return c.Config } +// Height returns the chain's height from the first available full node in the network + func (c *Chain) Height(ctx context.Context) (uint64, error) { node := c.GetFullNode() @@ -120,6 +128,9 @@ func (c *Chain) Height(ctx context.Context) (uint64, error) { return uint64(status.SyncInfo.LatestBlockHeight), nil } +// Init initializes the chain. That consists of generating the genesis transactions, genesis file, wallets, +// the distribution of configuration files and starting the network nodes up + func (c *Chain) Init(ctx context.Context) error { decimalPow := int64(math.Pow10(int(c.Config.Decimals))) @@ -288,6 +299,8 @@ func (c *Chain) Init(ctx context.Context) error { return nil } +// Teardown destroys all resources related to a chain and its' nodes + func (c *Chain) Teardown(ctx context.Context) error { c.logger.Info("tearing down chain", zap.String("name", c.Config.ChainId)) @@ -306,6 +319,9 @@ func (c *Chain) Teardown(ctx context.Context) error { return nil } +// PeerStrings returns a comma-delimited string with the addresses of chain nodes in +// the format of nodeid@host:port + func (c *Chain) PeerStrings(ctx context.Context) (string, error) { peerStrings := []string{} @@ -328,14 +344,20 @@ func (c *Chain) PeerStrings(ctx context.Context) (string, error) { return strings.Join(peerStrings, ","), nil } +// GetGRPCClient returns a gRPC client of the first available node + func (c *Chain) GetGRPCClient(ctx context.Context) (*grpc.ClientConn, error) { return c.GetFullNode().GetGRPCClient(ctx) } +// GetTMClient returns a CometBFT client of the first available node + func (c *Chain) GetTMClient(ctx context.Context) (*rpchttp.HTTP, error) { return c.GetFullNode().GetTMClient(ctx) } +// GetFullNode returns the first available full node in the chain + func (c *Chain) GetFullNode() petritypes.NodeI { if len(c.Nodes) > 0 { // use first full node @@ -345,6 +367,8 @@ func (c *Chain) GetFullNode() petritypes.NodeI { return c.Validators[0] } +// WaitForBlocks blocks until the chain increases in block height by delta + func (c *Chain) WaitForBlocks(ctx context.Context, delta uint64) error { c.logger.Info("waiting for blocks", zap.Uint64("delta", delta)) @@ -380,6 +404,8 @@ func (c *Chain) WaitForBlocks(ctx context.Context, delta uint64) error { return nil } +// WaitForHeight blocks until the chain reaches block height desiredHeight + func (c *Chain) WaitForHeight(ctx context.Context, desiredHeight uint64) error { c.logger.Info("waiting for height", zap.Uint64("desired_height", desiredHeight)) @@ -415,26 +441,39 @@ func (c *Chain) WaitForHeight(ctx context.Context, desiredHeight uint64) error { return nil } +// GetValidators returns all of the validating nodes in the chain + func (c *Chain) GetValidators() []petritypes.NodeI { return c.Validators } +// GetNodes returns all of the non-validating nodes in the chain + func (c *Chain) GetNodes() []petritypes.NodeI { return c.Nodes } +// GetValidatorWallets returns the wallets that were used to create the Validators on-chain. +// The ordering of the slice should correspond to the ordering of GetValidators + func (c *Chain) GetValidatorWallets() []petritypes.WalletI { return c.ValidatorWallets } +// GetFaucetWallet retunrs a wallet that was funded and can be used to fund other wallets + func (c *Chain) GetFaucetWallet() petritypes.WalletI { return c.FaucetWallet } +// GetTxConfig returns a Cosmos SDK TxConfig + func (c *Chain) GetTxConfig() sdkclient.TxConfig { return c.Config.EncodingConfig.TxConfig } +// GetInterfaceRegistry returns a Cosmos SDK InterfaceRegistry + func (c *Chain) GetInterfaceRegistry() codectypes.InterfaceRegistry { return c.Config.EncodingConfig.InterfaceRegistry } diff --git a/cosmos/chain/chain_wallet.go b/cosmos/chain/chain_wallet.go index 47f8f81..e9c5578 100644 --- a/cosmos/chain/chain_wallet.go +++ b/cosmos/chain/chain_wallet.go @@ -8,6 +8,9 @@ import ( "github.com/skip-mev/petri/general/v2/types" ) +// BuildWallet creates a wallet in the first available full node's keystore. If a mnemonic is not specificied, +// a random one will be generated + func (c *Chain) BuildWallet(ctx context.Context, keyName, mnemonic string, walletConfig types.WalletConfig) (types.WalletI, error) { // if mnemonic is empty, we just generate a wallet if mnemonic == "" { @@ -21,14 +24,20 @@ func (c *Chain) BuildWallet(ctx context.Context, keyName, mnemonic string, walle return wallet.NewWallet(keyName, mnemonic, walletConfig) } +// RecoverKey recovers a wallet in the first available full node's keystore using a provided mnemonic + func (c *Chain) RecoverKey(ctx context.Context, keyName, mnemonic string) error { return c.GetFullNode().RecoverKey(ctx, keyName, mnemonic) } +// CreateWallet creates a wallet in the first available full node's keystore using a randomly generated mnemonic + func (c *Chain) CreateWallet(ctx context.Context, keyName string) (types.WalletI, error) { return c.GetFullNode().CreateWallet(ctx, keyName, c.Config.WalletConfig) } +// GetAddress returns a Bech32 formatted address for a given key in the first available full node's keystore + func (c *Chain) GetAddress(ctx context.Context, keyName string) ([]byte, error) { b32Addr, err := c.GetFullNode().KeyBech32(ctx, keyName, "acc") if err != nil { diff --git a/cosmos/chain/genesis.go b/cosmos/chain/genesis.go index 0d0eafd..6fba0b6 100644 --- a/cosmos/chain/genesis.go +++ b/cosmos/chain/genesis.go @@ -8,15 +8,23 @@ import ( "strings" ) +// GenesisKV is used in ModifyGenesis to specify which keys have to be modified +// in the resulting genesis + type GenesisKV struct { Key string `json:"key"` Value interface{} `json:"value"` } +// GenesisModifier represents a type of function that takes in genesis formatted in bytes +// and returns a modified genesis file in the same format + type GenesisModifier func([]byte) ([]byte, error) var _ GenesisModifier = ModifyGenesis(nil) +// NewGenesisKV is a function for creating a GenesisKV object + func NewGenesisKV(key string, value interface{}) GenesisKV { return GenesisKV{ Key: key, @@ -24,6 +32,9 @@ func NewGenesisKV(key string, value interface{}) GenesisKV { } } +// ModifyGenesis is a function that is a GenesisModifier and takes in GenesisKV +// to specify which fields of the genesis file should be modified + func ModifyGenesis(genesisKV []GenesisKV) func([]byte) ([]byte, error) { return func(genbz []byte) ([]byte, error) { g := make(map[string]interface{}) diff --git a/cosmos/cosmosutil/auth.go b/cosmos/cosmosutil/auth.go index a84044a..8fe1e75 100644 --- a/cosmos/cosmosutil/auth.go +++ b/cosmos/cosmosutil/auth.go @@ -5,6 +5,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) +// Account fetches the Cosmos SDK account from a provided full node func (c *ChainClient) Account(ctx context.Context, address string) (authtypes.AccountI, error) { authClient, err := c.getAuthClient(ctx) diff --git a/cosmos/cosmosutil/bank.go b/cosmos/cosmosutil/bank.go index ce29080..9e365cc 100644 --- a/cosmos/cosmosutil/bank.go +++ b/cosmos/cosmosutil/bank.go @@ -11,6 +11,7 @@ import ( // bank queries +// Balances returns the bank module token balances for a given address func (c *ChainClient) Balances(ctx context.Context, address string) (sdk.Coins, error) { bankClient, err := c.getBankClient(ctx) @@ -49,6 +50,7 @@ func (c *ChainClient) Balances(ctx context.Context, address string) (sdk.Coins, return balances, nil } +// Balances returns the bank module token balance for a given address and denom func (c *ChainClient) Balance(ctx context.Context, address, denom string) (sdk.Coin, error) { bankClient, err := c.getBankClient(ctx) @@ -72,6 +74,7 @@ func (c *ChainClient) Balance(ctx context.Context, address, denom string) (sdk.C return *res.Balance, nil } +// DenomMetadata returns the bank module token metadata for a given denom func (c *ChainClient) DenomMetadata(ctx context.Context, denom string) (banktypes.Metadata, error) { bankClient, err := c.getBankClient(ctx) @@ -90,6 +93,7 @@ func (c *ChainClient) DenomMetadata(ctx context.Context, denom string) (banktype return res.Metadata, nil } +// DenomsMetadata returns the bank module token metadata for all denoms func (c *ChainClient) DenomsMetadata(ctx context.Context) ([]banktypes.Metadata, error) { bankClient, err := c.getBankClient(ctx) @@ -119,6 +123,7 @@ func (c *ChainClient) DenomsMetadata(ctx context.Context) ([]banktypes.Metadata, return metadatas, nil } +// TotalSupplyAll returns the total supply of all tokens in the bank module func (c *ChainClient) TotalSupplyAll(ctx context.Context) (sdk.Coins, error) { bankClient, err := c.getBankClient(ctx) @@ -148,6 +153,7 @@ func (c *ChainClient) TotalSupplyAll(ctx context.Context) (sdk.Coins, error) { return supplies, nil } +// TotalSupplySingle returns the total supply of a single token in the bank module func (c *ChainClient) BankTotalSupplySingle(ctx context.Context, denom string) (sdk.Coin, error) { bankClient, err := c.getBankClient(ctx) @@ -168,6 +174,7 @@ func (c *ChainClient) BankTotalSupplySingle(ctx context.Context, denom string) ( // bank transactions +// BankSend sends tokens from the given user to another address func (c *ChainClient) BankSend(ctx context.Context, user InteractingWallet, toAddress []byte, amount sdk.Coins, gasSettings types.GasSettings, blocking bool) (*sdk.TxResponse, error) { fromAccAddress, err := sdk.AccAddressFromHexUnsafe(hex.EncodeToString(user.Address())) if err != nil { diff --git a/cosmos/cosmosutil/consensus.go b/cosmos/cosmosutil/consensus.go index d5e67a9..79c8a65 100644 --- a/cosmos/cosmosutil/consensus.go +++ b/cosmos/cosmosutil/consensus.go @@ -5,6 +5,7 @@ import ( rpctypes "github.com/cometbft/cometbft/rpc/core/types" ) +// Block fetches the Cosmos SDK block from a provided full node func (c *ChainClient) Block(ctx context.Context, height *int64) (*rpctypes.ResultBlock, error) { cc, err := c.Chain.GetTMClient(ctx) defer cc.Stop() diff --git a/cosmos/cosmosutil/gov.go b/cosmos/cosmosutil/gov.go index 3a8e14c..4e576b6 100644 --- a/cosmos/cosmosutil/gov.go +++ b/cosmos/cosmosutil/gov.go @@ -8,6 +8,7 @@ import ( "github.com/skip-mev/petri/general/v2/types" ) +// GovProposal fetches a proposal from the governance module func (c *ChainClient) GovProposal(ctx context.Context, proposalID uint64) (*govtypes.Proposal, error) { govClient, err := c.getGovClient(ctx) @@ -26,6 +27,7 @@ func (c *ChainClient) GovProposal(ctx context.Context, proposalID uint64) (*govt return res.GetProposal(), nil } +// GovProposals fetches all proposals from the governance module func (c *ChainClient) GovProposals(ctx context.Context) ([]*govtypes.Proposal, error) { govClient, err := c.getGovClient(ctx) @@ -58,6 +60,7 @@ func (c *ChainClient) GovProposals(ctx context.Context) ([]*govtypes.Proposal, e return proposals, nil } +// GovProposalVotes fetches all votes for a given proposal from the governance module func (c *ChainClient) GovProposalVotes(ctx context.Context, proposalID uint64) ([]*govtypes.Vote, error) { govClient, err := c.getGovClient(ctx) @@ -91,6 +94,7 @@ func (c *ChainClient) GovProposalVotes(ctx context.Context, proposalID uint64) ( return votes, nil } +// GovProposalVote fetches a vote for a given proposal from the governance module func (c *ChainClient) GovProposalVote(ctx context.Context, proposalID uint64, voter string) (*govtypes.Vote, error) { govClient, err := c.getGovClient(ctx) @@ -110,6 +114,7 @@ func (c *ChainClient) GovProposalVote(ctx context.Context, proposalID uint64, vo return res.GetVote(), nil } +// GovProposalDeposits fetches all deposits for a given proposal from the governance module func (c *ChainClient) GovProposalDeposits(ctx context.Context, proposalID uint64) ([]*govtypes.Deposit, error) { govClient, err := c.getGovClient(ctx) @@ -143,6 +148,7 @@ func (c *ChainClient) GovProposalDeposits(ctx context.Context, proposalID uint64 return deposits, nil } +// GovProposalDeposit fetches a deposit for a given proposal and depositor from the governance module func (c *ChainClient) GovProposalDeposit(ctx context.Context, proposalID uint64, depositor string) (*govtypes.Deposit, error) { govClient, err := c.getGovClient(ctx) @@ -162,6 +168,7 @@ func (c *ChainClient) GovProposalDeposit(ctx context.Context, proposalID uint64, return res.GetDeposit(), nil } +// GovTallyResult fetches the tally result for a given proposal from the governance module func (c *ChainClient) GovTallyResult(ctx context.Context, proposalID uint64) (*govtypes.TallyResult, error) { govClient, err := c.getGovClient(ctx) @@ -180,6 +187,7 @@ func (c *ChainClient) GovTallyResult(ctx context.Context, proposalID uint64) (*g return res.GetTally(), nil } +// GovVoteOnProposal casts a vote on a given proposal using the address of the wallet voter func (c *ChainClient) GovVoteOnProposal(ctx context.Context, proposalID uint64, voter InteractingWallet, option govtypes.VoteOption, gasSettings types.GasSettings) (*sdk.TxResponse, error) { msg := govtypes.NewMsgVote(sdk.AccAddress(voter.FormattedAddress()), proposalID, option, "") @@ -192,6 +200,7 @@ func (c *ChainClient) GovVoteOnProposal(ctx context.Context, proposalID uint64, return txResp, err } +// GovDepositOnProposal deposits tokens on a given proposal using the address of the wallet depositor func (c *ChainClient) GovDepositOnProposal(ctx context.Context, proposalID uint64, depositor InteractingWallet, amount sdk.Coins, gasSettings types.GasSettings) (*sdk.TxResponse, error) { msg := govtypes.NewMsgDeposit(sdk.AccAddress(depositor.FormattedAddress()), proposalID, amount) @@ -204,6 +213,7 @@ func (c *ChainClient) GovDepositOnProposal(ctx context.Context, proposalID uint6 return txResp, err } +// GovSubmitProposal submits a proposal using the address of the wallet proposer func (c *ChainClient) GovSubmitProposal(ctx context.Context, proposer *InteractingWallet, messages []sdk.Msg, initialDeposit sdk.Coins, gasSettings types.GasSettings, metadata, title, summary string, expedited bool) (*sdk.TxResponse, error) { diff --git a/cosmos/cosmosutil/util.go b/cosmos/cosmosutil/util.go index a69d7b9..e0ad9f4 100644 --- a/cosmos/cosmosutil/util.go +++ b/cosmos/cosmosutil/util.go @@ -6,6 +6,7 @@ import ( "github.com/skip-mev/petri/general/v2/types" ) +// GetFeeAmountsFromGasSettings returns the fee amounts from the gas settings func GetFeeAmountsFromGasSettings(gasSettings types.GasSettings) sdk.Coins { return sdk.NewCoins(sdk.NewCoin(gasSettings.GasDenom, math.NewInt(gasSettings.Gas).Mul(math.NewInt(gasSettings.PricePerGas)))) } diff --git a/cosmos/cosmosutil/wallet.go b/cosmos/cosmosutil/wallet.go index 9a6e519..f0e12af 100644 --- a/cosmos/cosmosutil/wallet.go +++ b/cosmos/cosmosutil/wallet.go @@ -19,12 +19,14 @@ import ( "time" ) +// EncodingConfig is a struct that packs all the necessary encoding information type EncodingConfig struct { InterfaceRegistry codectypes.InterfaceRegistry Codec codec.Codec TxConfig client.TxConfig } +// InteractingWallet is a utility struct that helps generating, signing and broadcasting messages type InteractingWallet struct { petritypes.WalletI @@ -32,6 +34,7 @@ type InteractingWallet struct { encodingConfig EncodingConfig } +// NewInteractingWallet creates a new InteractingWallet func NewInteractingWallet(network petritypes.ChainI, wallet petritypes.WalletI, encodingConfig EncodingConfig) *InteractingWallet { return &InteractingWallet{ WalletI: wallet, @@ -40,6 +43,7 @@ func NewInteractingWallet(network petritypes.ChainI, wallet petritypes.WalletI, } } +// CreateAndBroadcastTx creates, signs and broadcasts a transaction func (w *InteractingWallet) CreateAndBroadcastTx(ctx context.Context, blocking bool, gas int64, fees sdk.Coins, timeoutHeight uint64, memo string, msgs ...sdk.Msg) (*sdk.TxResponse, error) { tx, err := w.CreateSignedTx(ctx, gas, fees, timeoutHeight, memo, msgs...) @@ -50,6 +54,7 @@ func (w *InteractingWallet) CreateAndBroadcastTx(ctx context.Context, blocking b return w.BroadcastTx(ctx, tx, blocking) } +// CreateSignedTx creates a signed transaction func (w *InteractingWallet) CreateSignedTx(ctx context.Context, gas int64, fees sdk.Coins, timeoutHeight uint64, memo string, msgs ...sdk.Msg) (sdk.Tx, error) { tx, err := w.CreateTx(ctx, gas, fees, timeoutHeight, memo, msgs...) @@ -66,6 +71,7 @@ func (w *InteractingWallet) CreateSignedTx(ctx context.Context, gas int64, fees return w.SignTx(ctx, tx, acc.GetAccountNumber(), acc.GetSequence()) } +// CreateTx creates an unsigned transaction func (w *InteractingWallet) CreateTx(ctx context.Context, gas int64, fees sdk.Coins, timeoutHeight uint64, memo string, msgs ...sdk.Msg) (sdk.Tx, error) { txFactory := w.encodingConfig.TxConfig.NewTxBuilder() @@ -83,6 +89,7 @@ func (w *InteractingWallet) CreateTx(ctx context.Context, gas int64, fees sdk.Co return txFactory.GetTx(), nil } +// SignTx signs an unsigned transaction func (w *InteractingWallet) SignTx(ctx context.Context, tx sdk.Tx, accNum, sequence uint64) (sdk.Tx, error) { privateKey, err := w.PrivateKey() @@ -145,6 +152,7 @@ func (w *InteractingWallet) SignTx(ctx context.Context, tx sdk.Tx, accNum, seque return txFactory.GetTx(), nil } +// BroadcastTx broadcasts a signed transaction func (w *InteractingWallet) BroadcastTx(ctx context.Context, tx sdk.Tx, blocking bool) (*sdk.TxResponse, error) { txBytes, err := w.chain.GetTxConfig().TxEncoder()(tx) @@ -194,6 +202,7 @@ func (w *InteractingWallet) BroadcastTx(ctx context.Context, tx sdk.Tx, blocking return &txResp, nil } +// Account returns the account of the wallet from the auth module func (w *InteractingWallet) Account(ctx context.Context) (authtypes.AccountI, error) { cc, err := w.chain.GetGRPCClient(ctx) if err != nil { diff --git a/cosmos/loadtest/client.go b/cosmos/loadtest/client.go index a3174bb..ea4b758 100644 --- a/cosmos/loadtest/client.go +++ b/cosmos/loadtest/client.go @@ -14,6 +14,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +// DefaultClient is a default tm-load-test client that implements the Client interface type DefaultClient struct { loader *cosmosutil.InteractingWallet chainClient *cosmosutil.ChainClient @@ -24,6 +25,7 @@ type DefaultClient struct { skipSeqIncrement bool } +// NewDefaultClient creates a new DefaultClient func NewDefaultClient(loader *cosmosutil.InteractingWallet, chainClient *cosmosutil.ChainClient, msgs []sdk.Msg, seq, accNum uint64, gasSettings petritypes.GasSettings, p *payload.Payload, skipSequenceIncrement bool) *DefaultClient { return &DefaultClient{ loader: loader, @@ -37,6 +39,7 @@ func NewDefaultClient(loader *cosmosutil.InteractingWallet, chainClient *cosmosu } } +// GenerateTx generates a transaction using the msgs provided in the constructor func (c *DefaultClient) GenerateTx() ([]byte, error) { // update padding to be unique (for this client) padding := make([]byte, 64) diff --git a/cosmos/loadtest/client_factory.go b/cosmos/loadtest/client_factory.go index 3e8fff6..b129773 100644 --- a/cosmos/loadtest/client_factory.go +++ b/cosmos/loadtest/client_factory.go @@ -11,8 +11,10 @@ import ( petritypes "github.com/skip-mev/petri/general/v2/types" ) +// GenerateMsgs is a function that generates messages given an address for the DefaultClient type GenerateMsgs func(senderAddress []byte) ([]sdk.Msg, petritypes.GasSettings, error) +// DefaultClientFactory is an implementation of the ClientFactory interface that creates DefaultClients type DefaultClientFactory struct { chain petritypes.ChainI chainClient *cosmosutil.ChainClient @@ -25,6 +27,7 @@ type DefaultClientFactory struct { msgGenerator GenerateMsgs } +// ClientFactoryConfig is a struct that packs all the necessary information for creating a new DefaultClientFactory type ClientFactoryConfig struct { Chain petritypes.ChainI Seeder *cosmosutil.InteractingWallet @@ -48,6 +51,8 @@ func NewDefaultClientFactory(cfg ClientFactoryConfig, mbm module.BasicManager) ( }, nil } +// NewClient implements the ClientFactory's interface for creating new clients. +// In this case, it'll create the DefaultClient func (f *DefaultClientFactory) NewClient(cfg loadtest.Config) (loadtest.Client, error) { // create a new private-key loaderWallet, err := wallet.NewGeneratedWallet("seed", f.walletConfig) @@ -75,7 +80,6 @@ func (f *DefaultClientFactory) NewClient(cfg loadtest.Config) (loadtest.Client, return nil, err } - // create the CosmosDefaultClient return NewDefaultClient(interactingLoaderWallet, f.chainClient, msgs, acc.GetSequence(), acc.GetAccountNumber(), gasSettings, &payload.Payload{ Connections: uint64(cfg.Connections), Id: f.id, @@ -84,6 +88,7 @@ func (f *DefaultClientFactory) NewClient(cfg loadtest.Config) (loadtest.Client, }, f.skipSeqIncrement), nil } +// ValidateConfig for the DefaultClientFactory is a no-op func (f *DefaultClientFactory) ValidateConfig(_ loadtest.Config) error { return nil } diff --git a/cosmos/node/config.go b/cosmos/node/config.go index b5824d7..9d2f5eb 100644 --- a/cosmos/node/config.go +++ b/cosmos/node/config.go @@ -13,7 +13,7 @@ import ( type Toml map[string]any // recursiveModifyToml will apply toml modifications at the current depth, -// then recurse for new depths. +// then recurse for new depths func recursiveModifyToml(c map[string]any, modifications Toml) error { for key, value := range modifications { if reflect.ValueOf(value).Kind() == reflect.Map { @@ -40,6 +40,7 @@ func recursiveModifyToml(c map[string]any, modifications Toml) error { return nil } +// GenerateDefaultConsensusConfig returns a default / sensible config for CometBFT func GenerateDefaultConsensusConfig() Toml { cometBftConfig := make(Toml) @@ -78,6 +79,7 @@ func GenerateDefaultConsensusConfig() Toml { return cometBftConfig } +// GenerateDefaultAppConfig returns a default / sensible config for the Cosmos SDK func GenerateDefaultAppConfig(c petritypes.ChainI) Toml { sdkConfig := make(Toml) sdkConfig["minimum-gas-prices"] = c.GetConfig().GasPrices @@ -101,6 +103,8 @@ func GenerateDefaultAppConfig(c petritypes.ChainI) Toml { return sdkConfig } +// ModifyTomlConfigFile will modify a TOML config file at filePath with the provided modifications. +// If a certain path defined in modifications is not found in the existing config, it will return an error func (n *Node) ModifyTomlConfigFile( ctx context.Context, filePath string, @@ -132,6 +136,7 @@ func (n *Node) ModifyTomlConfigFile( return nil } +// SetDefaultConfigs will generate the default configs for CometBFT and the app, and write them to disk func (n *Node) SetDefaultConfigs(ctx context.Context) error { appConfig := GenerateDefaultAppConfig(n.chain) consensusConfig := GenerateDefaultConsensusConfig() @@ -155,6 +160,7 @@ func (n *Node) SetDefaultConfigs(ctx context.Context) error { return nil } +// SetPersistentPeers will set the node's persistent peers in the CometBFT config func (n *Node) SetPersistentPeers(ctx context.Context, peers string) error { cometBftConfig := make(Toml) diff --git a/cosmos/node/genesis.go b/cosmos/node/genesis.go index 48746ea..34b7819 100644 --- a/cosmos/node/genesis.go +++ b/cosmos/node/genesis.go @@ -10,6 +10,7 @@ import ( "time" ) +// GenesisFileContent returns the genesis file on the node in byte format func (n *Node) GenesisFileContent(ctx context.Context) ([]byte, error) { n.logger.Info("reading genesis file", zap.String("node", n.Definition.Name)) @@ -22,6 +23,7 @@ func (n *Node) GenesisFileContent(ctx context.Context) ([]byte, error) { return bz, err } +// CopyGenTx retrieves the genesis transaction from the node and copy it to the destination node func (n *Node) CopyGenTx(ctx context.Context, dstNode petritypes.NodeI) error { n.logger.Info("copying gen tx", zap.String("from", n.GetConfig().Name), zap.String("to", dstNode.GetConfig().Name)) @@ -44,6 +46,7 @@ func (n *Node) CopyGenTx(ctx context.Context, dstNode petritypes.NodeI) error { return dstNode.GetTask().WriteFile(context.Background(), path, gentx) } +// AddGenesisAccount adds a genesis account to the node's local genesis file func (n *Node) AddGenesisAccount(ctx context.Context, address string, genesisAmounts []types.Coin) error { n.logger.Debug("adding genesis account", zap.String("node", n.Definition.Name), zap.String("address", address)) @@ -78,6 +81,7 @@ func (n *Node) AddGenesisAccount(ctx context.Context, address string, genesisAmo return nil } +// GenerateGenTx generates a genesis transaction for the node func (n *Node) GenerateGenTx(ctx context.Context, genesisSelfDelegation types.Coin) error { n.logger.Info("generating genesis transaction", zap.String("node", n.Definition.Name)) @@ -104,6 +108,7 @@ func (n *Node) GenerateGenTx(ctx context.Context, genesisSelfDelegation types.Co return err } +// CollectGenTxs collects the genesis transactions from the node and create a finalized genesis file func (n *Node) CollectGenTxs(ctx context.Context) error { n.logger.Info("collecting genesis transactions", zap.String("node", n.Definition.Name)) @@ -122,6 +127,7 @@ func (n *Node) CollectGenTxs(ctx context.Context) error { return err } +// OverwriteGenesisFile overwrites the genesis file on the node with the provided genesis file func (n *Node) OverwriteGenesisFile(ctx context.Context, bz []byte) error { n.logger.Info("overwriting genesis file", zap.String("node", n.Definition.Name)) diff --git a/cosmos/node/init.go b/cosmos/node/init.go index 309afea..a6101a5 100644 --- a/cosmos/node/init.go +++ b/cosmos/node/init.go @@ -5,6 +5,7 @@ import ( "go.uber.org/zap" ) +// InitHome initializes the node's home directory func (n *Node) InitHome(ctx context.Context) error { n.logger.Info("initializing home", zap.String("name", n.Definition.Name)) chainConfig := n.chain.GetConfig() diff --git a/cosmos/node/keys.go b/cosmos/node/keys.go index e3d8eba..cbcb3b1 100644 --- a/cosmos/node/keys.go +++ b/cosmos/node/keys.go @@ -10,6 +10,7 @@ import ( "go.uber.org/zap" ) +// CreateWallet creates a new wallet on the node using a randomly generated mnemonic func (n *Node) CreateWallet(ctx context.Context, name string, walletConfig types.WalletConfig) (types.WalletI, error) { n.logger.Info("creating wallet", zap.String("name", name)) @@ -28,6 +29,7 @@ func (n *Node) CreateWallet(ctx context.Context, name string, walletConfig types return keyWallet, nil } +// RecoverWallet recovers a wallet on the node using a mnemonic func (n *Node) RecoverKey(ctx context.Context, name, mnemonic string) error { n.logger.Info("recovering wallet", zap.String("name", name), zap.String("mnemonic", mnemonic)) chainConfig := n.chain.GetConfig() @@ -43,6 +45,7 @@ func (n *Node) RecoverKey(ctx context.Context, name, mnemonic string) error { return err } +// KeyBech32 returns the bech32 address of a key on the node using the app's binary func (n *Node) KeyBech32(ctx context.Context, name, bech string) (string, error) { chainConfig := n.chain.GetConfig() diff --git a/cosmos/node/node.go b/cosmos/node/node.go index 5027c54..3360bea 100644 --- a/cosmos/node/node.go +++ b/cosmos/node/node.go @@ -26,6 +26,7 @@ type Node struct { var _ petritypes.NodeCreator = CreateNode +// CreateNode creates a new logical node and creates the underlying workload for it func CreateNode(ctx context.Context, logger *zap.Logger, nodeConfig petritypes.NodeConfig) (petritypes.NodeI, error) { var node Node @@ -75,10 +76,12 @@ func CreateNode(ctx context.Context, logger *zap.Logger, nodeConfig petritypes.N return &node, nil } +// GetTask returns the underlying task of the node func (n *Node) GetTask() *provider.Task { return n.Task } +// GetTMClient returns a CometBFT HTTP client for the node func (n *Node) GetTMClient(ctx context.Context) (*rpchttp.HTTP, error) { addr, err := n.Task.GetExternalAddress(ctx, "26657") @@ -102,6 +105,7 @@ func (n *Node) GetTMClient(ctx context.Context) (*rpchttp.HTTP, error) { return rpcClient, nil } +// GetGRPCClient returns a GRPC client for the node func (n *Node) GetGRPCClient(ctx context.Context) (*grpc.ClientConn, error) { grpcAddr, err := n.GetExternalAddress(ctx, "9090") if err != nil { @@ -117,6 +121,7 @@ func (n *Node) GetGRPCClient(ctx context.Context) (*grpc.ClientConn, error) { return cc, nil } +// Height returns the current block height of the node func (n *Node) Height(ctx context.Context) (uint64, error) { n.logger.Debug("getting height", zap.String("node", n.Definition.Name)) client, err := n.GetTMClient(ctx) @@ -134,10 +139,8 @@ func (n *Node) Height(ctx context.Context) (uint64, error) { return uint64(block.Block.Height), nil } +// NodeId returns the node's p2p ID func (n *Node) NodeId(ctx context.Context) (string, error) { - // This used to call p2p.LoadNodeKey against the file on the host, - // but because we are transitioning to operating on Docker volumes, - // we only have to tmjson.Unmarshal the raw content. j, err := n.Task.ReadFile(ctx, "config/node_key.json") if err != nil { return "", fmt.Errorf("getting node_key.json content: %w", err) @@ -151,6 +154,7 @@ func (n *Node) NodeId(ctx context.Context) (string, error) { return string(nk.ID()), nil } +// BinCommand returns a command that can be used to run a binary on the node func (n *Node) BinCommand(command ...string) []string { chainConfig := n.chain.GetConfig() @@ -160,6 +164,7 @@ func (n *Node) BinCommand(command ...string) []string { ) } +// GetConfig returns the node's config func (n *Node) GetConfig() petritypes.NodeConfig { return n.config } diff --git a/cosmos/wallet/cosmos.go b/cosmos/wallet/cosmos.go index 6c498fd..75cb3cc 100644 --- a/cosmos/wallet/cosmos.go +++ b/cosmos/wallet/cosmos.go @@ -1,13 +1,13 @@ package wallet import ( - "cosmossdk.io/math" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/go-bip39" petritypes "github.com/skip-mev/petri/general/v2/types" ) +// CosmosWallet implements the types.Wallet interface and represents a valid Cosmos SDK wallet type CosmosWallet struct { mnemonic string privKey cryptotypes.PrivKey @@ -18,12 +18,7 @@ type CosmosWallet struct { var _ petritypes.WalletI = &CosmosWallet{} -type WalletAmount struct { - Address string - Denom string - Amount math.Int -} - +// NewWallet creates a new CosmosWallet from a mnemonic and a keyname func NewWallet(keyname string, mnemonic string, config petritypes.WalletConfig) (*CosmosWallet, error) { derivedPrivKey, err := config.DerivationFn(mnemonic, "", config.HDPath.String()) @@ -42,6 +37,7 @@ func NewWallet(keyname string, mnemonic string, config petritypes.WalletConfig) }, nil } +// NewGeneratedWallet creates a new CosmosWallet from a randomly generated mnemonic and a keyname func NewGeneratedWallet(keyname string, config petritypes.WalletConfig) (*CosmosWallet, error) { entropy, err := bip39.NewEntropy(128) @@ -58,35 +54,43 @@ func NewGeneratedWallet(keyname string, config petritypes.WalletConfig) (*Cosmos return NewWallet(keyname, mnemonic, config) } +// KeyName returns the keyname of the wallet func (w *CosmosWallet) KeyName() string { return w.keyName } -// Get formatted address, passing in a prefix +// FormattedAddress returns a Bech32 formatted address for the wallet, using the provided Bech32 prefix +// in WalletConfig func (w *CosmosWallet) FormattedAddress() string { return types.MustBech32ifyAddressBytes(w.bech32Prefix, w.privKey.PubKey().Address()) } +// Address returns the raw address bytes for the wallet func (w *CosmosWallet) Address() []byte { return w.privKey.PubKey().Address() } +// FormattedAddressWithPrefix returns a Bech32 formatted address for the wallet, using the provided prefix func (w *CosmosWallet) FormattedAddressWithPrefix(prefix string) string { return types.MustBech32ifyAddressBytes(prefix, w.privKey.PubKey().Address()) } +// PublicKey returns the public key for the wallet func (w *CosmosWallet) PublicKey() (cryptotypes.PubKey, error) { return w.privKey.PubKey(), nil } +// PrivateKey returns the private key for the wallet func (w *CosmosWallet) PrivateKey() (cryptotypes.PrivKey, error) { return w.privKey, nil } +// SigningAlgo returns the signing algorithm for the wallet func (w *CosmosWallet) SigningAlgo() string { return w.signingAlgo } +// Mnemonic returns the mnemonic for the wallet func (w *CosmosWallet) Mnemonic() string { return w.mnemonic } diff --git a/general/monitoring/files/grafana/config/dashboard.json b/general/monitoring/files/grafana/config/dashboard.json index 13bb237..14aa0a2 100644 --- a/general/monitoring/files/grafana/config/dashboard.json +++ b/general/monitoring/files/grafana/config/dashboard.json @@ -18,10 +18,23 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 1, + "id": 2, "links": [], "liveNow": false, "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 8, + "panels": [], + "title": "Consensus stats", + "type": "row" + }, { "datasource": { "type": "prometheus", @@ -65,6 +78,8 @@ } }, "mappings": [], + "max": 2, + "min": 0, "thresholds": { "mode": "absolute", "steps": [ @@ -77,7 +92,9 @@ "value": 80 } ] - } + }, + "unit": "s", + "unitScale": true }, "overrides": [] }, @@ -85,9 +102,105 @@ "h": 8, "w": 12, "x": 0, - "y": 0 + "y": 1 }, - "id": 2, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "editorMode": "code", + "expr": "avg(rate(cometbft_consensus_block_interval_seconds_sum[$__rate_interval]) / rate(cometbft_consensus_block_interval_seconds_count[$__rate_interval]))", + "instant": false, + "legendFormat": "Average seconds per block", + "range": true, + "refId": "A" + } + ], + "title": "Block production rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 3, "options": { "legend": { "calcs": [], @@ -108,17 +221,113 @@ }, "disableTextWrap": false, "editorMode": "builder", - "expr": "cometbft_p2p_peers", + "expr": "avg(cometbft_consensus_rounds)", "fullMetaSearch": false, - "includeNullMetadata": false, + "includeNullMetadata": true, "instant": false, - "legendFormat": "{{instance}}", + "legendFormat": "Consensus rounds", "range": true, "refId": "A", "useBackend": false } ], - "title": "Peer counts", + "title": "Consensus rounds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(cometbft_consensus_validator_missed_blocks[$__rate_interval]) * 60)", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Average block miss rate", "type": "timeseries" }, { @@ -126,7 +335,6 @@ "type": "prometheus", "uid": "petri_prometheus" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -177,7 +385,9 @@ "value": 80 } ] - } + }, + "unit": "txs", + "unitScale": true }, "overrides": [] }, @@ -185,9 +395,9 @@ "h": 8, "w": 12, "x": 12, - "y": 0 + "y": 9 }, - "id": 1, + "id": 2, "options": { "legend": { "calcs": [], @@ -208,17 +418,603 @@ }, "disableTextWrap": false, "editorMode": "builder", - "expr": "cometbft_consensus_height", + "expr": "avg(cometbft_mempool_size)", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, - "legendFormat": "{{instance}}", + "legendFormat": "__auto", "range": true, "refId": "A", "useBackend": false } ], - "title": "Consensus height", + "title": "Average mempool size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "txs", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "editorMode": "code", + "expr": "rate(cometbft_consensus_total_txs[$__rate_interval]) * 60", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Transaction rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "txs", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "cometbft_mempool_size", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{job}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Mempool size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "editorMode": "code", + "expr": "sum by (instance) (rate(cometbft_p2p_message_receive_bytes_total[$__rate_interval]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Node received bytes per second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "editorMode": "code", + "expr": "sum by (instance) (rate(cometbft_p2p_message_send_bytes_total[$__rate_interval]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Node transmitted bytes per second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "editorMode": "code", + "expr": "avg by (message_type) (sum by (instance, message_type) (rate(cometbft_p2p_message_receive_bytes_total[$__rate_interval])))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Node received bytes per second by message type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "petri_prometheus" + }, + "editorMode": "code", + "expr": "avg by (message_type) (sum by (job, message_type) (rate(cometbft_p2p_message_send_bytes_total[$__rate_interval])))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Node transmitted bytes per second by message type", "type": "timeseries" } ], @@ -229,7 +1025,7 @@ "list": [] }, "time": { - "from": "now-30m", + "from": "now-15m", "to": "now" }, "timepicker": {}, diff --git a/general/monitoring/files/prometheus/config/prometheus.yml b/general/monitoring/files/prometheus/config/prometheus.yml index eee02a0..7bad91f 100644 --- a/general/monitoring/files/prometheus/config/prometheus.yml +++ b/general/monitoring/files/prometheus/config/prometheus.yml @@ -1,7 +1,7 @@ # my global config global: - scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. - evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + scrape_interval: 15s + evaluation_interval: 15s # scrape_timeout is set to the global default (10s). scrape_configs: @@ -12,4 +12,4 @@ scrape_configs: # scheme defaults to 'http'. static_configs: - - targets: {{ .Targets }} \ No newline at end of file + - targets: {{ .Targets }} diff --git a/general/monitoring/grafana.go b/general/monitoring/grafana.go index 49d7510..9978eda 100644 --- a/general/monitoring/grafana.go +++ b/general/monitoring/grafana.go @@ -4,6 +4,7 @@ import ( "bytes" "context" _ "embed" + "github.com/skip-mev/petri/general/v2/provider" "go.uber.org/zap" "text/template" ) @@ -11,9 +12,9 @@ import ( const DEFAULT_PROMETHEUS_URL = "http://prometheus:9090" type GrafanaOptions struct { - PrometheusURL string - DashboardJSON string - ProviderSpecificConfig interface{} + PrometheusURL string // The URL of the Prometheus instance. This needs to be accessible from the Grafana container. + DashboardJSON string // The JSON of the Grafana dashboard to be provisioned. You can get the JSON by exporting a dashboard in the Grafana web interface + ProviderSpecificConfig interface{} // Provider-specific configuration for the Grafana task } //go:embed files/grafana/config/config.ini @@ -25,11 +26,13 @@ var grafanaDatasourceTemplate string //go:embed files/grafana/config/dashboards.yml var grafanaDashboardProvisioningConfig string +// SetupGrafanaTask sets up and configures (but does not start) a Grafana task. +// Additionally, it creates a Prometheus datasource and a dashboard (given the DashboardJSON in GrafanaOptions). func SetupGrafanaTask(ctx context.Context, logger *zap.Logger, p provider.Provider, opts GrafanaOptions) (*provider.Task, error) { task, err := provider.CreateTask(ctx, logger, p, provider.TaskDefinition{ Name: "grafana", Image: provider.ImageDefinition{ - Image: "grafana/grafana:latest", + Image: "grafana/grafana:main", UID: "472", GID: "0", }, diff --git a/general/monitoring/grafana_snapshot.go b/general/monitoring/grafana_snapshot.go index 2009d34..52c37f8 100644 --- a/general/monitoring/grafana_snapshot.go +++ b/general/monitoring/grafana_snapshot.go @@ -8,8 +8,10 @@ import ( "time" ) +// SnapshotGrafanaDashboard takes a snapshot of a grafana dashboard and returns the snapshot URL. This function uses +// the rod library to control a headless browser. func SnapshotGrafanaDashboard(ctx context.Context, uid, grafanaURL string) (string, error) { - url, err := launcher.New().Launch() + url, err := launcher.New().Headless(false).Launch() if err != nil { return "", err @@ -87,7 +89,7 @@ func SnapshotGrafanaDashboard(ctx context.Context, uid, grafanaURL string) (stri return "", err } - shareDashboardButton, err := page.Element("[aria-label='Share dashboard']") + shareDashboardButton, err := page.Element("[data-testid='data-testid share-button']") if err != nil { return "", err diff --git a/general/monitoring/prometheus.go b/general/monitoring/prometheus.go index a58d5ee..4d7b5b4 100644 --- a/general/monitoring/prometheus.go +++ b/general/monitoring/prometheus.go @@ -5,6 +5,7 @@ import ( "context" _ "embed" "fmt" + "github.com/skip-mev/petri/general/v2/provider" "go.uber.org/zap" "strings" "text/template" @@ -18,11 +19,13 @@ type PrometheusOptions struct { ProviderSpecificConfig interface{} } +// SetupPrometheusTask sets up and configures (but does not start) a Prometheus task. +// Additionally, it creates a Prometheus configuration file (given the Targets in PrometheusOptions). func SetupPrometheusTask(ctx context.Context, logger *zap.Logger, p provider.Provider, opts PrometheusOptions) (*provider.Task, error) { task, err := provider.CreateTask(ctx, logger, p, provider.TaskDefinition{ Name: "prometheus", Image: provider.ImageDefinition{ - Image: "prom/prometheus:latest", + Image: "prom/prometheus:v2.46.0", UID: "65534", GID: "65534", }, diff --git a/general/provider/definitions.go b/general/provider/definitions.go index 6a8ed09..2d9680a 100644 --- a/general/provider/definitions.go +++ b/general/provider/definitions.go @@ -1,11 +1,14 @@ package provider +// VolumeDefinition defines the configuration for a volume. Some providers might not support creating a volume, +// but this detail is abstracted away when CreateTask is used type VolumeDefinition struct { Name string MountPath string Size string } +// TaskDefinition defines the configuration for a task type TaskDefinition struct { Name string // Name is used when generating volumes, etc. - additional resources for the container ContainerName string // ContainerName is used for the actual container / job name @@ -21,6 +24,7 @@ type TaskDefinition struct { ProviderSpecificConfig interface{} } +// ImageDefinition defines the details of a specific Docker image type ImageDefinition struct { Image string UID string diff --git a/general/provider/digitalocean/digitalocean_provider.go b/general/provider/digitalocean/digitalocean_provider.go index a364a65..98252aa 100644 --- a/general/provider/digitalocean/digitalocean_provider.go +++ b/general/provider/digitalocean/digitalocean_provider.go @@ -32,6 +32,8 @@ type Provider struct { containers map[string]string } +// NewDigitalOceanProvider creates a provider that implements the Provider interface for DigitalOcean. +// Token is the DigitalOcean API token func NewDigitalOceanProvider(ctx context.Context, logger *zap.Logger, providerName string, token string) (*Provider, error) { doClient := godo.NewFromToken(token) @@ -96,6 +98,7 @@ func (p *Provider) Teardown(ctx context.Context) error { return nil } +// TODO: Implement teardownTasks func (p *Provider) teardownTasks(ctx context.Context) error { return nil } diff --git a/general/provider/docker/docker_provider.go b/general/provider/docker/docker_provider.go index fcbdc6c..fe2f5f0 100644 --- a/general/provider/docker/docker_provider.go +++ b/general/provider/docker/docker_provider.go @@ -28,6 +28,8 @@ type Provider struct { listeners map[string]Listeners } +// NewDockerProvider creates a provider that implements the Provider interface for Docker. It uses the default +// Docker client options unless provided with additional options func NewDockerProvider(ctx context.Context, logger *zap.Logger, providerName string, dockerOpts ...client.Opt) (*Provider, error) { dockerClient, err := client.NewClientWithOpts(dockerOpts...) if err != nil { diff --git a/general/provider/provider.go b/general/provider/provider.go index 89368a7..dac1520 100644 --- a/general/provider/provider.go +++ b/general/provider/provider.go @@ -5,6 +5,7 @@ import ( "go.uber.org/zap" ) +// TaskStatus defines the status of a task's underlying workload type TaskStatus int const ( @@ -14,6 +15,7 @@ const ( TASK_PAUSED ) +// Task is a stateful object that holds the underlying workload's details and tracks the workload's lifecycle type Task struct { Provider Provider @@ -27,6 +29,8 @@ type Task struct { PostStop func(context.Context, *Task) error } +// Provider is the representation of any infrastructure provider that can handle +// running arbitrary Docker workloads type Provider interface { CreateTask(context.Context, *zap.Logger, TaskDefinition) (string, error) StartTask(context.Context, string) error diff --git a/general/provider/task.go b/general/provider/task.go index 9a89246..abe6a22 100644 --- a/general/provider/task.go +++ b/general/provider/task.go @@ -6,6 +6,7 @@ import ( "go.uber.org/zap" ) +// CreateTask creates a task structure and sets up its underlying workload on a provider, including sidecars if there are any in the definition func CreateTask(ctx context.Context, logger *zap.Logger, provider Provider, definition TaskDefinition) (*Task, error) { task := &Task{ Provider: provider, @@ -46,6 +47,7 @@ func CreateTask(ctx context.Context, logger *zap.Logger, provider Provider, defi return task, nil } +// Start starts the underlying task's workload including its sidecars if startSidecars is set to true func (t *Task) Start(ctx context.Context, startSidecars bool) error { if startSidecars { for _, sidecar := range t.Sidecars { @@ -71,6 +73,7 @@ func (t *Task) Start(ctx context.Context, startSidecars bool) error { return nil } +// Stop stops the underlying task's workload including its sidecars if stopSidecars is set to true func (t *Task) Stop(ctx context.Context, stopSidecars bool) error { if stopSidecars { for _, sidecar := range t.Sidecars { @@ -97,29 +100,33 @@ func (t *Task) Stop(ctx context.Context, stopSidecars bool) error { return nil } +// WriteFile writes to a file in the task's volume at a relative path func (t *Task) WriteFile(ctx context.Context, path string, bz []byte) error { return t.Provider.WriteFile(ctx, t.ID, path, bz) } +// ReadFile returns a file's contents in the task's volume at a relative path func (t *Task) ReadFile(ctx context.Context, path string) ([]byte, error) { return t.Provider.ReadFile(ctx, t.ID, path) } +// DownloadDir downloads a directory from the task's volume at path relPath to a local path localPath func (t *Task) DownloadDir(ctx context.Context, relPath, localPath string) error { return t.Provider.DownloadDir(ctx, t.ID, relPath, localPath) } +// GetIP returns the task's IP func (t *Task) GetIP(ctx context.Context) (string, error) { return t.Provider.GetIP(ctx, t.ID) } // GetExternalAddress returns the external address for a specific task port in format host:port. // Providers choose the protocol to return the port for themselves. - func (t *Task) GetExternalAddress(ctx context.Context, port string) (string, error) { return t.Provider.GetExternalAddress(ctx, t.ID, port) } +// RunCommand executes a shell command on the task's workload, returning stdout/stderr, the exit code and an error if there is one func (t *Task) RunCommand(ctx context.Context, command []string) (string, string, int, error) { status, err := t.Provider.GetTaskStatus(ctx, t.ID) if err != nil { @@ -133,10 +140,12 @@ func (t *Task) RunCommand(ctx context.Context, command []string) (string, string return t.Provider.RunCommandWhileStopped(ctx, t.ID, t.Definition, command) } +// GetStatus returns the task's underlying workload's status func (t *Task) GetStatus(ctx context.Context) (TaskStatus, error) { return t.Provider.GetTaskStatus(ctx, t.ID) } +// Destroy destroys the task's underlying workload, including it's sidecars if destroySidecars is set to true func (t *Task) Destroy(ctx context.Context, destroySidecars bool) error { if destroySidecars { for _, sidecar := range t.Sidecars { @@ -155,10 +164,12 @@ func (t *Task) Destroy(ctx context.Context, destroySidecars bool) error { return nil } +// SetPreStart sets a task's hook function that gets called right before the task's underlying workload is about to be started func (t *Task) SetPreStart(f func(context.Context, *Task) error) { t.PreStart = f } +// SetPostStop sets a task's hook function that gets called right after the task's underlying workload is stopped func (t *Task) SetPostStop(f func(context.Context, *Task) error) { t.PostStop = f } diff --git a/general/types/chain.go b/general/types/chain.go index 35702fa..cb99143 100644 --- a/general/types/chain.go +++ b/general/types/chain.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc" ) +// ChainI is an interface for a logical chain type ChainI interface { Init(context.Context) error Teardown(context.Context) error @@ -31,41 +32,44 @@ type ChainI interface { WaitForHeight(ctx context.Context, desiredHeight uint64) error } +// ChainConfig is the configuration structure for a logical chain. +// It contains all the relevant details needed to create a Cosmos chain and it's sidecars type ChainConfig struct { - Denom string - Decimals uint64 - NumValidators int - NumNodes int + Denom string // Denom is the denomination of the native staking token + Decimals uint64 // Decimals is the number of decimals of the native staking token + NumValidators int // NumValidators is the number of validators to create + NumNodes int // NumNodes is the number of nodes to create - BinaryName string + BinaryName string // BinaryName is the name of the chain binary in the Docker image - Image provider.ImageDefinition - SidecarImage provider.ImageDefinition + Image provider.ImageDefinition // Image is the Docker ImageDefinition of the chain + SidecarImage provider.ImageDefinition // SidecarImage is the Docker ImageDefinition of the chain sidecar - GasPrices string - GasAdjustment float64 + GasPrices string // GasPrices are the minimum gas prices to set on the chain + GasAdjustment float64 // GasAdjustment is the margin by which to multiply the default gas prices - Bech32Prefix string + Bech32Prefix string // Bech32Prefix is the Bech32 prefix of the on-chain addresses - EncodingConfig testutil.TestEncodingConfig + EncodingConfig testutil.TestEncodingConfig // EncodingConfig is the encoding config of the chain - HomeDir string - SidecarHomeDir string - SidecarPorts []string - SidecarArgs []string + HomeDir string // HomeDir is the home directory of the chain + SidecarHomeDir string // SidecarHomeDir is the home directory of the chain sidecar + SidecarPorts []string // SidecarPorts are the ports to expose on the chain sidecar + SidecarArgs []string // SidecarArgs are the arguments to launch the chain sidecar - CoinType string - HDPath string - ChainId string + CoinType string // CoinType is the coin type of the chain (e.g. 118) + HDPath string // HDPath is the HD path of the chain (e.g. m/44'/118'/0'/0/0) + ChainId string // ChainId is the chain ID of the chain - ModifyGenesis GenesisModifier + ModifyGenesis GenesisModifier // ModifyGenesis is a function that modifies the genesis bytes of the chain - WalletConfig WalletConfig + WalletConfig WalletConfig // WalletConfig is the default configuration of a chain's wallet - UseGenesisSubCommand bool + UseGenesisSubCommand bool // UseGenesisSubCommand is a flag that indicates whether to use the 'genesis' subcommand to initialize the chain. Set to true if Cosmos SDK >v0.50 - NodeCreator NodeCreator - NodeDefinitionModifier NodeDefinitionModifier + NodeCreator NodeCreator // NodeCreator is a function that creates a node + NodeDefinitionModifier NodeDefinitionModifier // NodeDefinitionModifier is a function that modifies a node's definition } +// GenesisModifier is a function that takes in genesis bytes and returns modified genesis bytes type GenesisModifier func([]byte) ([]byte, error) diff --git a/general/types/constants.go b/general/types/constants.go index a9d5af9..36d7661 100644 --- a/general/types/constants.go +++ b/general/types/constants.go @@ -1,6 +1,8 @@ package types const ( - ValidatorKeyName = "validator" + // ValidatorKeyName is the key name for the validator key + ValidatorKeyName = "validator" + // FaucetAccountKeyName is the key name for the faucet account key FaucetAccountKeyName = "faucet" ) diff --git a/general/types/node.go b/general/types/node.go index fb57e1b..c07c4c9 100644 --- a/general/types/node.go +++ b/general/types/node.go @@ -9,46 +9,73 @@ import ( "google.golang.org/grpc" ) +// NodeConfig is the configuration structure for a logical node. type NodeConfig struct { - Name string - Index int + Name string // Name is the name of the node + Index int // Index denotes which node this is in the Validators/Nodes array - IsValidator bool + IsValidator bool // IsValidator denotes whether this node is a validator - Chain ChainI - Provider provider.Provider + Chain ChainI // Chain is the chain this node is running on + Provider provider.Provider // Provider is the provider this node is running on } +// NodeDefinitionModifier is a type of function that given a NodeConfig modifies the task definition. It usually +// adds additional sidecars or modifies the entrypoint. This function is typically called in NodeCreator +// before the task is created type NodeDefinitionModifier func(provider.TaskDefinition, NodeConfig) provider.TaskDefinition +// NodeCreator is a type of function that given a NodeConfig creates a new logical node type NodeCreator func(context.Context, *zap.Logger, NodeConfig) (NodeI, error) +// NodeI represents an interface for a logical node that is running on a chain type NodeI interface { + // GetConfig returns the configuration of the node GetConfig() NodeConfig + // GetTMClient returns the CometBFT RPC client of the node GetTMClient(context.Context) (*rpchttp.HTTP, error) + // GetGRPCClient returns the gRPC client of the node GetGRPCClient(context.Context) (*grpc.ClientConn, error) + + // Height returns the current height of the node Height(context.Context) (uint64, error) + // InitHome creates a home directory on the node InitHome(context.Context) error + // AddGenesisAccount adds a genesis account to the node's genesis file AddGenesisAccount(context.Context, string, []sdk.Coin) error + // GenerateGenTx creates a genesis transaction using the validator's key on the node GenerateGenTx(context.Context, sdk.Coin) error + // CopyGenTx copies the genesis transaction to another node CopyGenTx(context.Context, NodeI) error + // CollectGenTxs collects all of the genesis transactions on the node and creates the genesis file CollectGenTxs(context.Context) error + // GenesisFileContent returns the contents of the genesis file on the node GenesisFileContent(context.Context) ([]byte, error) + // OverwriteGenesisFile overwrites the genesis file on the node with the given contents OverwriteGenesisFile(context.Context, []byte) error + // CreateWallet creates a Cosmos wallet on the node CreateWallet(context.Context, string, WalletConfig) (WalletI, error) + // RecoverKey creates a Cosmos wallet on the node given a mnemonic RecoverKey(context.Context, string, string) error + // KeyBech32 returns the Bech32 address of a key on the node KeyBech32(context.Context, string, string) (string, error) + // SetDefaultConfigs sets the default configurations for the app and consensus of the node SetDefaultConfigs(context.Context) error + // SetPersistentPeers takes in a comma-delimited peer string (nodeid1@host1:port1,nodeid2@host2:port2) and writes it + // to the consensus config file on the node SetPersistentPeers(context.Context, string) error + // NodeId returns the p2p peer ID of the node NodeId(context.Context) (string, error) + // GetTask returns the underlying node's Task GetTask() *provider.Task + // GetIP returns the IP address of the node GetIP(context.Context) (string, error) } diff --git a/general/types/types.go b/general/types/types.go index 1396a47..c68737f 100644 --- a/general/types/types.go +++ b/general/types/types.go @@ -2,18 +2,27 @@ package types import cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +// WalletI is an interface for a Cosmos SDK type wallet type WalletI interface { + // FormattedAddress should return a Bech32 formatted address using the default prefix FormattedAddress() string + // KeyName should return the name of the key KeyName() string + // Address should return the byte-encoded address Address() []byte + // FormattedAddressWithPrefix should return a Bech32 formatted address using the given prefix FormattedAddressWithPrefix(prefix string) string + // PublicKey should return the public key of the wallet PublicKey() (cryptotypes.PubKey, error) + // PrivateKey should return the private key of the wallet PrivateKey() (cryptotypes.PrivKey, error) + // Mnemonic should return the mnemonic of the wallet Mnemonic() string } +// GasSettings is a configuration for Cosmos app gas settings type GasSettings struct { - Gas int64 - PricePerGas int64 - GasDenom string + Gas int64 // Gas is the gas limit + PricePerGas int64 // PricePerGas is the token price per gas + GasDenom string // GasDenom is the denomination of the gas token } diff --git a/general/types/wallet.go b/general/types/wallet.go index 3e0bf76..4de989e 100644 --- a/general/types/wallet.go +++ b/general/types/wallet.go @@ -2,10 +2,11 @@ package types import "github.com/cosmos/cosmos-sdk/crypto/hd" +// WalletConfig is a configuration for a Cosmos SDK type wallet type WalletConfig struct { - DerivationFn hd.DeriveFn - GenerationFn hd.GenerateFn - Bech32Prefix string - HDPath *hd.BIP44Params - SigningAlgorithm string + DerivationFn hd.DeriveFn // DerivationFn is the function to derive a seed from a mnemonic + GenerationFn hd.GenerateFn // GenerateFn is the function to derive a private key from a seed + Bech32Prefix string // Bech32Prefix is the default prefix that should be used for formatting addresses + HDPath *hd.BIP44Params // HDPath is the default HD path to use for deriving keys + SigningAlgorithm string // SigningAlgorithm is the default signing algorithm to use } diff --git a/general/util/random.go b/general/util/random.go index cef5129..ba0576e 100644 --- a/general/util/random.go +++ b/general/util/random.go @@ -6,6 +6,7 @@ import ( var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +// RandomString returns a string that consists of n random valid English alphabet characters func RandomString(n int) string { b := make([]rune, n) for i := range b { diff --git a/general/util/tar.go b/general/util/tar.go index b5087b4..732dff3 100644 --- a/general/util/tar.go +++ b/general/util/tar.go @@ -4,7 +4,6 @@ import ( "archive/tar" "bytes" "errors" - "fmt" "io" ) @@ -40,33 +39,3 @@ func MakeSingleFileTar(name string, file io.Reader) (io.Reader, error) { return b, nil } - -func UnarchiveSingleFileTar(archive io.Reader) (string, io.Reader, error) { - tarReader := tar.NewReader(archive) - - for { - header, err := tarReader.Next() - - if err == io.EOF { - return "", nil, fmt.Errorf("no files found in tar archive") - } - - if err != nil { - return "", nil, fmt.Errorf("extract failed: %v", err) - } - - if header.Typeflag != tar.TypeReg { - continue - } - - b := bytes.NewBuffer([]byte{}) - - _, err = io.Copy(b, tarReader) - - if err != nil { - return "", nil, fmt.Errorf("extract failed: %v", err) - } - - return header.Name, b, nil - } -} diff --git a/general/util/wait.go b/general/util/wait.go index c1ae9ca..c114830 100644 --- a/general/util/wait.go +++ b/general/util/wait.go @@ -6,6 +6,7 @@ import ( "time" ) +// WaitForCondition polls a function until it returns true or an error func WaitForCondition(ctx context.Context, timeoutAfter, pollingInterval time.Duration, fn func() (bool, error)) error { ctx, cancel := context.WithTimeout(ctx, timeoutAfter) defer cancel()