Building an application specific blockchain using Cosmos SDK Part-6
Welcome to the last part of this series and here we will be covering a very important feature- TestSimulation
.
Similar to module manager, we have a simulation manager defined in app.go
. It is basically an object containing a list of all those modules which implement the simulation package. Every module have their own simulation
package which contains primary functions for test simulation: store decoders, randomized genesis state and parameters, weighted operations and proposal contents
. We do have these methods registered on module
with the help of starport (under simulation.go
)
Note that, along with messages, starport also scaffolds the simulation weighted operations for each message type.(module_simulation.go
). Let’s change the weighted numbers for each message type -
//module_simulation.goconst (
opWeightMsgCreateDeal = "op_weight_msg_create_chain"
defaultWeightMsgCreateDeal int = 90opWeightMsgCreateContract = "op_weight_msg_create_chain"
defaultWeightMsgCreateContract int = 90opWeightMsgCommitContract = "op_weight_msg_create_chain"
defaultWeightMsgCommitContract int = 50opWeightMsgApproveContract = "op_weight_msg_create_chain"
defaultWeightMsgApproveContract int = 40opWeightMsgShipOrder = "op_weight_msg_create_chain"
defaultWeightMsgShipOrder int = 40opWeightMsgOrderDelivered = "op_weight_msg_create_chain"
defaultWeightMsgOrderDelivered int = 40opWeightMsgCancelOrder = "op_weight_msg_create_chain"
defaultWeightMsgCancelOrder int = 20
// this line is used by starport scaffolding # simapp/module/const
)
We will be defining randomised parameters for each message type and then sign and send the transactions to the simulated chain to verify if things work as expected. Note that simulation also helps us to test invariants registered on any module if any.
An
Invariant
is a function that helps us to detect bugs early on and act upon them to limit their potential consequences (e.g. by halting the chain).
We will use small utility function to sign and send the transactions to simulated chain -
// simap.go// SendMsg sends a transaction with the specified message.func SendMsg(
r *rand.Rand, app *baseapp.BaseApp, ak types.AccountKeeper, bk types.BankKeeper, msg sdk.Msg, ctx sdk.Context, chainID string, gasValue uint64, privkeys []cryptotypes.PrivKey,
) error {
addr := msg.GetSigners()[0]
account := ak.GetAccount(ctx, addr)
coins := bk.SpendableCoins(ctx, account.GetAddress())
fees, err := simtypes.RandomFees(r, ctx, coins)
if err != nil {
return err
}
txGen := simappparams.MakeTestEncodingConfig().TxConfig
tx, err := helpers.GenTx(
txGen,
[]sdk.Msg{msg},
fees,
gasValue,
chainID,
[]uint64{account.GetAccountNumber()},
[]uint64{account.GetSequence()},
privkeys...,
)if err != nil {
return err
}
_, _, err = app.Deliver(txGen.TxEncoder(), tx)if err != nil {
return err
}
return nil
}
We will define simulation function as well for each of the message-
// simulation/create_deal.gofunc SimulateMsgCreateDeal(ak types.AccountKeeper,
bk types.BankKeeper,
k keeper.Keeper,
) simtypes.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {creatorAccount := accs[0]
vendorAccount := accs[1]
commission := r.Intn(80) + 5msg := &types.MsgCreateDeal{
Creator: creatorAccount.Address.String(),
Vendor: vendorAccount.Address.String(),
Commission: uint64(commission),
}err := SendMsg(r, app, ak, bk, msg, ctx, chainID, DefaultGasValue, []cryptotypes.PrivKey{creatorAccount.PrivKey})if err != nil {
return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "CreateDeal"), nil, nil
}return simtypes.NewOperationMsg(msg, true, "create deal", nil), nil, nil
}
}
// simulation/create_contract.gofunc SimulateMsgCreateContract(
ak types.AccountKeeper,
bk types.BankKeeper,
k keeper.Keeper,
) simtypes.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {creatorAccount := accs[0]
consumerAccount := accs[2]
dealId := strconv.Itoa(r.Intn(30))
ownerETA := strconv.Itoa(r.Intn(501))
fees := r.Uint64()
expiry := strconv.Itoa(r.Int())msg := &types.MsgCreateContract{
Creator: creatorAccount.Address.String(),
DealId: dealId,
Consumer: consumerAccount.Address.String(),
Desc: "some random order desc",
OwnerETA: ownerETA,
Expiry: expiry,
Fees: fees,
}err := SendMsg(r, app, ak, bk, msg, ctx, chainID, DefaultGasValue, []cryptotypes.PrivKey{creatorAccount.PrivKey})if err != nil {
return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "CreateContract"), nil, nil
}return simtypes.NewOperationMsg(msg, true, "create contract", nil), nil, nil
}
}
// simulation/commit_contract.gofunc SimulateMsgCommitContract(
ak types.AccountKeeper,
bk types.BankKeeper,
k keeper.Keeper,
) simtypes.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
simAccount := accs[1]
dealId := strconv.Itoa(r.Intn(20))
contractId := strconv.Itoa(r.Intn(5))
vendorETA := strconv.Itoa(r.Intn(101))msg := &types.MsgCommitContract{
Creator: simAccount.Address.String(),
DealId: dealId,
ContractId: contractId,
VendorETA: vendorETA,
}err := SendMsg(r, app, ak, bk, msg, ctx, chainID, DefaultGasValue, []cryptotypes.PrivKey{simAccount.PrivKey})if err != nil {
return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "CommitContract"), nil, nil
}
return simtypes.NewOperationMsg(msg, true, "approve contract", nil), nil, nil
}
}
Once simulation methods are defined for every message, we can test the simulation for say, 200 blocks -
starport chain simulate -v --numBlocks 200 --blockSize 50 --seed 33 --exportStatsPath ./stats.json --exportStatePath ./state.json
This will simulate the chain for block height 200 and will invoke the simulation of each module.We will get the output for all different modules simulation and our custom transaction messages simulation. It will also generate stats.json
(status for each test-case)and state.json
(blockchain state after simulation) files.
Test results from simulation of deal module -
"deal": {
"approve_contract": {
"failure": 146
},
"cancel_order": {
"failure": 77
},
"commit_contract": {
"failure": 145,
"ok": 26
},
"create_contract": {
"failure": 20,
"ok": 303
},
"create_deal": {
"ok": 316
},
"order_delivered": {
"failure": 128
},
"ship_order": {
"failure": 134
}
}
Other test cases for keepers, client CLI and genesis can be referred from source code.
What’s Next -
Kudos! You made it till the end. We can further improve our dapp and make it more production ready. Other useful tasks one can continue with -
- Develop frontend for online store and integrate it with
deal
dapp. Run txs via Keplr wallet. - Create a dashboard for query details fetched from blockchain.
- Subscribe to custom events in frontend via tendermint web-socket.
- Create a rating service to facilitate consumers rating for order delivery service.
- Optimise the data structure and remove the expired contracts from store in module method
EndBlock
itself. - Deploy dapp over a test-net with 5 different nodes using docker-compose and test it locally.
- Enable
ibc
to integrate with thebandchain
oracle. Get the price info from oracle to normalise the pricing for different kind of orders.
Refer the source code here- source code
Series
* Part-1
* Part-2
* Part-3
* Part-4
* Part-5
* Part-6
Useful Links -
Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing
Also, Read
- My Experience with Crypto Copy Trading | Coinbase Review
- CoinFLEX Review | AEX Exchange Review | UPbit Review
- AscendEx Margin Trading | Bitfinex Staking | bitFlyer Review
- Sparrow Exchange Review | Nash Exchange Review
- Uphold Card Review | Trust Wallet vs MetaMask
- Exness Review | MoonXBT Vs Bitget Vs Bingbon