diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md index c124459895..ed494c4152 100644 --- a/UPGRADE_GUIDE.md +++ b/UPGRADE_GUIDE.md @@ -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, diff --git a/UPGRADING.md b/UPGRADING.md index 6f697058b4..598dd51905 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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: diff --git a/api/cosmos/tx/v1beta1/tx.pulsar.go b/api/cosmos/tx/v1beta1/tx.pulsar.go index 2bf64c4a67..4b84abb708 100644 --- a/api/cosmos/tx/v1beta1/tx.pulsar.go +++ b/api/cosmos/tx/v1beta1/tx.pulsar.go @@ -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. diff --git a/client/flags/flags.go b/client/flags/flags.go index e374c67cee..e9f7978974 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -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) diff --git a/client/tx/factory.go b/client/tx/factory.go index ca6a8d4403..4916f0ed4f 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -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) } } diff --git a/client/tx/factory_test.go b/client/tx/factory_test.go index 402d178a98..ebb2cc0125 100644 --- a/client/tx/factory_test.go +++ b/client/tx/factory_test.go @@ -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) { diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md index 2a342740f1..d4c228d693 100644 --- a/docs/architecture/adr-070-unordered-account.md +++ b/docs/architecture/adr-070-unordered-account.md @@ -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. diff --git a/docs/docs/learn/advanced/01-transactions.md b/docs/docs/learn/advanced/01-transactions.md index 21c42d34a1..dbb6aaf5e0 100644 --- a/docs/docs/learn/advanced/01-transactions.md +++ b/docs/docs/learn/advanced/01-transactions.md @@ -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. \ No newline at end of file diff --git a/docs/docs/learn/beginner/01-tx-lifecycle.md b/docs/docs/learn/beginner/01-tx-lifecycle.md index 6468f16dbc..b2e821e9f8 100644 --- a/docs/docs/learn/beginner/01-tx-lifecycle.md +++ b/docs/docs/learn/beginner/01-tx-lifecycle.md @@ -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. diff --git a/docs/docs/user/run-node/03-txs.md b/docs/docs/user/run-node/03-txs.md index efd29c524b..54b500d4a0 100644 --- a/docs/docs/user/run-node/03-txs.md +++ b/docs/docs/user/run-node/03-txs.md @@ -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. diff --git a/proto/cosmos/tx/v1beta1/tx.proto b/proto/cosmos/tx/v1beta1/tx.proto index bf641357df..64dcf02bed 100644 --- a/proto/cosmos/tx/v1beta1/tx.proto +++ b/proto/cosmos/tx/v1beta1/tx.proto @@ -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 diff --git a/tests/e2e/tx/service_test.go b/tests/e2e/tx/service_test.go index 363924c307..bddd5aea83 100644 --- a/tests/e2e/tx/service_test.go +++ b/tests/e2e/tx/service_test.go @@ -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] diff --git a/types/mempool/nonce.go b/types/mempool/nonce.go new file mode 100644 index 0000000000..bd61ddeff1 --- /dev/null +++ b/types/mempool/nonce.go @@ -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 +} diff --git a/types/mempool/nonce_test.go b/types/mempool/nonce_test.go new file mode 100644 index 0000000000..fa786f618f --- /dev/null +++ b/types/mempool/nonce_test.go @@ -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)) + } + }) + } +} diff --git a/types/mempool/priority_nonce.go b/types/mempool/priority_nonce.go index abecfb1432..abee245a66 100644 --- a/types/mempool/priority_nonce.go +++ b/types/mempool/priority_nonce.go @@ -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} diff --git a/types/mempool/priority_nonce_test.go b/types/mempool/priority_nonce_test.go index 5bee608a46..91843e578e 100644 --- a/types/mempool/priority_nonce_test.go +++ b/types/mempool/priority_nonce_test.go @@ -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) diff --git a/types/mempool/sender_nonce.go b/types/mempool/sender_nonce.go index 2623fefd95..ce2fa2e2c1 100644 --- a/types/mempool/sender_nonce.go +++ b/types/mempool/sender_nonce.go @@ -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] diff --git a/types/mempool/sender_nonce_test.go b/types/mempool/sender_nonce_test.go index cf39de452f..573c89cfb3 100644 --- a/types/mempool/sender_nonce_test.go +++ b/types/mempool/sender_nonce_test.go @@ -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()) diff --git a/types/tx/tx.pb.go b/types/tx/tx.pb.go index 18f5074030..62e3fff199 100644 --- a/types/tx/tx.pb.go +++ b/types/tx/tx.pb.go @@ -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. diff --git a/x/auth/ante/sigverify.go b/x/auth/ante/sigverify.go index b45846f0ab..653bc98e8b 100644 --- a/x/auth/ante/sigverify.go +++ b/x/auth/ante/sigverify.go @@ -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 diff --git a/x/auth/ante/sigverify_test.go b/x/auth/ante/sigverify_test.go index 8cc816e792..ed522954f2 100644 --- a/x/auth/ante/sigverify_test.go +++ b/x/auth/ante/sigverify_test.go @@ -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()