Skip to content

Commit

Permalink
refactor: make versioned gas consts version specific
Browse files Browse the repository at this point in the history
  • Loading branch information
ninabarbakadze committed Aug 1, 2024
1 parent 978c38b commit 612c3de
Show file tree
Hide file tree
Showing 17 changed files with 516 additions and 111 deletions.
2 changes: 1 addition & 1 deletion app/ante/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewAnteHandler(
ante.NewValidateMemoDecorator(accountKeeper),
// Ensure the tx's gas limit is > the gas consumed based on the tx size.
// Side effect: consumes gas from the gas meter.
ante.NewConsumeGasForTxSizeDecorator(accountKeeper),
NewConsumeGasForTxSizeDecorator(accountKeeper),
// Ensure the feepayer (fee granter or first signer) has enough funds to pay for the tx.
// Ensure the gas price >= network min gas price if app version >= 2.
// Side effect: deducts fees from the fee payer. Sets the tx priority in context.
Expand Down
149 changes: 149 additions & 0 deletions app/ante/tx_size.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package ante

import (
"encoding/hex"

"cosmossdk.io/errors"
"github.com/celestiaorg/celestia-app/v3/pkg/appconsts"
v2 "github.com/celestiaorg/celestia-app/v3/pkg/appconsts/v2"
v3 "github.com/celestiaorg/celestia-app/v3/pkg/appconsts/v3"
"github.com/cosmos/cosmos-sdk/codec/legacy"
"github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
ante "github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
)

// simulation signature values used to estimate gas consumption
var (
// simulation signature values used to estimate gas consumption
key = make([]byte, secp256k1.PubKeySize)
simSecp256k1Pubkey = &secp256k1.PubKey{Key: key}
simSecp256k1Sig [64]byte

_ authsigning.SigVerifiableTx = (*legacytx.StdTx)(nil) // assert StdTx implements SigVerifiableTx
)

func init() {
// This decodes a valid hex string into a sepc256k1Pubkey for use in transaction simulation
bz, _ := hex.DecodeString("035AD6810A47F073553FF30D2FCC7E0D3B1C0B74B61A1AAA2582344037151E143A")
copy(key, bz)
simSecp256k1Pubkey.Key = key
}

// ConsumeTxSizeGasDecorator will take in parameters and consume gas proportional
// to the size of tx before calling next AnteHandler. Note, the gas costs will be
// slightly over estimated due to the fact that any given signing account may need
// to be retrieved from state.
//
// CONTRACT: If simulate=true, then signatures must either be completely filled
// in or empty.
// CONTRACT: To use this decorator, signatures of transaction must be represented
// as legacytx.StdSignature otherwise simulate mode will incorrectly estimate gas cost.
type ConsumeTxSizeGasDecorator struct {
ak ante.AccountKeeper
}

func NewConsumeGasForTxSizeDecorator(ak ante.AccountKeeper) ConsumeTxSizeGasDecorator {
return ConsumeTxSizeGasDecorator{
ak: ak,
}
}

func (cgts ConsumeTxSizeGasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
sigTx, ok := tx.(authsigning.SigVerifiableTx)
if !ok {
return ctx, errors.Wrap(sdkerrors.ErrTxDecode, "invalid tx type")
}
params := cgts.ak.GetParams(ctx)

consumeGasForTxSize(ctx, sdk.Gas(len(ctx.TxBytes())), params)

// simulate gas cost for signatures in simulate mode
if simulate {
// in simulate mode, each element should be a nil signature
sigs, err := sigTx.GetSignaturesV2()
if err != nil {
return ctx, err
}
n := len(sigs)

for i, signer := range sigTx.GetSigners() {
// if signature is already filled in, no need to simulate gas cost
if i < n && !isIncompleteSignature(sigs[i].Data) {
continue
}

var pubkey cryptotypes.PubKey

acc := cgts.ak.GetAccount(ctx, signer)

// use placeholder simSecp256k1Pubkey if sig is nil
if acc == nil || acc.GetPubKey() == nil {
pubkey = simSecp256k1Pubkey
} else {
pubkey = acc.GetPubKey()
}

// use stdsignature to mock the size of a full signature
simSig := legacytx.StdSignature{ //nolint:staticcheck // this will be removed when proto is ready
Signature: simSecp256k1Sig[:],
PubKey: pubkey,
}

sigBz := legacy.Cdc.MustMarshal(simSig)
cost := sdk.Gas(len(sigBz) + 6)

// If the pubkey is a multi-signature pubkey, then we estimate for the maximum
// number of signers.
if _, ok := pubkey.(*multisig.LegacyAminoPubKey); ok {
cost *= params.TxSigLimit
}

consumeGasForTxSize(ctx, cost, params)
}
}

return next(ctx, tx, simulate)
}

// isIncompleteSignature tests whether SignatureData is fully filled in for simulation purposes
func isIncompleteSignature(data signing.SignatureData) bool {
if data == nil {
return true
}

switch data := data.(type) {
case *signing.SingleSignatureData:
return len(data.Signature) == 0
case *signing.MultiSignatureData:
if len(data.Signatures) == 0 {
return true
}
for _, s := range data.Signatures {
if isIncompleteSignature(s) {
return true
}
}
}

return false
}

func consumeGasForTxSize(ctx sdk.Context, cost uint64, params auth.Params) {
// For app v2 and below we should get txSizeCostPerByte from auth module
if ctx.BlockHeader().Version.App <= v2.Version {
// fmt.Println("HERE <= 2")
ctx.GasMeter().ConsumeGas(params.TxSizeCostPerByte*cost, "txSize")
} else {
// fmt.Println("HERE < 2")
// From v3 onwards, we should get txSizeCostPerByte from appconsts
ctx.GasMeter().ConsumeGas(appconsts.TxSizeCostPerByte(v3.Version)*cost, "txSize")
}
}
190 changes: 190 additions & 0 deletions app/ante/tx_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package ante_test

import (
"fmt"
"strings"
"testing"

"github.com/celestiaorg/celestia-app/v3/app"
"github.com/celestiaorg/celestia-app/v3/app/ante"
testutil "github.com/celestiaorg/celestia-app/v3/test/util"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/tx"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/crypto/types/multisig"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
require "github.com/stretchr/testify/require"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)

func setup() (*app.App, sdk.Context, client.Context, sdk.AnteHandler, error) {
app, _, _ := testutil.NewTestAppWithGenesisSet(app.DefaultConsensusParams())
ctx := app.NewContext(false, tmproto.Header{})
app.AccountKeeper.SetParams(ctx, authtypes.DefaultParams())
ctx = ctx.WithBlockHeight(1)

// Set up TxConfig.
encodingConfig := simapp.MakeTestEncodingConfig()
// We're using TestMsg encoding in some tests, so register it here.
encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil)
testdata.RegisterInterfaces(encodingConfig.InterfaceRegistry)

clientCtx := client.Context{}.
WithTxConfig(encodingConfig.TxConfig)

anteHandler, err := authante.NewAnteHandler(
authante.HandlerOptions{
AccountKeeper: app.AccountKeeper,
BankKeeper: app.BankKeeper,
FeegrantKeeper: app.FeeGrantKeeper,
SignModeHandler: encodingConfig.TxConfig.SignModeHandler(),
SigGasConsumer: authante.DefaultSigVerificationGasConsumer,
},
)
if err != nil {
return nil, sdk.Context{}, client.Context{}, nil, fmt.Errorf("error creating AnteHandler: %v", err)
}

return app, ctx, clientCtx, anteHandler, nil
}

