fix: do not allow unordered txs to have sequence values set (#24581)

Co-authored-by: Alex | Interchain Labs <alex@interchainlabs.io>
Co-authored-by: Aaron Craelius <aaronc@users.noreply.github.com>
This commit is contained in:
Tyler 2025-04-28 13:55:05 -07:00 committed by GitHub
parent a158c24b38
commit f9f3bfb066
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 255 additions and 46 deletions

View File

@ -408,6 +408,11 @@ Lastly, add an entry for epochs in the ModuleConfig:
To enable unordered transaction support on an application, the `x/auth` keeper must be supplied with the `WithUnorderedTransactions` option.
Note that unordered transactions require sequence values to be zero, and will **FAIL** if a non-zero sequence value is set.
Please ensure no sequence value is set when submitting an unordered transaction.
Services that rely on prior assumptions about sequence values should be updated to handle unordered transactions.
Services should be aware that when the transaction is unordered, the transaction sequence will always be zero.
```go
app.AccountKeeper = authkeeper.NewAccountKeeper(
appCodec,

View File

@ -25,6 +25,11 @@ To submit an unordered transaction, clients must set the `unordered` flag to
used as a TTL for the transaction and provides replay protection. Each transaction's `timeout_timestamp` must be
unique to the account; however, the difference may be as small as a nanosecond. See [ADR-070](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-070-unordered-transactions.md) for more details.
Note that unordered transactions require sequence values to be zero, and will **FAIL** if a non-zero sequence value is set.
Please ensure no sequence value is set when submitting an unordered transaction.
Services that rely on prior assumptions about sequence values should be updated to handle unordered transactions.
Services should be aware that when the transaction is unordered, the transaction sequence will always be zero.
#### Enabling Unordered Transactions
To enable unordered transactions, supply the `WithUnorderedTransactions` option to the `x/auth` keeper:

View File

@ -8561,6 +8561,10 @@ type TxBody struct {
// Note, when set to true, the existing 'timeout_timestamp' value must
// be set and will be used to correspond to a timestamp in which the transaction is deemed
// valid.
//
// When true, the sequence value MUST be 0, and any transaction with unordered=true and a non-zero sequence value will
// be rejected.
// External services that make assumptions about sequence values may need to be updated because of this.
Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"`
// timeout_timestamp is the block time after which this transaction will not
// be processed by the chain.

View File

@ -150,6 +150,8 @@ func AddTxFlagsToCmd(cmd *cobra.Command) {
GasFlagAuto, GasFlagAuto, FlagFees, DefaultGasLimit))
cmd.MarkFlagsMutuallyExclusive(FlagTimeoutHeight, TimeoutDuration)
// unordered transactions must not have sequence values.
cmd.MarkFlagsMutuallyExclusive(FlagUnordered, FlagSequence)
cmd.MarkFlagsRequiredTogether(FlagUnordered, TimeoutDuration)
AddKeyringFlags(f)

View File

