feat: simulate nested messages (#20291)

This commit is contained in:
Julián Toledano 2024-07-26 12:53:32 +02:00 committed by GitHub
parent cc5c4d0699
commit fe8474e9b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1392 additions and 31 deletions

View File

@ -42,6 +42,7 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i
### Features
* (baseapp) [#20291](https://github.com/cosmos/cosmos-sdk/pull/20291) Simulate nested messages.
* (tests) [#20013](https://github.com/cosmos/cosmos-sdk/pull/20013) Introduce system tests to run multi node local testnet in CI
* (runtime) [#19953](https://github.com/cosmos/cosmos-sdk/pull/19953) Implement `core/transaction.Service` in runtime.
* (client) [#19905](https://github.com/cosmos/cosmos-sdk/pull/19905) Add grpc client config to `client.toml`.

View File

@ -5,6 +5,39 @@ Note, always read the **SimApp** section for more information on application wir
## [Unreleased]
### BaseApp
#### Nested Messages Simulation
Now it is possible to simulate the nested messages of a message, providing developers with a powerful tool for
testing and predicting the behavior of complex transactions. This feature allows for a more comprehensive
evaluation of gas consumption, state changes, and potential errors that may occur when executing nested
messages. However, it's important to note that while the simulation can provide valuable insights, it does not
guarantee the correct execution of the nested messages in the future. Factors such as changes in the
blockchain state or updates to the protocol could potentially affect the actual execution of these nested
messages when the transaction is finally processed on the network.
For example, consider a governance proposal that includes nested messages to update multiple protocol
parameters. At the time of simulation, the blockchain state may be suitable for executing all these nested
messages successfully. However, by the time the actual governance proposal is executed (which could be days or
weeks later), the blockchain state might have changed significantly. As a result, while the simulation showed
a successful execution, the actual governance proposal might fail when it's finally processed.
By default, when simulating transactions, the gas cost of nested messages is not calculated. This means that
only the gas cost of the top-level message is considered. However, this behavior can be customized using the
`SetIncludeNestedMsgsGas` option when building the BaseApp. By providing a list of message types to this option,
you can specify which messages should have their nested message gas costs included in the simulation. This
allows for more accurate gas estimation for transactions involving specific message types that contain nested
messages, while maintaining the default behavior for other message types.
Here is an example on how `SetIncludeNestedMsgsGas` option could be set to calculate the gas of a gov proposal
nested messages:
```go
baseAppOptions = append(baseAppOptions, baseapp.SetIncludeNestedMsgsGas([]sdk.Message{&gov.MsgSubmitProposal{}}))
// ...
app.App = appBuilder.Build(db, traceStore, baseAppOptions...)
```
### SimApp
In this section we describe the changes made in Cosmos SDK' SimApp.

View File

@ -24,6 +24,7 @@ import (
"github.com/cosmos/gogoproto/jsonpb"
"github.com/cosmos/gogoproto/proto"
gogotypes "github.com/cosmos/gogoproto/types"
any "github.com/cosmos/gogoproto/types/any"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
@ -38,6 +39,7 @@ import (
"github.com/cosmos/cosmos-sdk/baseapp"
baseapptestutil "github.com/cosmos/cosmos-sdk/baseapp/testutil"
"github.com/cosmos/cosmos-sdk/baseapp/testutil/mock"
"github.com/cosmos/cosmos-sdk/codec"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/testutil"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
@ -760,6 +762,240 @@ func TestABCI_FinalizeBlock_MultiMsg(t *testing.T) {
require.Equal(t, int64(2), msgCounter2)
}
func anyMessage(t *testing.T, cdc codec.Codec, msg *baseapptestutil.MsgSend) *any.Any {
t.Helper()
b, err := cdc.Marshal(msg)
require.NoError(t, err)
return &any.Any{
TypeUrl: sdk.MsgTypeURL(msg),
Value: b,
}
}
func TestABCI_Query_SimulateNestedMessagesTx(t *testing.T) {
anteOpt := func(bapp *baseapp.BaseApp) {
bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
newCtx = ctx.WithGasMeter(storetypes.NewGasMeter(uint64(15)))
return
})
}
suite := NewBaseAppSuite(t, anteOpt)
_, err := suite.baseApp.InitChain(&abci.InitChainRequest{
ConsensusParams: &cmtproto.ConsensusParams{},
})
require.NoError(t, err)
baseapptestutil.RegisterNestedMessagesServer(suite.baseApp.MsgServiceRouter(), NestedMessgesServerImpl{})
baseapptestutil.RegisterSendServer(suite.baseApp.MsgServiceRouter(), SendServerImpl{})
_, _, addr := testdata.KeyTestPubAddr()
_, _, toAddr := testdata.KeyTestPubAddr()
tests := []struct {
name string
message sdk.Msg
wantErr bool
}{
{
name: "ok nested message",
message: &baseapptestutil.MsgSend{
From: addr.String(),
To: toAddr.String(),
Amount: "10000stake",
},
},
{
name: "different signers",
message: &baseapptestutil.MsgSend{
From: toAddr.String(),
To: addr.String(),
Amount: "10000stake",
},
wantErr: true,
},
{
name: "empty from",
message: &baseapptestutil.MsgSend{
From: "",
To: toAddr.String(),
Amount: "10000stake",
},
wantErr: true,
},
{
name: "empty to",
message: &baseapptestutil.MsgSend{
From: addr.String(),
To: "",
Amount: "10000stake",
},
wantErr: true,
},
{
name: "negative amount",
message: &baseapptestutil.MsgSend{
From: addr.String(),
To: toAddr.String(),
Amount: "-10000stake",
},
wantErr: true,
},
{
name: "with nested messages",
message: &baseapptestutil.MsgNestedMessages{
Signer: addr.String(),
Messages: []*any.Any{
anyMessage(t, suite.cdc, &baseapptestutil.MsgSend{
From: addr.String(),
To: toAddr.String(),
Amount: "10000stake",
}),
},
},
},
{
name: "with invalid nested messages",
message: &baseapptestutil.MsgNestedMessages{
Signer: addr.String(),
Messages: []*any.Any{
anyMessage(t, suite.cdc, &baseapptestutil.MsgSend{
From: "",
To: toAddr.String(),
Amount: "10000stake",
}),
},
},
wantErr: true,
},
{
name: "with different signer ",
message: &baseapptestutil.MsgNestedMessages{
Signer: addr.String(),
Messages: []*any.Any{
anyMessage(t, suite.cdc, &baseapptestutil.MsgSend{
From: toAddr.String(),
To: addr.String(),
Amount: "10000stake",
}),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nestedMessages := make([]*any.Any, 1)
b, err := suite.cdc.Marshal(tt.message)
require.NoError(t, err)
nestedMessages[0] = &any.Any{
TypeUrl: sdk.MsgTypeURL(tt.message),
Value: b,
}
msg := &baseapptestutil.MsgNestedMessages{
Messages: nestedMessages,
Signer: addr.String(),
}
builder := suite.txConfig.NewTxBuilder()
err = builder.SetMsgs(msg)
require.NoError(t, err)
setTxSignature(t, builder, 0)
tx := builder.GetTx()
txBytes, err := suite.txConfig.TxEncoder()(tx)
require.Nil(t, err)
_, result, err := suite.baseApp.Simulate(txBytes)
if tt.wantErr {
require.Error(t, err)
require.Nil(t, result)
} else {
require.NoError(t, err)
require.NotNil(t, result)
}
})
}
}
func TestABCI_Query_SimulateNestedMessagesGas(t *testing.T) {
anteOpt := func(bapp *baseapp.BaseApp) {
bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) {
newCtx = ctx.WithGasMeter(storetypes.NewGasMeter(uint64(10)))
return
})
}
_, _, addr := testdata.KeyTestPubAddr()
_, _, toAddr := testdata.KeyTestPubAddr()
tests := []struct {
name string
suite *BaseAppSuite
message sdk.Msg
consumedGas uint64
}{
{
name: "don't add gas",
suite: NewBaseAppSuite(t, anteOpt),
message: &baseapptestutil.MsgSend{
From: addr.String(),
To: toAddr.String(),
Amount: "10000stake",
},
consumedGas: 5,
},
{
name: "add gas",
suite: NewBaseAppSuite(t, anteOpt, baseapp.SetIncludeNestedMsgsGas([]sdk.Msg{&baseapptestutil.MsgNestedMessages{}})),
message: &baseapptestutil.MsgSend{
From: addr.String(),
To: toAddr.String(),
Amount: "10000stake",
},
consumedGas: 10,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.suite.baseApp.InitChain(&abci.InitChainRequest{
ConsensusParams: &cmtproto.ConsensusParams{},
})
require.NoError(t, err)
baseapptestutil.RegisterNestedMessagesServer(tt.suite.baseApp.MsgServiceRouter(), NestedMessgesServerImpl{})
baseapptestutil.RegisterSendServer(tt.suite.baseApp.MsgServiceRouter(), SendServerImpl{})
nestedMessages := make([]*any.Any, 1)
b, err := tt.suite.cdc.Marshal(tt.message)
require.NoError(t, err)
nestedMessages[0] = &any.Any{
TypeUrl: sdk.MsgTypeURL(tt.message),
Value: b,
}
msg := &baseapptestutil.MsgNestedMessages{
Messages: nestedMessages,
Signer: addr.String(),
}
builder := tt.suite.txConfig.NewTxBuilder()
err = builder.SetMsgs(msg)
require.NoError(t, err)
setTxSignature(t, builder, 0)
tx := builder.GetTx()
txBytes, err := tt.suite.txConfig.TxEncoder()(tx)
require.Nil(t, err)
gas, result, err := tt.suite.baseApp.Simulate(txBytes)
require.NoError(t, err)
require.NotNil(t, result)
require.True(t, gas.GasUsed == tt.consumedGas)
})
}
}
func TestABCI_Query_SimulateTx(t *testing.T) {
gasConsumed := uint64(5)
anteOpt := func(bapp *baseapp.BaseApp) {

View File

@ -186,6 +186,9 @@ type BaseApp struct {
// including the goroutine handling.This is experimental and must be enabled
// by developers.
optimisticExec *oe.OptimisticExecution
// includeNestedMsgsGas holds a set of message types for which gas costs for its nested messages are calculated.
includeNestedMsgsGas map[string]struct{}
}
// NewBaseApp returns a reference to an initialized BaseApp. It accepts a
@ -233,7 +236,9 @@ func NewBaseApp(
if app.interBlockCache != nil {
app.cms.SetInterBlockCache(app.interBlockCache)
}
if app.includeNestedMsgsGas == nil {
app.includeNestedMsgsGas = make(map[string]struct{})
}
app.runTxRecoveryMiddleware = newDefaultRecoveryMiddleware()
// Initialize with an empty interface registry to avoid nil pointer dereference.
@ -811,6 +816,10 @@ func (app *BaseApp) endBlock(_ context.Context) (sdk.EndBlock, error) {
return endblock, nil
}
type HasNestedMsgs interface {
GetMsgs() ([]sdk.Msg, error)
}
// runTx processes a transaction within a given execution mode, encoded transaction
// bytes, and the decoded transaction itself. All state transitions occur through
// a cached Context depending on the mode provided. State only gets persisted
@ -955,6 +964,15 @@ func (app *BaseApp) runTx(mode execMode, txBytes []byte) (gInfo sdk.GasInfo, res
result, err = app.runMsgs(runMsgCtx, msgs, reflectMsgs, mode)
}
if mode == execModeSimulate {
for _, msg := range msgs {
nestedErr := app.simulateNestedMessages(ctx, msg)
if nestedErr != nil {
return gInfo, nil, anteEvents, nestedErr
}
}
}
// Run optional postHandlers (should run regardless of the execution result).
//
// Note: If the postHandler fails, we also revert the runMsgs state.
@ -1061,6 +1079,49 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, reflectMsgs []proto
}, nil
}
// simulateNestedMessages simulates a message nested messages.
func (app *BaseApp) simulateNestedMessages(ctx sdk.Context, msg sdk.Msg) error {
nestedMsgs, ok := msg.(HasNestedMsgs)
if !ok {
return nil
}
msgs, err := nestedMsgs.GetMsgs()
if err != nil {
return err
}
if err := validateBasicTxMsgs(app.msgServiceRouter, msgs); err != nil {
return err
}
for _, msg := range msgs {
err = app.simulateNestedMessages(ctx, msg)
if err != nil {
return err
}
}
protoMessages := make([]protoreflect.Message, len(msgs))
for i, msg := range msgs {
_, protoMsg, err := app.cdc.GetMsgSigners(msg)
if err != nil {
return err
}
protoMessages[i] = protoMsg
}
initialGas := ctx.GasMeter().GasConsumed()
_, err = app.runMsgs(ctx, msgs, protoMessages, execModeSimulate)
if err == nil {
if _, includeGas := app.includeNestedMsgsGas[sdk.MsgTypeURL(msg)]; !includeGas {
consumedGas := ctx.GasMeter().GasConsumed() - initialGas
ctx.GasMeter().RefundGas(consumedGas, "simulation of nested messages")
}
}
return err
}
// makeABCIData generates the Data field to be sent to ABCI Check/DeliverTx.
func makeABCIData(msgResponses []*codectypes.Any) ([]byte, error) {
return proto.Marshal(&sdk.TxMsgData{MsgResponses: msgResponses})

View File

@ -119,6 +119,19 @@ func SetOptimisticExecution(opts ...func(*oe.OptimisticExecution)) func(*BaseApp
}
}
// SetIncludeNestedMsgsGas sets the message types for which gas costs for its nested messages are calculated when simulating.
func SetIncludeNestedMsgsGas(msgs []sdk.Msg) func(*BaseApp) {
return func(app *BaseApp) {
app.includeNestedMsgsGas = make(map[string]struct{})
for _, msg := range msgs {
if _, ok := msg.(HasNestedMsgs); !ok {
continue
}
app.includeNestedMsgsGas[sdk.MsgTypeURL(msg)] = struct{}{}
}
}
}
func (app *BaseApp) SetName(name string) {
if app.sealed {
panic("SetName() on sealed BaseApp")

View File

@ -3,6 +3,7 @@ package testutil
import (
errorsmod "cosmossdk.io/errors"
codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil"
"github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -16,10 +17,15 @@ func RegisterInterfaces(registry types.InterfaceRegistry) {
&MsgCounter{},
&MsgCounter2{},
&MsgKeyValue{},
&MsgNestedMessages{},
&MsgSend{},
)
msgservice.RegisterMsgServiceDesc(registry, &_Counter_serviceDesc)
msgservice.RegisterMsgServiceDesc(registry, &_Counter2_serviceDesc)
msgservice.RegisterMsgServiceDesc(registry, &_KeyValue_serviceDesc)
msgservice.RegisterMsgServiceDesc(registry, &_NestedMessages_serviceDesc)
msgservice.RegisterMsgServiceDesc(registry, &_Send_serviceDesc)
codec.RegisterInterfaces(registry)
}
@ -63,3 +69,21 @@ func (msg *MsgKeyValue) ValidateBasic() error {
}
return nil
}
func (msg *MsgNestedMessages) GetMsgs() ([]sdk.Msg, error) {
cdc := codectestutil.CodecOptions{}.NewCodec()
RegisterInterfaces(cdc.InterfaceRegistry())
msgs := make([]sdk.Msg, len(msg.GetMessages()))
for i, m := range msg.GetMessages() {
mm, err := cdc.InterfaceRegistry().Resolve(m.TypeUrl)
if err != nil {
return nil, err
}
err = cdc.UnpackAny(m, &mm)
if err != nil {
return nil, err
}
msgs[i] = mm
}
return msgs, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,25 @@ message MsgKeyValue {
message MsgCreateKeyValueResponse {}
message MsgSend {
option (cosmos.msg.v1.signer) = "from";
string from = 1;
string to = 2;
string amount = 3;
}
message MsgSendResponse {}
message MsgNestedMessages {
option (cosmos.msg.v1.signer) = "signer";
repeated google.protobuf.Any messages = 1;
string signer = 2;
}
message MsgCreateNestedMessagesResponse {}
service Counter {
rpc IncrementCounter(MsgCounter) returns (MsgCreateCounterResponse);
}
@ -44,4 +63,12 @@ service Counter2 {
service KeyValue {
rpc Set(MsgKeyValue) returns (MsgCreateKeyValueResponse);
}
}
service Send {
rpc Send(MsgSend) returns (MsgSendResponse);
}
service NestedMessages {
rpc Check(MsgNestedMessages) returns (MsgCreateNestedMessagesResponse);
}

View File

@ -32,6 +32,7 @@ import (
baseapptestutil "github.com/cosmos/cosmos-sdk/baseapp/testutil"
"github.com/cosmos/cosmos-sdk/client"
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -375,3 +376,72 @@ func wonkyMsg(t *testing.T, cfg client.TxConfig, tx signing.Tx) signing.Tx {
require.NoError(t, err)
return builder.GetTx()
}
type SendServerImpl struct {
gas uint64
}
func (s SendServerImpl) Send(ctx context.Context, send *baseapptestutil.MsgSend) (*baseapptestutil.MsgSendResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
if send.From == "" {
return nil, errors.New("from address cannot be empty")
}
if send.To == "" {
return nil, errors.New("to address cannot be empty")
}
_, err := sdk.ParseCoinNormalized(send.Amount)
if err != nil {
return nil, err
}
gas := s.gas
if gas == 0 {
gas = 5
}
sdkCtx.GasMeter().ConsumeGas(gas, "send test")
return &baseapptestutil.MsgSendResponse{}, nil
}
type NestedMessgesServerImpl struct {
gas uint64
}
func (n NestedMessgesServerImpl) Check(ctx context.Context, message *baseapptestutil.MsgNestedMessages) (*baseapptestutil.MsgCreateNestedMessagesResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
cdc := codectestutil.CodecOptions{}.NewCodec()
baseapptestutil.RegisterInterfaces(cdc.InterfaceRegistry())
signer, _, err := cdc.GetMsgSigners(message)
if err != nil {
return nil, err
}
if len(signer) != 1 {
return nil, fmt.Errorf("expected 1 signer, got %d", len(signer))
}
msgs, err := message.GetMsgs()
if err != nil {
return nil, err
}
for _, msg := range msgs {
s, _, err := cdc.GetMsgSigners(msg)
if err != nil {
return nil, err
}
if len(s) != 1 {
return nil, fmt.Errorf("expected 1 signer, got %d", len(s))
}
if !bytes.Equal(signer[0], s[0]) {
return nil, errors.New("signer does not match")
}
}
gas := n.gas
if gas == 0 {
gas = 5
}
sdkCtx.GasMeter().ConsumeGas(gas, "nested messages test")
return nil, nil
}

View File

@ -66,6 +66,7 @@ import (
"cosmossdk.io/x/gov"
govkeeper "cosmossdk.io/x/gov/keeper"
govtypes "cosmossdk.io/x/gov/types"
govv1 "cosmossdk.io/x/gov/types/v1"
govv1beta1 "cosmossdk.io/x/gov/types/v1beta1"
"cosmossdk.io/x/group"
groupkeeper "cosmossdk.io/x/group/keeper"
@ -247,7 +248,8 @@ func NewSimApp(
voteExtHandler := NewVoteExtensionHandler()
voteExtHandler.SetHandlers(bApp)
}
baseAppOptions = append(baseAppOptions, voteExtOp, baseapp.SetOptimisticExecution())
baseAppOptions = append(baseAppOptions, voteExtOp, baseapp.SetOptimisticExecution(),
baseapp.SetIncludeNestedMsgsGas([]sdk.Msg{&govv1.MsgSubmitProposal{}}))
bApp := baseapp.NewBaseApp(appName, logger, db, txConfig.TxDecoder(), baseAppOptions...)
bApp.SetCommitMultiStoreTracer(traceStore)

View File

@ -137,7 +137,6 @@ func NewSimApp(
appOpts,
// supply the logger
logger,
// ADVANCED CONFIGURATION
//