func TestConsumeGasForTxSize(t *testing.T) {
app, ctx, clientCtx, _, err := setup()
require.NoError(t, err)
sub, exs := app.ParamsKeeper.GetSubspace(authtypes.ModuleName)
fmt.Println(sub, exs)
var txBuilder client.TxBuilder

// keys and addresses
priv1, _, addr1 := testdata.KeyTestPubAddr()

// msg and signatures
msg := testdata.NewTestMsg(addr1)
feeAmount := testdata.NewTestFeeAmount()
gasLimit := testdata.NewTestGasLimit()

cgtsd := ante.NewConsumeGasForTxSizeDecorator(app.AccountKeeper)
antehandler := sdk.ChainAnteDecorators(cgtsd)

testCases := []struct {
name string
sigV2 signing.SignatureV2
}{
{"SingleSignatureData", signing.SignatureV2{PubKey: priv1.PubKey()}},
{"MultiSignatureData", signing.SignatureV2{PubKey: priv1.PubKey(), Data: multisig.NewMultisig(2)}},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
txBuilder = clientCtx.TxConfig.NewTxBuilder()
require.NoError(t, txBuilder.SetMsgs(msg))
txBuilder.SetFeeAmount(feeAmount)
txBuilder.SetGasLimit(gasLimit)
txBuilder.SetMemo(strings.Repeat("01234567890", 10))

privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0}
tx, err := CreateTestTx(txBuilder, clientCtx, privs, accNums, accSeqs, ctx.ChainID())
require.NoError(t, err)

txBytes, err := clientCtx.TxConfig.TxJSONEncoder()(tx)
require.Nil(t, err, "Cannot marshal tx: %v", err)

params := app.AccountKeeper.GetParams(ctx)
expectedGas := sdk.Gas(len(txBytes)) * params.TxSizeCostPerByte

// Set suite.ctx with TxBytes manually
ctx = ctx.WithTxBytes(txBytes)

// track how much gas is necessary to retrieve parameters
beforeGas := ctx.GasMeter().GasConsumed()
app.AccountKeeper.GetParams(ctx)
afterGas := ctx.GasMeter().GasConsumed()
expectedGas += afterGas - beforeGas

beforeGas = ctx.GasMeter().GasConsumed()
ctx, err = antehandler(ctx, tx, false)
require.Nil(t, err, "ConsumeTxSizeGasDecorator returned error: %v", err)

// require that decorator consumes expected amount of gas
consumedGas := ctx.GasMeter().GasConsumed() - beforeGas
require.Equal(t, expectedGas, consumedGas, "Decorator did not consume the correct amount of gas")

// simulation must not underestimate gas of this decorator even with nil signatures
txBuilder, err := clientCtx.TxConfig.WrapTxBuilder(tx)
require.NoError(t, err)
require.NoError(t, txBuilder.SetSignatures(tc.sigV2))
tx = txBuilder.GetTx()

simTxBytes, err := clientCtx.TxConfig.TxJSONEncoder()(tx)
require.Nil(t, err, "Cannot marshal tx: %v", err)
// require that simulated tx is smaller than tx with signatures
require.True(t, len(simTxBytes) < len(txBytes), "simulated tx still has signatures")

// Set suite.ctx with smaller simulated TxBytes manually
ctx = ctx.WithTxBytes(simTxBytes)

beforeSimGas := ctx.GasMeter().GasConsumed()

// run antehandler with simulate=true
ctx, err = antehandler(ctx, tx, true)
consumedSimGas := ctx.GasMeter().GasConsumed() - beforeSimGas

// require that antehandler passes and does not underestimate decorator cost
require.Nil(t, err, "ConsumeTxSizeGasDecorator returned error: %v", err)
require.True(t, consumedSimGas >= expectedGas, "Simulate mode underestimates gas on AnteDecorator. Simulated cost: %d, expected cost: %d", consumedSimGas, expectedGas)
})
}
}

