Skip to content

Commit

Permalink
testnet
Browse files Browse the repository at this point in the history
  • Loading branch information
DongLieu committed Oct 14, 2024
1 parent e7af80d commit 43ca986
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 5 deletions.
5 changes: 5 additions & 0 deletions baseapp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ func SetSnapshot(snapshotStore *snapshots.Store, opts snapshottypes.SnapshotOpti
return func(app *BaseApp) { app.SetSnapshot(snapshotStore, opts) }
}

// SetStoreLoader allows us to customize the rootMultiStore initialization.
func SetStoreLoader(loader StoreLoader) func(*BaseApp) {
return func(app *BaseApp) { app.SetStoreLoader(loader) }
}

func (app *BaseApp) SetName(name string) {
if app.sealed {
panic("SetName() on sealed BaseApp")
Expand Down
296 changes: 291 additions & 5 deletions server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ import (
"github.com/cosmos/cosmos-sdk/server/types"
"github.com/cosmos/cosmos-sdk/telemetry"
sdktypes "github.com/cosmos/cosmos-sdk/types"

"github.com/cosmos/cosmos-sdk/x/genutil"
// cmtcfg "github.com/tendermint/tendermint/config"
"bufio"

Check failure on line 40 in server/start.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not `gofumpt`-ed (gofumpt)
cmtjson "github.com/tendermint/tendermint/libs/json"
cmtstate "github.com/tendermint/tendermint/proto/tendermint/state"
cmtproto "github.com/tendermint/tendermint/proto/tendermint/types"
sm "github.com/tendermint/tendermint/state"
"github.com/tendermint/tendermint/store"
cmttypes "github.com/tendermint/tendermint/types"
db "github.com/tendermint/tm-db"
"io"

Check failure on line 48 in server/start.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not `gofumpt`-ed (gofumpt)
"strings"
)

const (
Expand Down Expand Up @@ -80,6 +93,16 @@ const (
flagGRPCAddress = "grpc.address"
flagGRPCWebEnable = "grpc-web.enable"
flagGRPCWebAddress = "grpc-web.address"

// FlagIAVLLazyLoading = "iavl-lazy-loading"
FlagIAVLFastNodeModuleWhitelist = "iavl-fastnode-module-whitelist"
// testnet keys
KeyIsTestnet = "is-testnet"
KeyNewChainID = "new-chain-ID"
KeyNewOpAddr = "new-operator-addr"
KeyNewValAddr = "new-validator-addr"
KeyUserPubKey = "user-pub-key"
KeyTriggerTestnetUpgrade = "trigger-testnet-upgrade"
)

// StartCmd runs the service passed in, either stand-alone or in-process with
Expand Down Expand Up @@ -155,7 +178,14 @@ is performed. Note, when enabled, gRPC will also be automatically enabled.
return nil
},
}
addStartNodeFlags(cmd, defaultNodeHome)

// add support for all Tendermint-specific command line options
tcmd.AddNodeFlags(cmd)
return cmd
}

func addStartNodeFlags(cmd *cobra.Command, defaultNodeHome string) {
cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory")
cmd.Flags().Bool(flagWithTendermint, true, "Run abci app embedded in-process with tendermint")
cmd.Flags().String(flagAddress, "tcp://0.0.0.0:26658", "Listen address")
Expand Down Expand Up @@ -194,10 +224,6 @@ is performed. Note, when enabled, gRPC will also be automatically enabled.
cmd.Flags().Uint32(FlagStateSyncSnapshotKeepRecent, 2, "State sync snapshot to keep")

cmd.Flags().Bool(FlagDisableIAVLFastNode, false, "Disable fast node for IAVL tree")

// add support for all Tendermint-specific command line options
tcmd.AddNodeFlags(cmd)
return cmd
}

func startStandAlone(ctx *Context, appCreator types.AppCreator) error {
Expand Down Expand Up @@ -274,7 +300,15 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App
return err
}

