Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CalculateGas integration-tests & improve deterministicgas docs #706

Merged
merged 17 commits into from
Nov 13, 2023
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ go 1.20
replace (
// cosmos keyring
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0

github.com/cosmos/cosmos-sdk => ../misc/cosmos-sdk
// dgrijalva/jwt-go is deprecated and doesn't receive security updates.
// TODO(v4): remove it: https://github.com/cosmos/cosmos-sdk/issues/13134
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,6 @@ github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk=
github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis=
github.com/cosmos/cosmos-proto v1.0.0-beta.3 h1:VitvZ1lPORTVxkmF2fAp3IiA61xVwArQYKXTdEcpW6o=
github.com/cosmos/cosmos-proto v1.0.0-beta.3/go.mod h1:t8IASdLaAq+bbHbjq4p960BvcTqtwuAxid3b/2rOD6I=
github.com/cosmos/cosmos-sdk v0.47.5 h1:n1+WjP/VM/gAEOx3TqU2/Ny734rj/MX1kpUnn7zVJP8=
github.com/cosmos/cosmos-sdk v0.47.5/go.mod h1:EHwCeN9IXonsjKcjpS12MqeStdZvIdxt3VYXhus3G3c=
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw=
Expand Down
227 changes: 214 additions & 13 deletions integration-tests/modules/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@
package modules

import (
"encoding/json"
"testing"

sdkmath "cosmossdk.io/math"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
authztypes "github.com/cosmos/cosmos-sdk/x/authz"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/samber/lo"
"github.com/stretchr/testify/require"

integrationtests "github.com/CoreumFoundation/coreum/v3/integration-tests"
moduleswasm "github.com/CoreumFoundation/coreum/v3/integration-tests/contracts/modules"
"github.com/CoreumFoundation/coreum/v3/pkg/client"
"github.com/CoreumFoundation/coreum/v3/testutil/integration"
assetfttypes "github.com/CoreumFoundation/coreum/v3/x/asset/ft/types"
"github.com/CoreumFoundation/coreum/v3/x/deterministicgas"
)

// TestAuthFeeLimits verifies that invalid message gas won't be accepted.
Expand Down Expand Up @@ -126,22 +133,20 @@ func TestAuthMultisig(t *testing.T) {
ctx, chain := integrationtests.NewCoreumTestingContext(t)
requireT := require.New(t)
recipient := chain.GenAccount()
amountToSendFromMultisigAccount := int64(1000)
amountToSendFromMultisigAccount := int64(1_000_000)

multisigPublicKey, keyNamesSet, err := chain.GenMultisigAccount(3, 2)
signersCount := 7
multisigTreshold := 6
multisigPublicKey, keyNamesSet, err := chain.GenMultisigAccount(signersCount, multisigTreshold)
requireT.NoError(err)
multisigAddress := sdk.AccAddress(multisigPublicKey.Address())
signer1KeyName := keyNamesSet[0]
signer2KeyName := keyNamesSet[1]

bankClient := banktypes.NewQueryClient(chain.ClientContext)

// fund the multisig account
chain.FundAccountWithOptions(ctx, t, multisigAddress, integration.BalancesOptions{
Messages: []sdk.Msg{&banktypes.MsgSend{}},
Amount: sdkmath.NewInt(amountToSendFromMultisigAccount),
Amount: sdkmath.NewInt(amountToSendFromMultisigAccount), // for gas estimation to work wee need account to exist on chain so we fund it with to be sent amount.
})

bankClient := banktypes.NewQueryClient(chain.ClientContext)

// prepare account to be funded from the multisig
recipientAddr := recipient.String()
coinsToSendToRecipient := sdk.NewCoins(chain.NewCoin(sdkmath.NewInt(amountToSendFromMultisigAccount)))
Expand All @@ -151,26 +156,45 @@ func TestAuthMultisig(t *testing.T) {
ToAddress: recipientAddr,
Amount: coinsToSendToRecipient,
}

_, gasEstimation, err := client.CalculateGas(
ctx,
chain.ClientContext.WithFromAddress(multisigAddress),
chain.TxFactory(),
bankSendMsg,
)
requireT.NoError(err)

// fund the multisig account
chain.FundAccountWithOptions(ctx, t, multisigAddress, integration.BalancesOptions{
Amount: sdkmath.NewInt(int64(gasEstimation)), // because of 6/7 multisig gas exceeds FixedGas, and we need to fund it to pay fees.
})

_, err = chain.SignAndBroadcastMultisigTx(
ctx,
chain.ClientContext.WithFromAddress(multisigAddress),
// We intentionally use simulation instead of using `WithGas(chain.GasLimitByMsgs(bankSendMsg))`.
// We do it to test simulation for multisig account.
chain.TxFactory().WithSimulateAndExecute(true),
bankSendMsg,
signer1KeyName)
keyNamesSet[0])
requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized)
t.Log("Partially signed tx executed with expected error")