// CreateTestTx is a helper function to create a tx given multiple inputs.
func CreateTestTx(txBuilder client.TxBuilder, clientCtx client.Context, privs []cryptotypes.PrivKey, accNums []uint64, accSeqs []uint64, chainID string) (xauthsigning.Tx, error) {
// First round: we gather all the signer infos. We use the "set empty
// signature" hack to do that.
sigsV2 := make([]signing.SignatureV2, 0, len(privs))
for i, priv := range privs {
sigV2 := signing.SignatureV2{
PubKey: priv.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: clientCtx.TxConfig.SignModeHandler().DefaultMode(),
Signature: nil,
},
Sequence: accSeqs[i],
}

sigsV2 = append(sigsV2, sigV2)
}
err := txBuilder.SetSignatures(sigsV2...)
if err != nil {
return nil, err
}

// Second round: all signer infos are set, so each signer can sign.
sigsV2 = []signing.SignatureV2{}
for i, priv := range privs {
signerData := xauthsigning.SignerData{
ChainID: chainID,
AccountNumber: accNums[i],
Sequence: accSeqs[i],
}
sigV2, err := tx.SignWithPrivKey(
clientCtx.TxConfig.SignModeHandler().DefaultMode(), signerData,
txBuilder, priv, clientCtx.TxConfig, accSeqs[i])
if err != nil {
return nil, err
}

sigsV2 = append(sigsV2, sigV2)
}
err = txBuilder.SetSignatures(sigsV2...)
if err != nil {
return nil, err
}

return txBuilder.GetTx(), nil
}
Loading

0 comments on commit 612c3de

Please sign in to comment.