Skip to content

Commit ddf9e18

Browse files
facundomedicaAlex | Interchain Labsalpe
authored
fix(auth): audit issues with unordered txs (#23392)
Co-authored-by: Alex | Interchain Labs <alex@interchainlabs.io> Co-authored-by: Alexander Peters <alpe@users.noreply.github.com>
1 parent 8eb6822 commit ddf9e18

File tree

9 files changed

+296
-35
lines changed

9 files changed

+296
-35
lines changed

types/mempool/mempool_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"math/rand"
77
"testing"
8+
"time"
89

910
"github.com/stretchr/testify/require"
1011
"github.com/stretchr/testify/suite"
@@ -55,6 +56,21 @@ type testTx struct {
5556
address sdk.AccAddress
5657
// useful for debugging
5758
strAddress string
59+
unordered bool
60+
timeout *time.Time
61+
}
62+
63+
// GetTimeoutTimeStamp implements types.TxWithUnordered.
64+
func (tx testTx) GetTimeoutTimeStamp() time.Time {
65+
if tx.timeout == nil {
66+
return time.Time{}
67+
}
68+
return *tx.timeout
69+
}
70+
71+
// GetUnordered implements types.TxWithUnordered.
72+
func (tx testTx) GetUnordered() bool {
73+
return tx.unordered
5874
}
5975

6076
func (tx testTx) GetSigners() ([][]byte, error) { panic("not implemented") }
@@ -73,6 +89,7 @@ func (tx testTx) GetSignaturesV2() (res []txsigning.SignatureV2, err error) {
7389

7490
var (
7591
_ sdk.Tx = (*testTx)(nil)
92+
_ sdk.TxWithUnordered = (*testTx)(nil)
7693
_ signing.SigVerifiableTx = (*testTx)(nil)
7794
_ cryptotypes.PubKey = (*testPubKey)(nil)
7895
)

types/mempool/priority_nonce.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,13 @@ func (mp *PriorityNonceMempool[C]) Insert(ctx context.Context, tx sdk.Tx) error
224224
priority := mp.cfg.TxPriority.GetTxPriority(ctx, tx)
225225
nonce := sig.Sequence
226226

227-
// if it's an unordered tx, we use the gas instead of the nonce
227+
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
228228
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
229-
gasLimit, err := unordered.GetGasLimit()
230-
nonce = gasLimit
231-
if err != nil {
232-
return err
229+
timestamp := unordered.GetTimeoutTimeStamp().Unix()
230+
if timestamp < 0 {
231+
return errors.New("invalid timestamp value")
233232
}
233+
nonce = uint64(timestamp)
234234
}
235235

236236
key := txMeta[C]{nonce: nonce, priority: priority, sender: sender}
@@ -469,13 +469,13 @@ func (mp *PriorityNonceMempool[C]) Remove(tx sdk.Tx) error {
469469
sender := sig.Signer.String()
470470
nonce := sig.Sequence
471471

472-
// if it's an unordered tx, we use the gas instead of the nonce
472+
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
473473
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
474-
gasLimit, err := unordered.GetGasLimit()
475-
nonce = gasLimit
476-
if err != nil {
477-
return err
474+
timestamp := unordered.GetTimeoutTimeStamp().Unix()
475+
if timestamp < 0 {
476+
return errors.New("invalid timestamp value")
478477
}
478+
nonce = uint64(timestamp)
479479
}
480480

481481
scoreKey := txMeta[C]{nonce: nonce, sender: sender}

types/mempool/priority_nonce_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,3 +970,40 @@ func TestNextSenderTx_TxReplacement(t *testing.T) {
970970
iter := mp.Select(ctx, nil)
971971
require.Equal(t, txs[3], iter.Tx())
972972
}
973+
974+
func TestPriorityNonceMempool_UnorderedTx(t *testing.T) {
975+
ctx := sdk.NewContext(nil, false, log.NewNopLogger())
976+
accounts := simtypes.RandomAccounts(rand.New(rand.NewSource(0)), 2)
977+
sa := accounts[0].Address
978+
sb := accounts[1].Address
979+
980+
mp := mempool.DefaultPriorityMempool()
981+
982+
now := time.Now()
983+
oneHour := now.Add(1 * time.Hour)
984+
thirtyMin := now.Add(30 * time.Minute)
985+
twoHours := now.Add(2 * time.Hour)
986+
fifteenMin := now.Add(15 * time.Minute)
987+
988+
txs := []testTx{
989+
{id: 1, priority: 0, address: sa, timeout: &thirtyMin, unordered: true},
990+
{id: 0, priority: 0, address: sa, timeout: &oneHour, unordered: true},
991+
{id: 3, priority: 0, address: sb, timeout: &fifteenMin, unordered: true},
992+
{id: 2, priority: 0, address: sb, timeout: &twoHours, unordered: true},
993+
}
994+
995+
for _, tx := range txs {
996+
c := ctx.WithPriority(tx.priority)
997+
require.NoError(t, mp.Insert(c, tx))
998+
}
999+
1000+
require.Equal(t, 4, mp.CountTx())
1001+
1002+
orderedTxs := fetchTxs(mp.Select(ctx, nil), 100000)
1003+
require.Equal(t, len(txs), len(orderedTxs))
1004+
1005+
// check order
1006+
for i, tx := range orderedTxs {
1007+
require.Equal(t, txs[i].id, tx.(testTx).id)
1008+
}
1009+
}

types/mempool/sender_nonce.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,21 +139,21 @@ func (snm *SenderNonceMempool) Insert(_ context.Context, tx sdk.Tx) error {
139139
sender := sdk.AccAddress(sig.PubKey.Address()).String()
140140
nonce := sig.Sequence
141141

142+
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
143+
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
144+
timestamp := unordered.GetTimeoutTimeStamp().Unix()
145+
if timestamp < 0 {
146+
return errors.New("invalid timestamp value")
147+
}
148+
nonce = uint64(timestamp)
149+
}
150+
142151
senderTxs, found := snm.senders[sender]
143152
if !found {
144153
senderTxs = skiplist.New(skiplist.Uint64)
145154
snm.senders[sender] = senderTxs
146155
}
147156

148-
// if it's an unordered tx, we use the gas instead of the nonce
149-
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
150-
gasLimit, err := unordered.GetGasLimit()
151-
nonce = gasLimit
152-
if err != nil {
153-
return err
154-
}
155-
}
156-
157157
senderTxs.Set(nonce, tx)
158158

159159
key := txKey{nonce: nonce, address: sender}
@@ -236,13 +236,13 @@ func (snm *SenderNonceMempool) Remove(tx sdk.Tx) error {
236236
sender := sdk.AccAddress(sig.PubKey.Address()).String()
237237
nonce := sig.Sequence
238238

239-
// if it's an unordered tx, we use the gas instead of the nonce
239+
// if it's an unordered tx, we use the timeout timestamp instead of the nonce
240240
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
241-
gasLimit, err := unordered.GetGasLimit()
242-
nonce = gasLimit
243-
if err != nil {
244-
return err
241+
timestamp := unordered.GetTimeoutTimeStamp().Unix()
242+
if timestamp < 0 {
243+
return errors.New("invalid timestamp value")
245244
}
245+
nonce = uint64(timestamp)
246246
}
247247

248248
senderTxs, found := snm.senders[sender]

types/mempool/sender_nonce_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"math/rand"
66
"testing"
7+
"time"
78

89
"github.com/stretchr/testify/require"
910

@@ -192,3 +193,67 @@ func (s *MempoolTestSuite) TestTxNotFoundOnSender() {
192193
err = mp.Remove(tx)
193194
require.Equal(t, mempool.ErrTxNotFound, err)
194195
}
196+
197+
func (s *MempoolTestSuite) TestUnorderedTx() {
198+
t := s.T()
199+
200+
ctx := sdk.NewContext(nil, false, log.NewNopLogger())
201+
accounts := simtypes.RandomAccounts(rand.New(rand.NewSource(0)), 2)
202+
sa := accounts[0].Address
203+
sb := accounts[1].Address
204+
205+
mp := mempool.NewSenderNonceMempool(mempool.SenderNonceMaxTxOpt(5000))
206+
207+
now := time.Now()
208+
oneHour := now.Add(1 * time.Hour)
209+
thirtyMin := now.Add(30 * time.Minute)
210+
twoHours := now.Add(2 * time.Hour)
211+
fifteenMin := now.Add(15 * time.Minute)
212+
213+
txs := []testTx{
214+
{id: 0, address: sa, timeout: &oneHour, unordered: true},
215+
{id: 1, address: sa, timeout: &thirtyMin, unordered: true},
216+
{id: 2, address: sb, timeout: &twoHours, unordered: true},
217+
{id: 3, address: sb, timeout: &fifteenMin, unordered: true},
218+
}
219+
220+
for _, tx := range txs {
221+
c := ctx.WithPriority(tx.priority)
222+
require.NoError(t, mp.Insert(c, tx))
223+
}
224+
225+
require.Equal(t, 4, mp.CountTx())
226+
227+
orderedTxs := fetchTxs(mp.Select(ctx, nil), 100000)
228+
require.Equal(t, len(txs), len(orderedTxs))
229+
230+
// Because the sender is selected randomly it can be any of these options
231+
acceptableOptions := [][]int{
232+
{3, 1, 2, 0},
233+
{3, 1, 0, 2},
234+
{3, 2, 1, 0},
235+
{1, 3, 0, 2},
236+
{1, 3, 2, 0},
237+
{1, 0, 3, 2},
238+
}
239+
240+
orderedTxsIds := make([]int, len(orderedTxs))
241+
for i, tx := range orderedTxs {
242+
orderedTxsIds[i] = tx.(testTx).id
243+
}
244+
245+
anyAcceptableOrder := false
246+
for _, option := range acceptableOptions {
247+
for i, tx := range orderedTxs {
248+
if tx.(testTx).id != txs[option[i]].id {
249+
break
250+
}
251+
252+
if i == len(orderedTxs)-1 {
253+
anyAcceptableOrder = true
254+
}
255+
}
256+
}
257+
258+
require.True(t, anyAcceptableOrder, "expected any of %v but got %v", acceptableOptions, orderedTxsIds)
259+
}

x/auth/ante/ante_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"strings"
88
"testing"
9+
"time"
910

1011
"github.com/stretchr/testify/require"
1112
"go.uber.org/mock/gomock"
@@ -1384,3 +1385,34 @@ func TestAnteHandlerReCheck(t *testing.T) {
13841385
_, err = suite.anteHandler(suite.ctx, tx, false)
13851386
require.NotNil(t, err, "antehandler on recheck did not fail once feePayer no longer has sufficient funds")
13861387
}
1388+
1389+
func TestAnteHandlerUnorderedTx(t *testing.T) {
1390+
suite := SetupTestSuite(t, false)
1391+
accs := suite.CreateTestAccounts(1)
1392+
msg := testdata.NewTestMsg(accs[0].acc.GetAddress())
1393+
1394+
// First send a normal sequential tx with sequence 0
1395+
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), accs[0].acc.GetAddress(), authtypes.FeeCollectorName, testdata.NewTestFeeAmount()).Return(nil).AnyTimes()
1396+
1397+
privs, accNums, accSeqs := []cryptotypes.PrivKey{accs[0].priv}, []uint64{1000}, []uint64{0}
1398+
_, err := suite.DeliverMsgs(t, privs, []sdk.Msg{msg}, testdata.NewTestFeeAmount(), testdata.NewTestGasLimit(), accNums, accSeqs, suite.ctx.ChainID(), false)
1399+
require.NoError(t, err)
1400+
1401+
// we try to send another tx with the same sequence, it will fail
1402+
_, err = suite.DeliverMsgs(t, privs, []sdk.Msg{msg}, testdata.NewTestFeeAmount(), testdata.NewTestGasLimit(), accNums, accSeqs, suite.ctx.ChainID(), false)
1403+
require.Error(t, err)
1404+
1405+
// now we'll still use the same sequence but because it's unordered, it will be ignored and accepted anyway
1406+
msgs := []sdk.Msg{msg}
1407+
require.NoError(t, suite.txBuilder.SetMsgs(msgs...))
1408+
suite.txBuilder.SetFeeAmount(testdata.NewTestFeeAmount())
1409+
suite.txBuilder.SetGasLimit(testdata.NewTestGasLimit())
1410+
1411+
tx, txErr := suite.CreateTestUnorderedTx(suite.ctx, privs, accNums, accSeqs, suite.ctx.ChainID(), apisigning.SignMode_SIGN_MODE_DIRECT, true, time.Now().Add(time.Minute))
1412+
require.NoError(t, txErr)
1413+
txBytes, err := suite.clientCtx.TxConfig.TxEncoder()(tx)
1414+
bytesCtx := suite.ctx.WithTxBytes(txBytes)
1415+
require.NoError(t, err)
1416+
_, err = suite.anteHandler(bytesCtx, tx, false)
1417+
require.NoError(t, err)
1418+
}

x/auth/ante/sigverify.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,18 +320,24 @@ func (svd SigVerificationDecorator) consumeSignatureGas(
320320
// verifySig will verify the signature of the provided signer account.
321321
func (svd SigVerificationDecorator) verifySig(ctx context.Context, tx sdk.Tx, acc sdk.AccountI, sig signing.SignatureV2, newlyCreated bool) error {
322322
execMode := svd.ak.GetEnvironment().TransactionService.ExecMode(ctx)
323-
if execMode == transaction.ExecModeCheck {
324-
if sig.Sequence < acc.GetSequence() {
323+
unorderedTx, ok := tx.(sdk.TxWithUnordered)
324+
isUnordered := ok && unorderedTx.GetUnordered()
325+
326+
// only check sequence if the tx is not unordered
327+
if !isUnordered {
328+
if execMode == transaction.ExecModeCheck {
329+
if sig.Sequence < acc.GetSequence() {
330+
return errorsmod.Wrapf(
331+
sdkerrors.ErrWrongSequence,
332+
"account sequence mismatch: expected higher than or equal to %d, got %d", acc.GetSequence(), sig.Sequence,
333+
)
334+
}
335+
} else if sig.Sequence != acc.GetSequence() {
325336
return errorsmod.Wrapf(
326337
sdkerrors.ErrWrongSequence,
327-
"account sequence mismatch, expected higher than or equal to %d, got %d", acc.GetSequence(), sig.Sequence,
338+
"account sequence mismatch: expected %d, got %d", acc.GetSequence(), sig.Sequence,
328339
)
329340
}
330-
} else if sig.Sequence != acc.GetSequence() {
331-
return errorsmod.Wrapf(
332-
sdkerrors.ErrWrongSequence,
333-
"account sequence mismatch: expected %d, got %d", acc.GetSequence(), sig.Sequence,
334-
)
335341
}
336342

337343
// we're in simulation mode, or in ReCheckTx, or context is not

x/auth/ante/testutil_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ante_test
33
import (
44
"context"
55
"testing"
6+
"time"
67

78
"github.com/stretchr/testify/require"
89
"go.uber.org/mock/gomock"
@@ -241,6 +242,67 @@ func (suite *AnteTestSuite) RunTestCase(t *testing.T, tc TestCase, args TestCase
241242
}
242243
}
243244

245+
func (suite *AnteTestSuite) CreateTestUnorderedTx(
246+
ctx sdk.Context, privs []cryptotypes.PrivKey,
247+
accNums, accSeqs []uint64,
248+
chainID string, signMode apisigning.SignMode,
249+
unordered bool, unorderedTimeout time.Time,
250+
) (xauthsigning.Tx, error) {
251+
suite.txBuilder.SetUnordered(unordered)
252+
suite.txBuilder.SetTimeoutTimestamp(unorderedTimeout)
253+
254+
// First round: we gather all the signer infos. We use the "set empty
255+
// signature" hack to do that.
256+
var sigsV2 []signing.SignatureV2
257+
for i, priv := range privs {
258+
sigV2 := signing.SignatureV2{
259+
PubKey: priv.PubKey(),
260+
Data: &signing.SingleSignatureData{
261+
SignMode: signMode,
262+
Signature: nil,
263+
},
264+
Sequence: accSeqs[i],
265+
}
266+
267+
sigsV2 = append(sigsV2, sigV2)
268+
}
269+
err := suite.txBuilder.SetSignatures(sigsV2...)
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
// Second round: all signer infos are set, so each signer can sign.
275+
sigsV2 = []signing.SignatureV2{}
276+
for i, priv := range privs {
277+
anyPk, err := codectypes.NewAnyWithValue(priv.PubKey())
278+
if err != nil {
279+
return nil, err
280+
}
281+
282+
signerData := txsigning.SignerData{
283+
Address: sdk.AccAddress(priv.PubKey().Address()).String(),
284+
ChainID: chainID,
285+
AccountNumber: accNums[i],
286+
Sequence: accSeqs[i],
287+
PubKey: &anypb.Any{TypeUrl: anyPk.TypeUrl, Value: anyPk.Value},
288+
}
289+
sigV2, err := tx.SignWithPrivKey(
290+
ctx, signMode, signerData,
291+
suite.txBuilder, priv, suite.clientCtx.TxConfig, accSeqs[i])
292+
if err != nil {
293+
return nil, err
294+
}
295+
296+
sigsV2 = append(sigsV2, sigV2)
297+
}
298+
err = suite.txBuilder.SetSignatures(sigsV2...)
299+
if err != nil {
300+
return nil, err
301+
}
302+
303+
return suite.txBuilder.GetTx(), nil
304+
}
305+
244306
// CreateTestTx is a helper function to create a tx given multiple inputs.
245307
func (suite *AnteTestSuite) CreateTestTx(
246308
ctx sdk.Context, privs []cryptotypes.PrivKey,

0 commit comments

Comments
 (0)