Skip to content

Commit abcc7d4

Browse files
Patch account sequence bug
1 parent bd14027 commit abcc7d4

File tree

2 files changed

+112
-6
lines changed

2 files changed

+112
-6
lines changed

pkg/lumera/modules/tx/helper.go

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"strings"
7+
"sync"
78

89
"github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/auth"
910
"github.com/cosmos/cosmos-sdk/crypto/keyring"
@@ -19,6 +20,8 @@ type TxHelper struct {
1920
txmod Module
2021
config *TxConfig
2122

23+
mu sync.Mutex
24+
2225
accountNumber uint64
2326
nextSequence uint64
2427
seqInit bool
@@ -77,6 +80,9 @@ func (h *TxHelper) ExecuteTransaction(
7780
msgCreator func(creator string) (types.Msg, error),
7881
) (*sdktx.BroadcastTxResponse, error) {
7982

83+
h.mu.Lock()
84+
defer h.mu.Unlock()
85+
8086
// --- Step 1: Resolve creator address ---
8187
key, err := h.config.Keyring.Key(h.config.KeyName)
8288
if err != nil {
@@ -95,6 +101,9 @@ func (h *TxHelper) ExecuteTransaction(
95101
if err != nil {
96102
return nil, fmt.Errorf("failed to fetch initial account info: %w", err)
97103
}
104+
if accInfoRes == nil || accInfoRes.Info == nil {
105+
return nil, fmt.Errorf("empty account info response for creator %s", creator)
106+
}
98107

99108
h.accountNumber = accInfoRes.Info.AccountNumber
100109
h.nextSequence = accInfoRes.Info.Sequence
@@ -129,18 +138,27 @@ func (h *TxHelper) ExecuteTransaction(
129138

130139
// Check if this is a sequence mismatch error
131140
if !isSequenceMismatch(err) {
132-
return nil, err // unrelated error → bail out
141+
return resp, err // unrelated error → bail out (preserve response for debugging)
133142
}
134143

135144
// If retry unavailable, bubble error
136145
if attempt == maxAttempts {
137-
return nil, fmt.Errorf("sequence mismatch after retry: %w", err)
146+
return resp, fmt.Errorf("sequence mismatch after retry: %w", err)
147+
}
148+
149+
// --- Retry logic: prefer expected sequence from the error ---
150+
if expectedSeq, ok := parseExpectedSequence(err); ok {
151+
h.nextSequence = expectedSeq
152+
continue
138153
}
139154

140-
// --- Retry logic: resync from chain ---
155+
// Fallback: resync from chain state.
141156
accInfoRes, err2 := h.authmod.AccountInfoByAddress(ctx, creator)
142157
if err2 != nil {
143-
return nil, fmt.Errorf("failed to resync account info after mismatch: %w", err2)
158+
return resp, fmt.Errorf("failed to resync account info after mismatch: %w", err2)
159+
}
160+
if accInfoRes == nil || accInfoRes.Info == nil {
161+
return resp, fmt.Errorf("empty account info response for creator %s after mismatch", creator)
144162
}
145163

146164
h.accountNumber = accInfoRes.Info.AccountNumber
@@ -155,12 +173,30 @@ func isSequenceMismatch(err error) bool {
155173
return false
156174
}
157175

158-
msg := err.Error()
176+
msg := strings.ToLower(err.Error())
159177

160178
return strings.Contains(msg, "incorrect account sequence") ||
161179
strings.Contains(msg, "account sequence mismatch") ||
162-
(strings.Contains(msg, "expected") && strings.Contains(msg, "got"))
180+
strings.Contains(msg, "wrong sequence")
181+
}
163182

183+
func parseExpectedSequence(err error) (uint64, bool) {
184+
if err == nil {
185+
return 0, false
186+
}
187+
188+
msg := strings.ToLower(err.Error())
189+
idx := strings.Index(msg, "expected ")
190+
if idx == -1 {
191+
return 0, false
192+
}
193+
194+
var expected, got uint64
195+
if _, scanErr := fmt.Sscanf(msg[idx:], "expected %d, got %d", &expected, &got); scanErr == nil {
196+
return expected, true
197+
}
198+
199+
return 0, false
164200
}
165201

166202
// ExecuteTransactionWithMsgs processes a transaction with pre-created messages and account info
@@ -199,6 +235,9 @@ func (h *TxHelper) GetAccountInfo(ctx context.Context) (*authtypes.BaseAccount,
199235
}
200236

201237
func (h *TxHelper) UpdateConfig(config *TxHelperConfig) {
238+
h.mu.Lock()
239+
defer h.mu.Unlock()
240+
202241
if h.config == nil {
203242
h.config = &TxConfig{}
204243
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package tx
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestIsSequenceMismatch(t *testing.T) {
9+
t.Parallel()
10+
11+
tests := []struct {
12+
name string
13+
err error
14+
want bool
15+
}{
16+
{
17+
name: "grpc simulation mismatch",
18+
err: fmt.Errorf(
19+
"simulation failed: simulation error: rpc error: code = Unknown desc = account sequence mismatch, expected 7855, got 7854: incorrect account sequence [cosmos/cosmos-sdk@v0.53.0/x/auth/ante/sigverify.go:290] with gas used: '35369'",
20+
),
21+
want: true,
22+
},
23+
{
24+
name: "broadcast raw_log mismatch",
25+
err: fmt.Errorf(
26+
"tx failed: code=32 codespace=sdk height=0 gas_wanted=0 gas_used=0 raw_log=account sequence mismatch, expected 10, got 9: incorrect account sequence",
27+
),
28+
want: true,
29+
},
30+
{
31+
name: "unrelated expected/got",
32+
err: fmt.Errorf("expected 5, got 4"),
33+
want: false,
34+
},
35+
{
36+
name: "nil",
37+
err: nil,
38+
want: false,
39+
},
40+
}
41+
42+
for _, tt := range tests {
43+
t.Run(tt.name, func(t *testing.T) {
44+
t.Parallel()
45+
if got := isSequenceMismatch(tt.err); got != tt.want {
46+
t.Fatalf("isSequenceMismatch() = %v, want %v", got, tt.want)
47+
}
48+
})
49+
}
50+
}
51+
52+
func TestParseExpectedSequence(t *testing.T) {
53+
t.Parallel()
54+
55+
err := fmt.Errorf("account sequence mismatch, expected 7855, got 7854: incorrect account sequence")
56+
if got, ok := parseExpectedSequence(err); !ok || got != 7855 {
57+
t.Fatalf("parseExpectedSequence() = (%d, %v), want (7855, true)", got, ok)
58+
}
59+
60+
if _, ok := parseExpectedSequence(fmt.Errorf("incorrect account sequence")); ok {
61+
t.Fatalf("parseExpectedSequence() unexpectedly matched message without expected/got")
62+
}
63+
64+
if _, ok := parseExpectedSequence(nil); ok {
65+
t.Fatalf("parseExpectedSequence() unexpectedly matched nil error")
66+
}
67+
}

0 commit comments

Comments
 (0)