cosmos-sdk/x/auth/ante/unordered.go
Marko 2cf378174d
fix: use contents of the tx as identifier in cache (#20533)
Co-authored-by: Alexander Peters <alpe@users.noreply.github.com>
2024-07-15 09:40:41 +00:00

152 lines
5.3 KiB
Go

package ante
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"sync"
"github.com/golang/protobuf/proto" // nolint: staticcheck // for proto.Message
"cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/transaction"
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/x/auth/ante/unorderedtx"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// bufPool is a pool of bytes.Buffer objects to reduce memory allocations.
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
const DefaultSha256Cost = 25
var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil)
// UnorderedTxDecorator defines an AnteHandler decorator that is responsible for
// checking if a transaction is intended to be unordered and if so, evaluates
// the transaction accordingly. An unordered transaction will bypass having it's
// nonce incremented, which allows fire-and-forget along with possible parallel
// transaction processing, without having to deal with nonces.
//
// The transaction sender must ensure that unordered=true and a timeout_height
// is appropriately set. The AnteHandler will check that the transaction is not
// a duplicate and will evict it from memory when the timeout is reached.
//
// The UnorderedTxDecorator should be placed as early as possible in the AnteHandler
// chain to ensure that during DeliverTx, the transaction is added to the UnorderedTxManager.
type UnorderedTxDecorator struct {
// maxUnOrderedTTL defines the maximum TTL a transaction can define.
maxUnOrderedTTL uint64
txManager *unorderedtx.Manager
env appmodule.Environment
sha256Cost uint64
}
func NewUnorderedTxDecorator(maxTTL uint64, m *unorderedtx.Manager, env appmodule.Environment, gasCost uint64) *UnorderedTxDecorator {
return &UnorderedTxDecorator{
maxUnOrderedTTL: maxTTL,
txManager: m,
env: env,
sha256Cost: gasCost,
}
}
func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, _ bool, next sdk.AnteHandler) (sdk.Context, error) {
unorderedTx, ok := tx.(sdk.TxWithUnordered)
if !ok || !unorderedTx.GetUnordered() {
// If the transaction does not implement unordered capabilities or has the
// unordered value as false, we bypass.
return next(ctx, tx, false)
}
// TTL is defined as a specific block height at which this tx is no longer valid
ttl := unorderedTx.GetTimeoutHeight()
if ttl == 0 {
return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction must have timeout_height set")
}
if ttl < uint64(ctx.BlockHeight()) {
return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction has a timeout_height that has already passed")
}
if ttl > uint64(ctx.BlockHeight())+d.maxUnOrderedTTL {
return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL)
}
// consume gas in all exec modes to avoid gas estimation discrepancies
if err := d.env.GasService.GasMeter(ctx).Consume(d.sha256Cost, "consume gas for calculating tx hash"); err != nil {
return ctx, errorsmod.Wrap(sdkerrors.ErrOutOfGas, "out of gas")
}
// Avoid checking for duplicates and creating the identifier in simulation mode
// This is done to avoid sha256 computation in simulation mode
if d.env.TransactionService.ExecMode(ctx) == transaction.ExecModeSimulate {
return next(ctx, tx, false)
}
// calculate the tx hash
txHash, err := TxIdentifier(ttl, tx)
if err != nil {
return ctx, err
}
// check for duplicates
if d.txManager.Contains(txHash) {
return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "tx %X is duplicated")
}
if d.env.TransactionService.ExecMode(ctx) == transaction.ExecModeFinalize {
// a new tx included in the block, add the hash to the unordered tx manager
d.txManager.Add(txHash, ttl)
}
return next(ctx, tx, false)
}
// TxIdentifier returns a unique identifier for a transaction that is intended to be unordered.
func TxIdentifier(timeout uint64, tx sdk.Tx) ([32]byte, error) {
feetx := tx.(sdk.FeeTx)
if feetx.GetFee().IsZero() {
return [32]byte{}, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction must have a fee")
}
buf := bufPool.Get().(*bytes.Buffer)
// Make sure to reset the buffer
buf.Reset()
defer bufPool.Put(buf)
// Use the buffer
for _, msg := range tx.GetMsgs() {
// loop through the messages and write them to the buffer
// encoding the msg to bytes makes it deterministic within the state machine.
// Malleability is not a concern here because the state machine will encode the transaction deterministically.
bz, err := proto.Marshal(msg)
if err != nil {
return [32]byte{}, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "failed to marshal message")
}
if _, err := buf.Write(bz); err != nil {
return [32]byte{}, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "failed to write message to buffer")
}
}
// write the timeout height to the buffer
if err := binary.Write(buf, binary.LittleEndian, timeout); err != nil {
return [32]byte{}, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "failed to write timeout_height to buffer")
}
// write gas to the buffer
if err := binary.Write(buf, binary.LittleEndian, feetx.GetGas()); err != nil {
return [32]byte{}, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "failed to write unordered to buffer")
}
txHash := sha256.Sum256(buf.Bytes())
// Return the Buffer to the pool
return txHash, nil
}