@ -515,6 +515,9 @@ func (f Factory) getSimSignatureData(pk cryptotypes.PubKey) signing.SignatureDat
// A new Factory with the updated fields will be returned.
// Note: When in offline mode, the Prepare does nothing and returns the original factory.
func (f Factory) Prepare(clientCtx client.Context) (Factory, error) {
if f.sequence > 0 && f.unordered {
return f, errors.New("unordered transactions must not have sequence values set")
}
if clientCtx.Offline {
return f, nil
}
@ -537,7 +540,7 @@ func (f Factory) Prepare(clientCtx client.Context) (Factory, error) {
fc = fc.WithAccountNumber(num)
}
if initSeq == 0 {
if initSeq == 0 && !f.unordered {
fc = fc.WithSequence(seq)
}
}

View File

@ -41,6 +41,17 @@ func TestFactoryPrepare(t *testing.T) {
require.NotEqual(t, output, factory)
require.Equal(t, output.AccountNumber(), uint64(10))
require.Equal(t, output.Sequence(), uint64(1))
// sequence and unordered set should break the tx.
factory = Factory{}.WithAccountRetriever(client.MockAccountRetriever{ReturnAccNum: 10, ReturnAccSeq: 15}).WithUnordered(true).WithSequence(15)
_, err = factory.Prepare(clientCtx.WithFrom("foo"))
require.ErrorContains(t, err, "unordered transactions must not have sequence values set")
// unordered set should ignore the retrieved sequence value.
factory = Factory{}.WithAccountRetriever(client.MockAccountRetriever{ReturnAccNum: 10, ReturnAccSeq: 15}).WithUnordered(true)
output, err = factory.Prepare(clientCtx.WithFrom("foo"))
require.NoError(t, err)
require.Zero(t, output.Sequence())
}
func TestFactory_getSimPKType(t *testing.T) {

View File

@ -5,6 +5,7 @@
- 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
@ -289,6 +290,8 @@ for _, tx := range txs {
}
```
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.

View File

@ -139,7 +139,7 @@ https://github.com/cosmos/cosmos-sdk/blob/v0.53.0-rc.2/client/tx_config.go#L39-L
* `Memo`, a note or comment to send with the transaction.
* `FeeAmount`, the maximum amount the user is willing to pay in fees.
* `TimeoutHeight`, block height until which the transaction is valid.
* `Unordered`, an option indicating this transaction may be executed in any order (requires TimeoutTimestamp to be set)
* `Unordered`, an option indicating this transaction may be executed in any order (requires Sequence to be unset.)
* `TimeoutTimestamp`, the timeout timestamp (unordered nonce) of the transaction (required to be used with Unordered).
* `Signatures`, the array of signatures from all signers of the transaction.
@ -211,11 +211,19 @@ Check out the [v0.53.0 Upgrade Guide](https://docs.cosmos.network/v0.53/build/mi
:::
:::warning
Unordered transactions MUST leave sequence values unset. When a transaction is both unordered and contains a non-zero sequence value,
the transaction will be rejected. Services that operate on prior assumptions about transaction sequence values should be updated to handle unordered transactions.
Services should be aware that when the transaction is unordered, the transaction sequence will always be zero.
:::
Beginning with Cosmos SDK v0.53.0, chains may enable unordered transaction support.
Unordered transactions work by using a timestamp as the transaction's nonce value.
Unordered transactions work by using a timestamp as the transaction's nonce value. The sequence value must NOT be set in the signature(s) of the transaction.
The timestamp must be greater than the current block time and not exceed the chain's configured max unordered timeout timestamp duration.
Senders must use a unique timestamp for each distinct transaction. The difference may be as small as a nanosecond, however.
These unique timestamps serve as a one-shot nonce, and their lifespan in state is short-lived.
Upon transaction inclusion, an entry consisting of timeout timestamp and account address will be recorded to state.
Once the block time is passed the timeout timestamp value, the entry will be removed. This ensures that unordered nonces do not indefinitely fill up the chain's storage.
Once the block time is passed the timeout timestamp value, the entry will be removed. This ensures that unordered nonces do not indefinitely fill up the chain's storage.

View File

@ -46,9 +46,17 @@ Later, validators decide whether to include the transaction in their block by co
With Cosmos SDK v0.53.0, users may send unordered transactions to chains that have this feature enabled.
The following flags allow a user to build an unordered transaction from the CLI.
* `--unordered` specifies that this transaction should be unordered.
* `--unordered` specifies that this transaction should be unordered. (transaction sequence must be unset)
* `--timeout-duration` specifies the amount of time the unordered transaction should be valid in the mempool. The transaction's unordered nonce will be set to the time of transaction creation + timeout duration.
:::warning
Unordered transactions MUST leave sequence values unset. When a transaction is both unordered and contains a non-zero sequence value,
the transaction will be rejected. External services that operate on prior assumptions about transaction sequence values should be updated to handle unordered transactions.
Services should be aware that when the transaction is unordered, the transaction sequence will always be zero.
:::
#### CLI Example
Users of the application `app` can enter the following command into their CLI to generate a transaction to send 1000uatom from a `senderAddress` to a `recipientAddress`. The command specifies how much gas they are willing to pay: an automatic estimate scaled up by 1.5 times, with a gas price of 0.025uatom per unit gas.

View File

@ -183,6 +183,14 @@ At this point, `TxBuilder`'s underlying transaction is ready to be signed.
Starting with Cosmos SDK v0.53.0, users may send unordered transactions to chains that have the feature enabled.
:::warning
Unordered transactions MUST leave sequence values unset. When a transaction is both unordered and contains a non-zero sequence value,
the transaction will be rejected. External services that operate on prior assumptions about transaction sequence values should be updated to handle unordered transactions.
Services should be aware that when the transaction is unordered, the transaction sequence will always be zero.
:::
Using the example above, we can set the required fields to mark a transaction as unordered.
By default, unordered transactions charge an extra 2240 units of gas to offset the additional storage overhead that supports their functionality.
The extra units of gas are customizable and therefore vary by chain, so be sure to check the chain's ante handler for the gas value set, if any.

View File

@ -122,6 +122,10 @@ message TxBody {
// Note, when set to true, the existing 'timeout_timestamp' value must
// be set and will be used to correspond to a timestamp in which the transaction is deemed
// valid.
//
// When true, the sequence value MUST be 0, and any transaction with unordered=true and a non-zero sequence value will
// be rejected.
// External services that make assumptions about sequence values may need to be updated because of this.
bool unordered = 4 [(cosmos_proto.field_added_in) = "cosmos-sdk 0.53"];
// timeout_timestamp is the block time after which this transaction will not

View File

@ -557,6 +557,26 @@ func (s *E2ETestSuite) TestBroadcastTx_GRPCGateway() {
}
}
func (s *E2ETestSuite) TestUnorderedCannotUseSequence() {
val1 := *s.network.Validators[0]
coins := sdk.NewInt64Coin(s.cfg.BondDenom, 15)
_, err := cli.MsgSendExec(
val1.ClientCtx,
val1.Address,
val1.Address,
sdk.NewCoins(coins),
addresscodec.NewBech32Codec("cosmos"),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, math.NewInt(10))).String()),
fmt.Sprintf("--gas=%d", flags.DefaultGasLimit),
fmt.Sprintf("--sequence=%d", 15),
"--unordered",
fmt.Sprintf("--timeout-duration=%s", "10s"),
)
s.Require().ErrorContains(err, "if any flags in the group [unordered sequence] are set none of the others can be; [sequence unordered] were all set")
}
func (s *E2ETestSuite) TestSimMultiSigTx() {
val1 := *s.network.Validators[0]

26
types/mempool/nonce.go Normal file
View File

@ -0,0 +1,26 @@
package mempool
import (
"errors"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// ChooseNonce gets the nonce from a transaction. If the transaction is unordered,
// it uses the timeout timestamp as the nonce. Sequence values must be zero in this case.
// If the transaction is ordered, it uses the sequence number as the nonce.
func ChooseNonce(seq uint64, tx sdk.Tx) (uint64, error) {
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
if seq > 0 {
return 0, errors.New("unordered txs must not have sequence set")
}
timestamp := unordered.GetTimeoutTimeStamp().UnixNano()
if timestamp < 0 {
return 0, errors.New("invalid timestamp value")
}
return uint64(timestamp), nil
}
// otherwise, use the sequence as normal.
return seq, nil
}

View File

@ -0,0 +1,58 @@
package mempool_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/types/mempool"
)
func TestChooseNonce(t *testing.T) {
testCases := []struct {
name string
seq uint64
unordered bool
timeout time.Time
expErr string
expNonce int64
}{
{
name: "unordered nonce chosen",
unordered: true,
timeout: time.Unix(100, 15),
expNonce: time.Unix(100, 15).UnixNano(),
},
{
name: "sequence chosen",
seq: 15,
expNonce: 15,
},
{
name: "timeout invalid",
unordered: true,
timeout: time.Time{},
expErr: "invalid timestamp value",
},
{
name: "invalid if sequence and unordered set",
unordered: true,
seq: 15,
expErr: "unordered txs must not have sequence set",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tx := testTx{unordered: tc.unordered, nonce: tc.seq, timeout: &tc.timeout}
nonce, err := mempool.ChooseNonce(tc.seq, tx)
if tc.expErr != "" {
require.ErrorContains(t, err, tc.expErr)
} else {
require.NoError(t, err)
require.Equal(t, nonce, uint64(tc.expNonce))
}
})
}
}

View File

@ -2,7 +2,6 @@ package mempool
import (
"context"
"errors"
"fmt"
"math"
"sync"
@ -222,15 +221,9 @@ func (mp *PriorityNonceMempool[C]) Insert(ctx context.Context, tx sdk.Tx) error
sig := sigs[0]
sender := sig.Signer.String()
priority := mp.cfg.TxPriority.GetTxPriority(ctx, tx)
nonce := sig.Sequence
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
timestamp := unordered.GetTimeoutTimeStamp().UnixNano()
if timestamp < 0 {
return errors.New("invalid timestamp value")
}
nonce = uint64(timestamp)
nonce, err := ChooseNonce(sig.Sequence, tx)
if err != nil {
return err
}
key := txMeta[C]{nonce: nonce, priority: priority, sender: sender}
@ -467,15 +460,9 @@ func (mp *PriorityNonceMempool[C]) Remove(tx sdk.Tx) error {
sig := sigs[0]
sender := sig.Signer.String()
nonce := sig.Sequence
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
timestamp := unordered.GetTimeoutTimeStamp().UnixNano()
if timestamp < 0 {
return errors.New("invalid timestamp value")
}
nonce = uint64(timestamp)
nonce, err := ChooseNonce(sig.Sequence, tx)
if err != nil {
return err
}
scoreKey := txMeta[C]{nonce: nonce, sender: sender}

View File

@ -973,6 +973,14 @@ func TestNextSenderTx_TxReplacement(t *testing.T) {
require.Equal(t, txs[3], iter.Tx())
}
func TestPriorityNonceMempool_UnorderedTx_FailsForSequence(t *testing.T) {
mp := mempool.DefaultPriorityMempool()
accounts := simtypes.RandomAccounts(rand.New(rand.NewSource(0)), 1)
tx := testTx{id: 1, priority: 0, address: accounts[0].Address, nonce: 1, unordered: true}
err := mp.Insert(sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger()), tx)
require.ErrorContains(t, err, "unordered txs must not have sequence set")
}
func TestPriorityNonceMempool_UnorderedTx(t *testing.T) {
ctx := sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
accounts := simtypes.RandomAccounts(rand.New(rand.NewSource(0)), 2)

View File

@ -4,7 +4,6 @@ import (
"context"
crand "crypto/rand" // #nosec // crypto/rand is used for seed generation
"encoding/binary"
"errors"
"fmt"
"math/rand" // #nosec // math/rand is used for random selection and seeded from crypto/rand
"slices"
@ -139,15 +138,9 @@ func (snm *SenderNonceMempool) Insert(_ context.Context, tx sdk.Tx) error {
sig := sigs[0]
sender := sdk.AccAddress(sig.PubKey.Address()).String()
nonce := sig.Sequence
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
timestamp := unordered.GetTimeoutTimeStamp().UnixNano()
if timestamp < 0 {
return errors.New("invalid timestamp value")
}
nonce = uint64(timestamp)
nonce, err := ChooseNonce(sig.Sequence, tx)
if err != nil {
return err
}
senderTxs, found := snm.senders[sender]
@ -236,15 +229,9 @@ func (snm *SenderNonceMempool) Remove(tx sdk.Tx) error {
sig := sigs[0]
sender := sdk.AccAddress(sig.PubKey.Address()).String()
nonce := sig.Sequence
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
timestamp := unordered.GetTimeoutTimeStamp().UnixNano()
if timestamp < 0 {
return errors.New("invalid timestamp value")
}
nonce = uint64(timestamp)
nonce, err := ChooseNonce(sig.Sequence, tx)
if err != nil {
return err
}
senderTxs, found := snm.senders[sender]

View File

@ -170,6 +170,22 @@ func (s *MempoolTestSuite) TestMaxTx() {
require.Equal(t, mempool.ErrMempoolTxMaxCapacity, err)
}
func (s *MempoolTestSuite) TestTxRejectedWithUnorderedAndSequence() {
t := s.T()
ctx := sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())
accounts := simtypes.RandomAccounts(rand.New(rand.NewSource(0)), 1)
mp := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
txSender := testTx{
nonce: 15,
address: accounts[0].Address,
priority: rand.Int63(),
unordered: true,
}
err := mp.Insert(ctx, txSender)
require.ErrorContains(t, err, "unordered txs must not have sequence set")
}
func (s *MempoolTestSuite) TestTxNotFoundOnSender() {
t := s.T()
ctx := sdk.NewContext(nil, cmtproto.Header{}, false, log.NewNopLogger())

View File

@ -373,6 +373,10 @@ type TxBody struct {
// Note, when set to true, the existing 'timeout_timestamp' value must
// be set and will be used to correspond to a timestamp in which the transaction is deemed
// valid.
//
// When true, the sequence value MUST be 0, and any transaction with unordered=true and a non-zero sequence value will
// be rejected.
// External services that make assumptions about sequence values may need to be updated because of this.
Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"`
// timeout_timestamp is the block time after which this transaction will not
// be processed by the chain.

View File

@ -120,11 +120,21 @@ func (spkd SetPubKeyDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b
return ctx, err
}
isUnordered := false
utx, ok := tx.(sdk.TxWithUnordered)
if ok && utx.GetUnordered() {
isUnordered = true
}
var events sdk.Events
for i, sig := range sigs {
events = append(events, sdk.NewEvent(sdk.EventTypeTx,
sdk.NewAttribute(sdk.AttributeKeyAccountSequence, fmt.Sprintf("%s/%d", signerStrs[i], sig.Sequence)),
))
// this shouldn't happen, but if we somehow got a tx with both a sequence set, and is unordered,
// we shouldn't emit the event, as this is a false sequence, and won't actually be used.
if !isUnordered {
events = append(events, sdk.NewEvent(sdk.EventTypeTx,
sdk.NewAttribute(sdk.AttributeKeyAccountSequence, fmt.Sprintf("%s/%d", signerStrs[i], sig.Sequence)),
))
}
sigBzs, err := signatureDataToBz(sig.Data)
if err != nil {
@ -334,6 +344,9 @@ func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul
}
for i, sig := range sigs {
if sig.Sequence > 0 && isUnordered {
return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "sequence is not allowed for unordered transactions")
}
acc, err := GetSignerAcc(ctx, svd.ak, signers[i])
if err != nil {
return ctx, err

View File

@ -72,6 +72,35 @@ func TestSetPubKey(t *testing.T) {
}
}
// TestSetPubKey_UnorderedNoEvents tests that when the tx is unordered, the sequence event is not emitted.
func TestSetPubKey_UnorderedNoEvents(t *testing.T) {
suite := SetupTestSuite(t, true)
suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder()
// prepare accounts for tx
priv1, _, addr1 := testdata.KeyTestPubAddr()
acc := suite.accountKeeper.NewAccountWithAddress(suite.ctx, addr1)
require.NoError(t, acc.SetAccountNumber(uint64(1000)))
suite.accountKeeper.SetAccount(suite.ctx, acc)
require.NoError(t, suite.txBuilder.SetMsgs(testdata.NewTestMsg(addr1)))
privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0}
tx, err := suite.CreateTestUnorderedTx(suite.ctx, privs, accNums, accSeqs, suite.ctx.ChainID(), signing.SignMode_SIGN_MODE_DIRECT, true, time.Unix(100, 0))
require.NoError(t, err)
spkd := ante.NewSetPubKeyDecorator(suite.accountKeeper)
antehandler := sdk.ChainAnteDecorators(spkd)
ctx, err := antehandler(suite.ctx.WithBlockTime(time.Unix(95, 0)), tx, false)
require.NoError(t, err)
events := ctx.EventManager().Events()
for _, event := range events {
// if this event were emitted, the tx search by address/sequence would break when an unordered
// transaction uses the same sequence number as another transaction from the same sender.
require.NotContains(t, event.Attributes, sdk.AttributeKeyAccountSequence)
}
}
func TestConsumeSignatureVerificationGas(t *testing.T) {
suite := SetupTestSuite(t, true)
params := types.DefaultParams()