190 lines
5.6 KiB
Go
190 lines
5.6 KiB
Go
package ante
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cosmos/gogoproto/proto"
|
|
|
|
"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.
|
|
maxTimeoutDuration time.Duration
|
|
txManager *unorderedtx.Manager
|
|
env appmodule.Environment
|
|
sha256Cost uint64
|
|
}
|
|
|
|
func NewUnorderedTxDecorator(
|
|
maxDuration time.Duration,
|
|
m *unorderedtx.Manager,
|
|
env appmodule.Environment,
|
|
gasCost uint64,
|
|
) *UnorderedTxDecorator {
|
|
return &UnorderedTxDecorator{
|
|
maxTimeoutDuration: maxDuration,
|
|
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)
|
|
}
|
|
|
|
headerInfo := d.env.HeaderService.HeaderInfo(ctx)
|
|
timeoutTimestamp := unorderedTx.GetTimeoutTimeStamp()
|
|
if timeoutTimestamp.IsZero() || timeoutTimestamp.Unix() == 0 {
|
|
return ctx, errorsmod.Wrap(
|
|
sdkerrors.ErrInvalidRequest,
|
|
"unordered transaction must have timeout_timestamp set",
|
|
)
|
|
}
|
|
if timeoutTimestamp.Before(headerInfo.Time) {
|
|
return ctx, errorsmod.Wrap(
|
|
sdkerrors.ErrInvalidRequest,
|
|
"unordered transaction has a timeout_timestamp that has already passed",
|
|
)
|
|
}
|
|
if timeoutTimestamp.After(headerInfo.Time.Add(d.maxTimeoutDuration)) {
|
|
return ctx, errorsmod.Wrapf(
|
|
sdkerrors.ErrInvalidRequest,
|
|
"unordered tx ttl exceeds %s",
|
|
d.maxTimeoutDuration.String(),
|
|
)
|
|
}
|
|
|
|
// 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(uint64(timeoutTimestamp.Unix()), 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, timeoutTimestamp)
|
|
}
|
|
|
|
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
|
|
}
|