app := appCreator(ctx.Logger, db, traceWriter, ctx.Viper)
var app types.Application
if isTestnet, ok := ctx.Viper.Get(KeyIsTestnet).(bool); ok && isTestnet {
app, err = testnetify(ctx, appCreator, db, traceWriter)
if err != nil {
return err
}
} else {
app = appCreator(ctx.Logger, db, traceWriter, ctx.Viper)
}

nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile())
if err != nil {
Expand Down Expand Up @@ -541,3 +575,255 @@ func wrapCPUProfile(ctx *Context, callback func() error) error {

return WaitForQuitSignals()
}

// InPlaceTestnetCreator utilizes the provided chainID and operatorAddress as well as the local private validator key to
// control the network represented in the data folder. This is useful to create testnets nearly identical to your
// mainnet environment.
func InPlaceTestnetCreator(testnetAppCreator types.AppCreator, defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Use: "in-place-testnet [newChainID] [newOperatorAddress]",
Short: "Create and start a testnet from current local state",
Long: `Create and start a testnet from current local state.
After utilizing this command the network will start. If the network is stopped,
the normal "start" command should be used. Re-using this command on state that
has already been modified by this command could result in unexpected behavior.
Additionally, the first block may take up to one minute to be committed, depending
on how old the block is. For instance, if a snapshot was taken weeks ago and we want
to turn this into a testnet, it is possible lots of pending state needs to be committed
(expiring locks, etc.). It is recommended that you should wait for this block to be committed
before stopping the daemon.
If the --trigger-testnet-upgrade flag is set, the upgrade handler specified by the flag will be run
on the first block of the testnet.
Regardless of whether the flag is set or not, if any new stores are introduced in the daemon being run,
those stores will be registered in order to prevent panics. Therefore, you only need to set the flag if
you want to test the upgrade handler itself.
`,
Example: "in-place-testnet localosmosis osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj",
Args: cobra.ExactArgs(2),
PreRunE: func(cmd *cobra.Command, _ []string) error {
serverCtx := GetServerContextFromCmd(cmd)
if err := serverCtx.Viper.BindPFlags(cmd.Flags()); err != nil {
return err
}
_, err := GetPruningOptionsFromFlags(serverCtx.Viper)
return err
},
RunE: func(cmd *cobra.Command, args []string) error {
serverCtx := GetServerContextFromCmd(cmd)
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
newChainID := args[0]
newOperatorAddress := args[1]
skipConfirmation, _ := cmd.Flags().GetBool("skip-confirmation")
if !skipConfirmation {
// Confirmation prompt to prevent accidental modification of state.
reader := bufio.NewReader(os.Stdin)
fmt.Println("This operation will modify state in your data folder and cannot be undone. Do you want to continue? (y/n)")
text, _ := reader.ReadString('\n')
response := strings.TrimSpace(strings.ToLower(text))
if response != "y" && response != "yes" {
fmt.Println("Operation canceled.")
return nil
}
}
serverCtx.Viper.Set(KeyIsTestnet, true)
serverCtx.Viper.Set(KeyNewChainID, newChainID)
serverCtx.Viper.Set(KeyNewOpAddr, newOperatorAddress)
return startInProcess(serverCtx, clientCtx, testnetAppCreator)
},
}
addStartNodeFlags(cmd, defaultNodeHome)
cmd.Flags().String(KeyTriggerTestnetUpgrade, "", "If set (example: \"v21\"), triggers the v21 upgrade handler to run on the first block of the testnet")
cmd.Flags().Bool("skip-confirmation", false, "Skip the confirmation prompt")
return cmd
}