// sign and submit with the min threshold
txRes, err := chain.SignAndBroadcastMultisigTx(
ctx,
chain.ClientContext.WithFromAddress(multisigAddress),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(bankSendMsg)),
chain.TxFactory().WithSimulateAndExecute(true),
bankSendMsg,
signer1KeyName, signer2KeyName)
keyNamesSet[:multisigTreshold]...)
requireT.NoError(err)
t.Logf("Fully signed tx executed, txHash:%s", txRes.TxHash)
t.Logf("Fully signed tx executed, txHash:%s, gasUsed:%d, gasWanted:%d", txRes.TxHash, txRes.GasUsed, txRes.GasWanted)

// Real gas used might be less that estimation for multisig account (especially when there are many signers)
// because in ConsumeTxSizeGasDecorator ([email protected]/x/auth/ante/basic.go:99) amount of bytes is estimated
// for the worst case.
requireT.LessOrEqual(txRes.GasUsed, int64(gasEstimation))

recipientBalances, err := bankClient.AllBalances(ctx, &banktypes.QueryAllBalancesRequest{
Address: recipientAddr,
Expand Down Expand Up @@ -212,3 +236,180 @@ func TestAuthUnexpectedSequenceNumber(t *testing.T) {
msg)
require.True(t, cosmoserrors.ErrWrongSequence.Is(err))
}

func TestGasEstimation(t *testing.T) {
t.Parallel()

ctx, chain := integrationtests.NewCoreumTestingContext(t)

singlesigAddress := chain.GenAccount()

multisigPublicKey1, _, err := chain.GenMultisigAccount(3, 2)
require.NoError(t, err)
multisigAddress1 := sdk.AccAddress(multisigPublicKey1.Address())

multisigPublicKey2, _, err := chain.GenMultisigAccount(7, 6)
require.NoError(t, err)
multisigAddress2 := sdk.AccAddress(multisigPublicKey2.Address())

dgc := deterministicgas.DefaultConfig()
authParams, err := authtypes.NewQueryClient(chain.ClientContext).Params(ctx, &authtypes.QueryParamsRequest{})
require.NoError(t, err)

// For accounts to exist on chain we need to fund them at least with min amount (1ucore).
chain.FundAccountWithOptions(ctx, t, singlesigAddress, integration.BalancesOptions{Amount: sdkmath.NewInt(1)})
chain.FundAccountWithOptions(ctx, t, multisigAddress1, integration.BalancesOptions{Amount: sdkmath.NewInt(1)})
chain.FundAccountWithOptions(ctx, t, multisigAddress2, integration.BalancesOptions{Amount: sdkmath.NewInt(1)})

// For deterministic messages we are able to assert that gas estimation is equal to exact number.
testsDeterm := []struct {
name string
fromAddress sdk.AccAddress
msgs []sdk.Msg
expectedGas uint64
}{
{
name: "singlesig_bank_send",
fromAddress: singlesigAddress,
msgs: []sdk.Msg{
&banktypes.MsgSend{
FromAddress: singlesigAddress.String(),
ToAddress: singlesigAddress.String(),
Amount: sdk.NewCoins(chain.NewCoin(sdkmath.NewInt(1))),
},
},
// single signature no extra bytes.
expectedGas: dgc.FixedGas + 1*deterministicgas.BankSendPerCoinGas,
},
{
name: "multisig_2_3_bank_send",
fromAddress: multisigAddress1,
msgs: []sdk.Msg{
&banktypes.MsgSend{
FromAddress: multisigAddress1.String(),
ToAddress: multisigAddress1.String(),
Amount: sdk.NewCoins(chain.NewCoin(sdkmath.NewInt(1))),
},
},
// single signature no extra bytes.
expectedGas: dgc.FixedGas + 1*deterministicgas.BankSendPerCoinGas,
},
{
name: "multisig_6_7_bank_send",
fromAddress: multisigAddress2,
msgs: []sdk.Msg{
&banktypes.MsgSend{
FromAddress: multisigAddress2.String(),
ToAddress: multisigAddress2.String(),
Amount: sdk.NewCoins(chain.NewCoin(sdkmath.NewInt(1))),
},
},
// estimation uses worst case to estimate number of bytes in tx which causes possible overflow of free bytes.
// 10 is price for each extra byte over FreeBytes.
expectedGas: dgc.FixedGas + 1*deterministicgas.BankSendPerCoinGas + 1133*authParams.Params.TxSizeCostPerByte,
},
{
name: "singlesig_auth_exec_and_bank_send",
fromAddress: singlesigAddress,
msgs: []sdk.Msg{
lo.ToPtr(
authztypes.NewMsgExec(singlesigAddress, []sdk.Msg{
&banktypes.MsgSend{
FromAddress: singlesigAddress.String(),
ToAddress: singlesigAddress.String(),
Amount: sdk.NewCoins(chain.NewCoin(sdkmath.NewInt(1))),
},
})),
&banktypes.MsgSend{
FromAddress: singlesigAddress.String(),
ToAddress: singlesigAddress.String(),
Amount: sdk.NewCoins(chain.NewCoin(sdkmath.NewInt(1))),
},
},
// single signature no extra bytes.
expectedGas: dgc.FixedGas + 1*deterministicgas.BankSendPerCoinGas + (1*deterministicgas.AuthzExecOverhead + 1*deterministicgas.BankSendPerCoinGas),
},
}
for _, test := range testsDeterm {
t.Run(test.name, func(t *testing.T) {
_, estimatedGas, err := client.CalculateGas(
ctx,
chain.ClientContext.WithFromAddress(test.fromAddress),
chain.TxFactory(),
test.msgs...,
)
require.NoError(t, err)
require.Equal(t, int(test.expectedGas), int(estimatedGas))
})
}

// For non-deterministic messages we need to deploy a contract.
// Any address could be admin since we are not going to execute it but just estimate.
admin := chain.GenAccount()
chain.FundAccountWithOptions(ctx, t, admin, integration.BalancesOptions{Amount: sdkmath.NewInt(1_000_000)})

initialPayload, err := json.Marshal(moduleswasm.SimpleState{
Count: 1337,
})
require.NoError(t, err)
contractAddr, _, err := chain.Wasm.DeployAndInstantiateWASMContract(
ctx,
chain.TxFactory().WithSimulateAndExecute(true),
admin,
moduleswasm.SimpleStateWASM,
integration.InstantiateConfig{
AccessType: wasmtypes.AccessTypeUnspecified,
Payload: initialPayload,
Label: "simple_state",
},
)
require.NoError(t, err)

wasmPayload, err := moduleswasm.MethodToEmptyBodyPayload(moduleswasm.SimpleIncrement)
require.NoError(t, err)

// For non-deterministic messages we are unable to know exact number, so we do just basic assertion.
testsNonDeterm := []struct {
name string
fromAddress sdk.AccAddress
msgs []sdk.Msg
}{
{
name: "singlesig_wasm_execute_contract",
fromAddress: singlesigAddress,
msgs: []sdk.Msg{
&wasmtypes.MsgExecuteContract{
Sender: singlesigAddress.String(),
Contract: contractAddr,
Msg: wasmtypes.RawContractMessage(wasmPayload),
Funds: sdk.Coins{},
},
},
},
{
name: "multisig_2_3_wasm_execute_contract",
fromAddress: multisigAddress1,
msgs: []sdk.Msg{
&wasmtypes.MsgExecuteContract{
Sender: multisigAddress1.String(),
Contract: contractAddr,
Msg: wasmtypes.RawContractMessage(wasmPayload),
Funds: sdk.Coins{},
},
},
},
}
for _, test := range testsNonDeterm {
t.Run(test.name, func(t *testing.T) {
_, estimatedGas, err := client.CalculateGas(
ctx,
chain.ClientContext.WithFromAddress(test.fromAddress),
chain.TxFactory(),
test.msgs...,
)
require.NoError(t, err)
require.Greater(t, int(estimatedGas), 0)
})
}

}
3 changes: 1 addition & 2 deletions integration-tests/modules/bank_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,10 @@ func TestBankSendGasEstimation(t *testing.T) {
Amount: sdk.NewCoins(chain.NewCoin(amountToSend)),
}

clientCtx := chain.ClientContext.WithFromAddress(sender)
bankSendGas := chain.GasLimitByMsgs(&banktypes.MsgSend{})
_, estimatedGas, err := client.CalculateGas(
ctx,
clientCtx,
chain.ClientContext.WithFromAddress(sender),
chain.TxFactory().
WithGas(bankSendGas),
msg)
Expand Down
5 changes: 3 additions & 2 deletions pkg/client/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ func BroadcastTx(ctx context.Context, clientCtx Context, txf Factory, msgs ...sd

// CalculateGas simulates the execution of a transaction and returns the
// simulation response obtained by the query and the adjusted gas amount.
//
// FIXME(v47-multisig-calculate-gas-test) add test to calculate
// The main differences between our version and the one from cosmos-sdk are:
// - we respect context.Context
// - it works when estimating for multisig accounts.
func CalculateGas(ctx context.Context, clientCtx Context, txf Factory, msgs ...sdk.Msg) (*sdktx.SimulateResponse, uint64, error) {
txf, err := prepareFactory(ctx, clientCtx, txf)
if err != nil {
Expand Down
21 changes: 13 additions & 8 deletions x/deterministicgas/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,36 @@ complicated, nondeterministic execution path (e.g `/cosmwasm.wasm.v1.MsgExecuteC
Here is formula for the transaction

`
Gas = FixedGas + Sum(Gas for each message) + GasForExtraBytes + GasForExtraSignatures
Gas = FixedGas + max((GasForBytes + GasForSignatures - TxBaseGas), 0) + Sum(Gas for each message)
`

If message type is deterministic, then the value is looked up from the table, if it is non-deterministic, then the
required gas is determined after the execution.

`
GasForExtraBytes = max(0, TxByteSize-FreeBytes) * TxSizeCostPerByte
GasForBytes = TxByteSize * TxSizeCostPerByte
`

`
GasForExtraSignatures = max(0, NumOfSigs-FreeSigs) * SigVerifyCost
GasForSignatures = NumOfSigs * SigVerifyCost
`

Currently, we have values for the above variables as follows:

- `FixedGas`: 65000
- `TxBaseGas`: 21480
- `SigVerifyCost`: 1000
- `TxSizeCostPerByte`: 10
- `FreeSignatures`: 1
- `FreeBytes`: 2048

As an example if the transaction has 1 signature on it and is below
2048 bytes, the user will not pay anything extra, and if one of those values exceed those limits, the user will pay for
the extra resources.

To summarize user pays FixedGas as long as `GasForBytes + GasForSignatures <= TxBaseGas`.
If `GasForBytes + GasForSignatures > TxBaseGas` user will have to pay anything above `TxBaseGas` on top of `FixedGas`.

As an example if the transaction has 1 signature on it and size is below
2048 bytes, the user will not pay anything extra. Or user can have multiple signatures but fewer bytes then nothing extra should be paid.


### Full examples

Expand All @@ -50,7 +55,7 @@ Let's say we have a transaction with 1 messages of type
signatures and the tx size is 1000 bytes, total will be:

`
TotalGas = 65000 + 1 * 50000 + 1 * 1000 + max(0, 1000-2048) * 10
TotalGas = 65000 + max(0, (21480 - 1 * 1000 + 1000 * 10)) + 1 * 50000
`

#### Example 2
Expand All @@ -59,7 +64,7 @@ Let's say we have a transaction with 2 messages of type
signatures and the tx size is 2050 bytes, total will be:

`
TotalGas = 65000 + 2 * 70000 + 2 * 1000 + max(0, 2050-2048) * 10
TotalGas = 65000 + max(0, (21480 - 2 * 1000 + 2050 * 10)) + 2 * 70000
`

## Gas Tables
Expand Down
Loading
Loading