Skip to content

Commit 4d1ee7c

Browse files
committed
add section on creating a testnets from mainnet exports
1 parent 4db9838 commit 4d1ee7c

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
---
2+
sidebar_position: 1
3+
---
4+
5+
# Application Testnets
6+
7+
Building an application is complicated and requires a lot of testing. The Cosmos SDK provides a way to test your application in a real-world environment: a testnet.
8+
9+
We allow developers to take the state from their mainnet and run tests against the state. This is useful for testing upgrade migrations, or for testing the application in a real-world environment.
10+
11+
## Testnet Setup
12+
13+
We will be breaking down the steps to create a testnet from mainnet state.
14+
15+
```go
16+
// InitMerlinAppForTestnet is broken down into two sections:
17+
// Required Changes: Changes that, if not made, will cause the testnet to halt or panic
18+
// Optional Changes: Changes to customize the testnet to one's liking (lower vote times, fund accounts, etc)
19+
func InitMerlinAppForTestnet(app *MerlinApp, newValAddr bytes.HexBytes, newValPubKey crypto.PubKey, newOperatorAddress, upgradeToTrigger string) *MerlinApp {
20+
...
21+
}
22+
```
23+
24+
### Required Changes
25+
26+
#### Staking
27+
28+
When creating a testnet the important part is migrate the validator set from many validators to one or a few. This allows developers to spin up the chain without needing to replace validator keys.
29+
30+
```go
31+
ctx := app.BaseApp.NewUncachedContext(true, tmproto.Header{})
32+
pubkey := &ed25519.PubKey{Key: newValPubKey.Bytes()}
33+
pubkeyAny, err := types.NewAnyWithValue(pubkey)
34+
if err != nil {
35+
tmos.Exit(err.Error())
36+
}
37+
38+
// STAKING
39+
//
40+
41+
// Create Validator struct for our new validator.
42+
_, bz, err := bech32.DecodeAndConvert(newOperatorAddress)
43+
if err != nil {
44+
tmos.Exit(err.Error())
45+
}
46+
bech32Addr, err := bech32.ConvertAndEncode("merlinvaloper", bz)
47+
if err != nil {
48+
tmos.Exit(err.Error())
49+
}
50+
newVal := stakingtypes.Validator{
51+
OperatorAddress: bech32Addr,
52+
ConsensusPubkey: pubkeyAny,
53+
Jailed: false,
54+
Status: stakingtypes.Bonded,
55+
Tokens: sdk.NewInt(900000000000000),
56+
DelegatorShares: sdk.MustNewDecFromStr("10000000"),
57+
Description: stakingtypes.Description{
58+
Moniker: "Testnet Validator",
59+
},
60+
Commission: stakingtypes.Commission{
61+
CommissionRates: stakingtypes.CommissionRates{
62+
Rate: sdk.MustNewDecFromStr("0.05"),
63+
MaxRate: sdk.MustNewDecFromStr("0.1"),
64+
MaxChangeRate: sdk.MustNewDecFromStr("0.05"),
65+
},
66+
},
67+
MinSelfDelegation: sdk.OneInt(),
68+
}
69+
70+
// Remove all validators from power store
71+
stakingKey := app.GetKey(stakingtypes.ModuleName)
72+
stakingStore := ctx.KVStore(stakingKey)
73+
iterator := app.StakingKeeper.ValidatorsPowerStoreIterator(ctx)
74+
for ; iterator.Valid(); iterator.Next() {
75+
stakingStore.Delete(iterator.Key())
76+
}
77+
iterator.Close()
78+
79+
// Remove all valdiators from last validators store
80+
iterator = app.StakingKeeper.LastValidatorsIterator(ctx)
81+
for ; iterator.Valid(); iterator.Next() {
82+
stakingStore.Delete(iterator.Key())
83+
}
84+
iterator.Close()
85+
86+
// Add our validator to power and last validators store
87+
app.StakingKeeper.SetValidator(ctx, newVal)
88+
err = app.StakingKeeper.SetValidatorByConsAddr(ctx, newVal)
89+
if err != nil {
90+
tmos.Exit(err.Error())
91+
}
92+
app.StakingKeeper.SetValidatorByPowerIndex(ctx, newVal)
93+
app.StakingKeeper.SetLastValidatorPower(ctx, newVal.GetOperator(), 0)
94+
if err := app.StakingKeeper.Hooks().AfterValidatorCreated(ctx, newVal.GetOperator()); err != nil {
95+
panic(err)
96+
}
97+
```
98+
99+
#### Distribution
100+
101+
Since the validator set has changed, we need to update the distribution records for the new validator.
102+
103+
104+
```go
105+
// Initialize records for this validator across all distribution stores
106+
app.DistrKeeper.SetValidatorHistoricalRewards(ctx, newVal.GetOperator(), 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1))
107+
app.DistrKeeper.SetValidatorCurrentRewards(ctx, newVal.GetOperator(), distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1))
108+
app.DistrKeeper.SetValidatorAccumulatedCommission(ctx, newVal.GetOperator(), distrtypes.InitialValidatorAccumulatedCommission())
109+
app.DistrKeeper.SetValidatorOutstandingRewards(ctx, newVal.GetOperator(), distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}})
110+
```
111+
112+
#### Slashing
113+
114+
We also need to set the validator signing info for the new validator.
115+
116+
```go
117+
// SLASHING
118+
//
119+
120+
// Set validator signing info for our new validator.
121+
newConsAddr := sdk.ConsAddress(newValAddr.Bytes())
122+
newValidatorSigningInfo := slashingtypes.ValidatorSigningInfo{
123+
Address: newConsAddr.String(),
124+
StartHeight: app.LastBlockHeight() - 1,
125+
Tombstoned: false,
126+
}
127+
app.SlashingKeeper.SetValidatorSigningInfo(ctx, newConsAddr, newValidatorSigningInfo)
128+
```
129+
130+
#### Bank
131+
132+
It is useful to create new accounts for your testing purposes. This avoids the need to have the same key as you may have on mainnet.
133+
134+
```go
135+
// BANK
136+
//
137+
138+
defaultCoins := sdk.NewCoins(sdk.NewInt64Coin("umerlin", 1000000000000))
139+
140+
localMerlinAccounts := []sdk.AccAddress{
141+
sdk.MustAccAddressFromBech32("merlin12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj"),
142+
sdk.MustAccAddressFromBech32("merlin1cyyzpxplxdzkeea7kwsydadg87357qnahakaks"),
143+
sdk.MustAccAddressFromBech32("merlin18s5lynnmx37hq4wlrw9gdn68sg2uxp5rgk26vv"),
144+
sdk.MustAccAddressFromBech32("merlin1qwexv7c6sm95lwhzn9027vyu2ccneaqad4w8ka"),
145+
sdk.MustAccAddressFromBech32("merlin14hcxlnwlqtq75ttaxf674vk6mafspg8xwgnn53"),
146+
sdk.MustAccAddressFromBech32("merlin12rr534cer5c0vj53eq4y32lcwguyy7nndt0u2t"),
147+
sdk.MustAccAddressFromBech32("merlin1nt33cjd5auzh36syym6azgc8tve0jlvklnq7jq"),
148+
sdk.MustAccAddressFromBech32("merlin10qfrpash5g2vk3hppvu45x0g860czur8ff5yx0"),
149+
sdk.MustAccAddressFromBech32("merlin1f4tvsdukfwh6s9swrc24gkuz23tp8pd3e9r5fa"),
150+
sdk.MustAccAddressFromBech32("merlin1myv43sqgnj5sm4zl98ftl45af9cfzk7nhjxjqh"),
151+
sdk.MustAccAddressFromBech32("merlin14gs9zqh8m49yy9kscjqu9h72exyf295afg6kgk"),
152+
sdk.MustAccAddressFromBech32("merlin1jllfytsz4dryxhz5tl7u73v29exsf80vz52ucc")}
153+
154+
// Fund localMerlin accounts
155+
for _, account := range localMerlinAccounts {
156+
err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins)
157+
if err != nil {
158+
tmos.Exit(err.Error())
159+
}
160+
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, account, defaultCoins)
161+
if err != nil {
162+
tmos.Exit(err.Error())
163+
}
164+
}
165+
```
166+
167+
#### Upgrade
168+
169+
If you would like to schedule an upgrade the below can be used.
170+
171+
```go
172+
// UPGRADE
173+
//
174+
175+
if upgradeToTrigger != "" {
176+
upgradePlan := upgradetypes.Plan{
177+
Name: upgradeToTrigger,
178+
Height: app.LastBlockHeight(),
179+
}
180+
err = app.UpgradeKeeper.ScheduleUpgrade(ctx, upgradePlan)
181+
if err != nil {
182+
panic(err)
183+
}
184+
}
185+
```
186+
187+
### Optional Changes
188+
189+
If you have custom modules that rely on specific state from the above modules and/or you would like to test your custom module, you will need to update the state of your custom module to reflect your needs
190+
191+
## Running the Testnet
192+
193+
Before we can run the testnet we must plug everything together.
194+
195+
in `root.go`, in the `initRootCmd` function we add:
196+
197+
```go
198+
server.AddCommands(rootCmd, merlin.DefaultNodeHome, newApp, createMerlinAppAndExport, addModuleInitFlags)
199+
++ server.AddTestnetCreatorCommand(rootCmd, merlin.DefaultNodeHome, newTestnetApp, addModuleInitFlags)
200+
```
201+
202+
Next we will add a newTestnetCommand helper function:
203+
204+
```go
205+
// newTestnetApp starts by running the normal newApp method. From there, the app interface returned is modified in order
206+
// for a testnet to be created from the provided app.
207+
func newTestnetApp(logger log.Logger, db cometbftdb.DB, traceStore io.Writer, appOpts servertypes.AppOptions) servertypes.Application {
208+
// Create an app and type cast to an MerlinApp
209+
app := newApp(logger, db, traceStore, appOpts)
210+
merlinApp, ok := app.(*merlin.MerlinApp)
211+
if !ok {
212+
panic("app created from newApp is not of type merlinApp")
213+
}
214+
215+
newValAddr, ok := appOpts.Get(server.KeyNewValAddr).(bytes.HexBytes)
216+
if !ok {
217+
panic("newValAddr is not of type bytes.HexBytes")
218+
}
219+
newValPubKey, ok := appOpts.Get(server.KeyUserPubKey).(crypto.PubKey)
220+
if !ok {
221+
panic("newValPubKey is not of type crypto.PubKey")
222+
}
223+
newOperatorAddress, ok := appOpts.Get(server.KeyNewOpAddr).(string)
224+
if !ok {
225+
panic("newOperatorAddress is not of type string")
226+
}
227+
upgradeToTrigger, ok := appOpts.Get(server.KeyTriggerTestnetUpgrade).(string)
228+
if !ok {
229+
panic("upgradeToTrigger is not of type string")
230+
}
231+
232+
// Make modifications to the normal MerlinApp required to run the network locally
233+
return meriln.InitMerlinAppForTestnet(merlinApp, newValAddr, newValPubKey, newOperatorAddress, upgradeToTrigger)
234+
}
235+
```

0 commit comments

Comments
 (0)