Some checks failed
Build SimApp / build (amd64) (push) Waiting to run
Build SimApp / build (arm64) (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Build & Push / build (push) Waiting to run
Run Gosec / Gosec (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Checks dependencies and mocks generation / Check go mod tidy (push) Waiting to run
Checks dependencies and mocks generation / Check up to date mocks (push) Waiting to run
System Tests / setup (push) Waiting to run
System Tests / test-system (push) Blocked by required conditions
System Tests / test-system-legacy (push) Blocked by required conditions
Tests / Code Coverage / split-test-files (push) Waiting to run
Tests / Code Coverage / tests (00) (push) Blocked by required conditions
Tests / Code Coverage / tests (01) (push) Blocked by required conditions
Tests / Code Coverage / tests (02) (push) Blocked by required conditions
Tests / Code Coverage / tests (03) (push) Blocked by required conditions
Tests / Code Coverage / test-integration (push) Waiting to run
Tests / Code Coverage / test-e2e (push) Waiting to run
Tests / Code Coverage / repo-analysis (push) Blocked by required conditions
Tests / Code Coverage / test-sim-nondeterminism (push) Waiting to run
Tests / Code Coverage / test-clientv2 (push) Waiting to run
Tests / Code Coverage / test-core (push) Waiting to run
Tests / Code Coverage / test-depinject (push) Waiting to run
Tests / Code Coverage / test-errors (push) Waiting to run
Tests / Code Coverage / test-math (push) Waiting to run
Tests / Code Coverage / test-schema (push) Waiting to run
Tests / Code Coverage / test-collections (push) Waiting to run
Tests / Code Coverage / test-cosmovisor (push) Waiting to run
Tests / Code Coverage / test-confix (push) Waiting to run
Tests / Code Coverage / test-store (push) Waiting to run
Tests / Code Coverage / test-log (push) Waiting to run
Tests / Code Coverage / test-x-tx (push) Waiting to run
Tests / Code Coverage / test-x-nft (push) Waiting to run
Tests / Code Coverage / test-x-circuit (push) Waiting to run
Tests / Code Coverage / test-x-feegrant (push) Waiting to run
Tests / Code Coverage / test-x-evidence (push) Waiting to run
Tests / Code Coverage / test-x-upgrade (push) Waiting to run
Tests / Code Coverage / test-tools-benchmark (push) Waiting to run
Build & Push SDK Proto Builder / build (push) Has been cancelled
327 lines
11 KiB
Markdown
327 lines
11 KiB
Markdown
# ADR 070: Unordered Transactions
|
||
|
||
## Changelog
|
||
|
||
- Dec 4, 2023: Initial Draft (@yihuang, @tac0turtle, @alexanderbez)
|
||
- Jan 30, 2024: Include section on deterministic transaction encoding
|
||
- Mar 18, 2025: Revise implementation to use Cosmos SDK KV Store and require unique timeouts per-address (@technicallyty)
|
||
- Apr 25, 2025: Add note about rejecting unordered txs with sequence values.
|
||
|
||
## Status
|
||
|
||
ACCEPTED Not Implemented
|
||
|
||
## Abstract
|
||
|
||
We propose a way to do replay-attack protection without enforcing the order of
|
||
transactions and without requiring the use of monotonically increasing sequences. Instead, we propose
|
||
the use of a time-based, ephemeral sequence.
|
||
|
||
## Context
|
||
|
||
Account sequence values serve to prevent replay attacks and ensure transactions from the same sender are included into blocks and executed
|
||
in sequential order. Unfortunately, this makes it difficult to reliably send many concurrent transactions from the
|
||
same sender. Victims of such limitations include IBC relayers and crypto exchanges.
|
||
|
||
## Decision
|
||
|
||
We propose adding a boolean field `unordered` and a google.protobuf.Timestamp field `timeout_timestamp` to the transaction body.
|
||
|
||
Unordered transactions will bypass the traditional account sequence rules and follow the rules described
|
||
below, without impacting traditional ordered transactions which will follow the same sequence rules as before.
|
||
|
||
We will introduce new storage of time-based, ephemeral unordered sequences using the SDK's existing KV Store library.
|
||
Specifically, we will leverage the existing x/auth KV store to store the unordered sequences.
|
||
|
||
When an unordered transaction is included in a block, a concatenation of the `timeout_timestamp` and sender’s address bytes
|
||
will be recorded to state (i.e. `542939323/<address_bytes>`). In cases of multi-party signing, one entry per signer
|
||
will be recorded to state.
|
||
|
||
New transactions will be checked against the state to prevent duplicate submissions. To prevent the state from growing indefinitely, we propose the following:
|
||
|
||
- Define an upper bound for the value of `timeout_timestamp` (i.e. 10 minutes).
|
||
- Add PreBlocker method x/auth that removes state entries with a `timeout_timestamp` earlier than the current block time.
|
||
|
||
### Transaction Format
|
||
|
||
```protobuf
|
||
message TxBody {
|
||
...
|
||
|
||
bool unordered = 4;
|
||
google.protobuf.Timestamp timeout_timestamp = 5
|
||
}
|
||
```
|
||
|
||
### Replay Protection
|
||
|
||
We facilitate replay protection by storing the unordered sequence in the Cosmos SDK KV store. Upon transaction ingress, we check if the transaction's unordered
|
||
sequence exists in state, or if the TTL value is stale, i.e. before the current block time. If so, we reject it. Otherwise,
|
||
we add the unordered sequence to the state. This section of the state will belong to the `x/auth` module.
|
||
|
||
The state is evaluated during x/auth's `PreBlocker`. All transactions with an unordered sequence earlier than the current block time
|
||
will be deleted.
|
||
|
||
```go
|
||
func (am AppModule) PreBlock(ctx context.Context) (appmodule.ResponsePreBlock, error) {
|
||
err := am.accountKeeper.RemoveExpired(sdk.UnwrapSDKContext(ctx))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &sdk.ResponsePreBlock{ConsensusParamsChanged: false}, nil
|
||
}
|
||
```
|
||
|
||
```golang
|
||
package keeper
|
||
|
||
import (
|
||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||
|
||
"cosmossdk.io/collections"
|
||
"cosmossdk.io/core/store"
|
||
)
|
||
|
||
var (
|
||
// just arbitrarily picking some upper bound number.
|
||
unorderedSequencePrefix = collections.NewPrefix(90)
|
||
)
|
||
|
||
type AccountKeeper struct {
|
||
// ...
|
||
unorderedSequences collections.KeySet[collections.Pair[uint64, []byte]]
|
||
}
|
||
|
||
func (m *AccountKeeper) Contains(ctx sdk.Context, sender []byte, timestamp uint64) (bool, error) {
|
||
return m.unorderedSequences.Has(ctx, collections.Join(timestamp, sender))
|
||
}
|
||
|
||
func (m *AccountKeeper) Add(ctx sdk.Context, sender []byte, timestamp uint64) error {
|
||
return m.unorderedSequences.Set(ctx, collections.Join(timestamp, sender))
|
||
}
|
||
|
||
func (m *AccountKeeper) RemoveExpired(ctx sdk.Context) error {
|
||
blkTime := ctx.BlockTime().UnixNano()
|
||
it, err := m.unorderedSequences.Iterate(ctx, collections.NewPrefixUntilPairRange[uint64, []byte](uint64(blkTime)))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer it.Close()
|
||
|
||
keys, err := it.Keys()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, key := range keys {
|
||
if err := m.unorderedSequences.Remove(ctx, key); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
```
|
||
|
||
### AnteHandler Decorator
|
||
|
||
To facilitate bypassing nonce verification, we must modify the existing
|
||
`IncrementSequenceDecorator` AnteHandler decorator to skip the nonce verification
|
||
when the transaction is marked as unordered.
|
||
|
||
```golang
|
||
func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
|
||
if tx.UnOrdered() {
|
||
return next(ctx, tx, simulate)
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
We also introduce a new decorator to perform the unordered transaction verification.
|
||
|
||
```golang
|
||
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()
|
||
}
|
||
|
||
```
|
||
|
||
### Unordered Sequences
|
||
|
||
Unordered sequences provide a simple, straightforward mechanism to protect against both transaction malleability and
|
||
transaction duplication. It is important to note that the unordered sequence must still be unique. However,
|
||
the value is not required to be strictly increasing as with regular sequences, and the order in which the node receives
|
||
the transactions no longer matters. Clients can handle building unordered transactions similarly to the code below:
|
||
|
||
```go
|
||
for _, tx := range txs {
|
||
tx.SetUnordered(true)
|
||
tx.SetTimeoutTimestamp(time.Now() + 1 * time.Nanosecond)
|
||
}
|
||
```
|
||
|
||
We will reject transactions that have both sequence and unordered timeouts set. We do this to avoid assuming the intent of the user.
|
||
|
||
### State Management
|
||
|
||
The storage of unordered sequences will be facilitated using the Cosmos SDK's KV Store service.
|
||
|
||
## Note On Previous Design Iteration
|
||
|
||
The previous iteration of unordered transactions worked by using an ad-hoc state-management system that posed severe
|
||
risks and a vector for duplicated tx processing. It relied on graceful app closure which would flush the current state
|
||
of the unordered sequence mapping. If the 2/3's of the network crashed, and the graceful closure did not trigger,
|
||
the system would lose track of all sequences in the mapping, allowing those transactions to be replayed. The
|
||
implementation proposed in the updated version of this ADR solves this by writing directly to the Cosmos KV Store.
|
||
While this is less performant, for the initial implementation, we opted to choose a safer path and postpone performance optimizations until we have more data on real-world impacts and a more battle-tested approach to optimization.
|
||
|
||
Additionally, the previous iteration relied on using hashes to create what we call an "unordered sequence." There are known
|
||
issues with transaction malleability in Cosmos SDK signing modes. This ADR gets away from this problem by enforcing
|
||
single-use unordered nonces, instead of deriving nonces from bytes in the transaction.
|
||
|
||
## Consequences
|
||
|
||
### Positive
|
||
|
||
* Support unordered transaction inclusion, enabling the ability to "fire and forget" many transactions at once.
|
||
|
||
### Negative
|
||
|
||
* Requires additional storage overhead.
|
||
* Requirement of unique timestamps per transaction causes a small amount of additional overhead for clients. Clients must ensure each transaction's timeout timestamp is different. However, nanosecond differentials suffice.
|
||
* Usage of Cosmos SDK KV store is slower in comparison to using a non-merklized store or ad-hoc methods, and block times may slow down as a result.
|
||
|
||
## References
|
||
|
||
* https://github.com/cosmos/cosmos-sdk/issues/13009
|
||
|