package ante
import (
"slices"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
errorsmod "cosmossdk.io/errors"
)
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 its
// nonce incremented, which allows fire-and-forget transaction broadcasting,
// removing the necessity of ordering on the sender-side.
//
// 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 state 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 unordered sequence state.
type UnorderedTxDecorator struct {
// maxUnOrderedTTL defines the maximum TTL a transaction can define.
maxTimeoutDuration time.Duration
txManager authkeeper.UnorderedTxManager
}
func NewUnorderedTxDecorator(
utxm authkeeper.UnorderedTxManager,
) *UnorderedTxDecorator {
return &UnorderedTxDecorator{
maxTimeoutDuration: 10 * time.Minute,
txManager: utxm,
}
}
func (d *UnorderedTxDecorator)
AnteHandle(
ctx sdk.Context,
tx sdk.Tx,
_ bool,
next sdk.AnteHandler,
) (sdk.Context, error) {
if err := d.ValidateTx(ctx, tx); err != nil {
return ctx, err
}
return next(ctx, tx, false)
}
func (d *UnorderedTxDecorator)
ValidateTx(ctx sdk.Context, tx sdk.Tx)
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 nil
}
blockTime := ctx.BlockTime()
timeoutTimestamp := unorderedTx.GetTimeoutTimeStamp()
if timeoutTimestamp.IsZero() || timeoutTimestamp.Unix() == 0 {
return errorsmod.Wrap(
sdkerrors.ErrInvalidRequest,
"unordered transaction must have timeout_timestamp set",
)
}
if timeoutTimestamp.Before(blockTime) {
return errorsmod.Wrap(
sdkerrors.ErrInvalidRequest,
"unordered transaction has a timeout_timestamp that has already passed",
)
}
if timeoutTimestamp.After(blockTime.Add(d.maxTimeoutDuration)) {
return errorsmod.Wrapf(
sdkerrors.ErrInvalidRequest,
"unordered tx ttl exceeds %s",
d.maxTimeoutDuration.String(),
)
}
execMode := ctx.ExecMode()
if execMode == sdk.ExecModeSimulate {
return nil
}
signerAddrs, err := getSigners(tx)
if err != nil {
return err
}
for _, signer := range signerAddrs {
contains, err := d.txManager.Contains(ctx, signer, uint64(unorderedTx.GetTimeoutTimeStamp().Unix()))
if err != nil {
return errorsmod.Wrap(
sdkerrors.ErrIO,
"failed to check contains",
)
}
if contains {
return errorsmod.Wrapf(
sdkerrors.ErrInvalidRequest,
"tx is duplicated for signer %x", signer,
)
}
if err := d.txManager.Add(ctx, signer, uint64(unorderedTx.GetTimeoutTimeStamp().Unix())); err != nil {
return errorsmod.Wrap(
sdkerrors.ErrIO,
"failed to add unordered sequence to state",
)
}
}
return nil
}
func getSigners(tx sdk.Tx) ([][]byte, error) {
sigTx, ok := tx.(authsigning.SigVerifiableTx)
if !ok {
return nil, errorsmod.Wrap(sdkerrors.ErrTxDecode, "invalid tx type")
}
return sigTx.GetSigners()
}