// testnetify modifies both state and blockStore, allowing the provided operator address and local validator key to control the network
// that the state in the data folder represents. The chainID of the local genesis file is modified to match the provided chainID.
func testnetify(ctx *Context, testnetAppCreator types.AppCreator, db db.DB, traceWriter io.Writer) (types.Application, error) {
config := ctx.Config
newChainID, ok := ctx.Viper.Get(KeyNewChainID).(string)
if !ok {
return nil, fmt.Errorf("expected string for key %s", KeyNewChainID)
}
genDocProvider := node.DefaultGenesisDocProviderFunc(config)
// Initialize blockStore and stateDB.
blockStoreDB, err := node.DefaultDBProvider(&node.DBContext{ID: "blockstore", Config: config})
if err != nil {
return nil, err
}
blockStore := store.NewBlockStore(blockStoreDB)
stateDB, err := node.DefaultDBProvider(&node.DBContext{ID: "state", Config: config})
if err != nil {
return nil, err
}
defer blockStore.Close()
defer stateDB.Close()
privValidator := pvm.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile())
userPubKey, err := privValidator.GetPubKey()
if err != nil {
return nil, err
}
validatorAddress := userPubKey.Address()
if err != nil {
return nil, err
}
stateStore := sm.NewStore(stateDB, sm.StoreOptions{
DiscardABCIResponses: config.Storage.DiscardABCIResponses,
})
state, genDoc, err := node.LoadStateFromDBOrGenesisDocProvider(stateDB, genDocProvider)
if err != nil {
return nil, err
}
genDoc.ChainID = newChainID
genFilePath := config.GenesisFile()
if err = genutil.ExportGenesisFile(genDoc, genFilePath); err != nil {
return nil, err
}
ctx.Viper.Set(KeyNewValAddr, validatorAddress)
ctx.Viper.Set(KeyUserPubKey, userPubKey)
testnetApp := testnetAppCreator(ctx.Logger, db, traceWriter, ctx.Viper)
// We need to create a temporary proxyApp to get the initial state of the application.
// Depending on how the node was stopped, the application height can differ from the blockStore height.
// This height difference changes how we go about modifying the state.
clientCreator := proxy.NewLocalClientCreator(testnetApp)
// metrics := node.DefaultMetricsProvider(cmtcfg.DefaultConfig().Instrumentation)
// _, _, _, _, proxyMetrics := metrics(genDoc.ChainID) //nolint:dogsled
proxyApp := proxy.NewAppConns(clientCreator)
if err := proxyApp.Start(); err != nil {
return nil, fmt.Errorf("error starting proxy app connections: %v", err)
}
res, err := proxyApp.Query().InfoSync(proxy.RequestInfo)
if err != nil {
return nil, fmt.Errorf("error calling Info: %v", err)
}
err = proxyApp.Stop()
if err != nil {
return nil, err
}
appHash := res.LastBlockAppHash
appHeight := res.LastBlockHeight
var block *cmttypes.Block
switch {
case appHeight == blockStore.Height():
block = blockStore.LoadBlock(blockStore.Height())
// If the state's last blockstore height does not match the app and blockstore height, we likely stopped with the halt height flag.
if state.LastBlockHeight != appHeight {
state.LastBlockHeight = appHeight
block.AppHash = appHash
state.AppHash = appHash
} else {
// Node was likely stopped via SIGTERM, delete the next block's seen commit
err := blockStoreDB.Delete([]byte(fmt.Sprintf("SC:%v", blockStore.Height()+1)))
if err != nil {
return nil, err
}
}
case blockStore.Height() > state.LastBlockHeight:
// This state usually occurs when we gracefully stop the node.
_, err = blockStore.PruneBlocks(blockStore.Height() - 1)
if err != nil {
return nil, err
}
block = blockStore.LoadBlock(blockStore.Height())
default:
// If there is any other state, we just load the block
block = blockStore.LoadBlock(blockStore.Height())
}
block.ChainID = newChainID
state.ChainID = newChainID
block.LastBlockID = state.LastBlockID
block.LastCommit.BlockID = state.LastBlockID
// Create a vote from our validator
vote := cmttypes.Vote{
Type: cmtproto.PrecommitType,
Height: state.LastBlockHeight,
Round: 0,
BlockID: state.LastBlockID,
Timestamp: time.Now(),
ValidatorAddress: validatorAddress,
ValidatorIndex: 0,
Signature: []byte{},
}
// Sign the vote, and copy the proto changes from the act of signing to the vote itself
voteProto := vote.ToProto()
err = privValidator.SignVote(newChainID, voteProto)
if err != nil {
return nil, err
}
vote.Signature = voteProto.Signature
vote.Timestamp = voteProto.Timestamp
// Modify the block's lastCommit to be signed only by our validator
block.LastCommit.Signatures[0].ValidatorAddress = validatorAddress
block.LastCommit.Signatures[0].Signature = vote.Signature
block.LastCommit.Signatures = []cmttypes.CommitSig{block.LastCommit.Signatures[0]}
// Load the seenCommit of the lastBlockHeight and modify it to be signed from our validator
seenCommit := blockStore.LoadSeenCommit(state.LastBlockHeight)
seenCommit.BlockID = state.LastBlockID
seenCommit.Round = vote.Round
seenCommit.Signatures[0].Signature = vote.Signature
seenCommit.Signatures[0].ValidatorAddress = validatorAddress
seenCommit.Signatures[0].Timestamp = vote.Timestamp
seenCommit.Signatures = []cmttypes.CommitSig{seenCommit.Signatures[0]}
err = blockStore.SaveSeenCommit(state.LastBlockHeight, seenCommit)
if err != nil {
return nil, err
}
// Create ValidatorSet struct containing just our valdiator.
newVal := &cmttypes.Validator{
Address: validatorAddress,
PubKey: userPubKey,
VotingPower: 900000000000000,
}
newValSet := &cmttypes.ValidatorSet{
Validators: []*cmttypes.Validator{newVal},
Proposer: newVal,
}
// Replace all valSets in state to be the valSet with just our validator.
state.Validators = newValSet
state.LastValidators = newValSet
state.NextValidators = newValSet
state.LastHeightValidatorsChanged = blockStore.Height()
err = stateStore.Save(state)
if err != nil {
return nil, err
}
// Create a ValidatorsInfo struct to store in stateDB.
valSet, err := state.Validators.ToProto()
if err != nil {
return nil, err
}
valInfo := &cmtstate.ValidatorsInfo{
ValidatorSet: valSet,
LastHeightChanged: state.LastBlockHeight,
}
buf, err := valInfo.Marshal()
if err != nil {
return nil, err
}
// Modfiy Validators stateDB entry.
err = stateDB.Set([]byte(fmt.Sprintf("validatorsKey:%v", blockStore.Height())), buf)
if err != nil {
return nil, err
}
// Modify LastValidators stateDB entry.
err = stateDB.Set([]byte(fmt.Sprintf("validatorsKey:%v", blockStore.Height()-1)), buf)
if err != nil {
return nil, err
}
// Modify NextValidators stateDB entry.
err = stateDB.Set([]byte(fmt.Sprintf("validatorsKey:%v", blockStore.Height()+1)), buf)
if err != nil {
return nil, err
}
b, err := cmtjson.Marshal(genDoc)
if err != nil {
return nil, err
}
if err := stateDB.SetSync([]byte("genesisDoc"), b); err != nil {
return nil, err
}
return testnetApp, err
}
7 changes: 7 additions & 0 deletions server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,13 @@ func AddCommands(rootCmd *cobra.Command, defaultNodeHome string, appCreator type
)
}

// AddTestnetCreatorCommand allows chains to create a testnet from the state existing in their node's data directory.
func AddTestnetCreatorCommand(rootCmd *cobra.Command, defaultNodeHome string, appCreator types.AppCreator, addStartFlags types.ModuleInitFlags) {
testnetCreateCmd := InPlaceTestnetCreator(appCreator, defaultNodeHome)
addStartFlags(testnetCreateCmd)
rootCmd.AddCommand(testnetCreateCmd)
}

// https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go
// TODO there must be a better way to get external IP
func ExternalIP() (string, error) {
Expand Down

0 comments on commit 43ca986

Please sign in to comment.