From bd1402746c2d02f97f774d4266c9b4e03b2d0d13 Mon Sep 17 00:00:00 2001 From: Matee ullah Date: Wed, 26 Nov 2025 01:40:53 +0500 Subject: [PATCH 1/2] maintain account sequence --- pkg/lumera/modules/tx/helper.go | 121 +++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 27 deletions(-) diff --git a/pkg/lumera/modules/tx/helper.go b/pkg/lumera/modules/tx/helper.go index 1da9cbb0..1c7a9dc4 100644 --- a/pkg/lumera/modules/tx/helper.go +++ b/pkg/lumera/modules/tx/helper.go @@ -3,6 +3,7 @@ package tx import ( "context" "fmt" + "strings" "github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/auth" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -17,6 +18,10 @@ type TxHelper struct { authmod auth.Module txmod Module config *TxConfig + + accountNumber uint64 + nextSequence uint64 + seqInit bool } // TxHelperConfig holds configuration for creating a TxHelper @@ -67,10 +72,12 @@ func NewTxHelperWithDefaults(authmod auth.Module, txmod Module, chainID, keyName return NewTxHelper(authmod, txmod, config) } -// ExecuteTransaction is a convenience method that handles the complete transaction flow -// for a single message. It gets account info, creates the message, and processes the transaction. -func (h *TxHelper) ExecuteTransaction(ctx context.Context, msgCreator func(creator string) (types.Msg, error)) (*sdktx.BroadcastTxResponse, error) { - // Step 1: Get creator address from keyring +func (h *TxHelper) ExecuteTransaction( + ctx context.Context, + msgCreator func(creator string) (types.Msg, error), +) (*sdktx.BroadcastTxResponse, error) { + + // --- Step 1: Resolve creator address --- key, err := h.config.Keyring.Key(h.config.KeyName) if err != nil { return nil, fmt.Errorf("failed to get key from keyring: %w", err) @@ -78,24 +85,82 @@ func (h *TxHelper) ExecuteTransaction(ctx context.Context, msgCreator func(creat addr, err := key.GetAddress() if err != nil { - return nil, fmt.Errorf("failed to get address from key: %w", err) + return nil, fmt.Errorf("failed to get address: %w", err) } creator := addr.String() - // Step 2: Get account info - accInfoRes, err := h.authmod.AccountInfoByAddress(ctx, creator) - if err != nil { - return nil, fmt.Errorf("failed to get account info: %w", err) + // --- Step 2: Local sequence initialization (run once) --- + if !h.seqInit { + accInfoRes, err := h.authmod.AccountInfoByAddress(ctx, creator) + if err != nil { + return nil, fmt.Errorf("failed to fetch initial account info: %w", err) + } + + h.accountNumber = accInfoRes.Info.AccountNumber + h.nextSequence = accInfoRes.Info.Sequence + h.seqInit = true } - // Step 3: Create the message using the provided creator function + // --- Step 3: Create message --- msg, err := msgCreator(creator) if err != nil { return nil, fmt.Errorf("failed to create message: %w", err) } - // Step 4: Process transaction - return h.ExecuteTransactionWithMsgs(ctx, []types.Msg{msg}, accInfoRes.Info) + // --- Step 4: Attempt tx (with 1 retry on sequence mismatch) --- + const maxAttempts = 2 + + for attempt := 1; attempt <= maxAttempts; attempt++ { + + // Build a local accountInfo using in-memory sequence + localAcc := &authtypes.BaseAccount{ + AccountNumber: h.accountNumber, + Sequence: h.nextSequence, + Address: creator, + } + + // Run full tx flow + resp, err := h.ExecuteTransactionWithMsgs(ctx, []types.Msg{msg}, localAcc) + if err == nil { + // SUCCESS → bump local sequence and return + h.nextSequence++ + return resp, nil + } + + // Check if this is a sequence mismatch error + if !isSequenceMismatch(err) { + return nil, err // unrelated error → bail out + } + + // If retry unavailable, bubble error + if attempt == maxAttempts { + return nil, fmt.Errorf("sequence mismatch after retry: %w", err) + } + + // --- Retry logic: resync from chain --- + accInfoRes, err2 := h.authmod.AccountInfoByAddress(ctx, creator) + if err2 != nil { + return nil, fmt.Errorf("failed to resync account info after mismatch: %w", err2) + } + + h.accountNumber = accInfoRes.Info.AccountNumber + h.nextSequence = accInfoRes.Info.Sequence + } + + return nil, fmt.Errorf("unreachable state in ExecuteTransaction") +} + +func isSequenceMismatch(err error) bool { + if err == nil { + return false + } + + msg := err.Error() + + return strings.Contains(msg, "incorrect account sequence") || + strings.Contains(msg, "account sequence mismatch") || + (strings.Contains(msg, "expected") && strings.Contains(msg, "got")) + } // ExecuteTransactionWithMsgs processes a transaction with pre-created messages and account info @@ -133,45 +198,47 @@ func (h *TxHelper) GetAccountInfo(ctx context.Context) (*authtypes.BaseAccount, return accInfoRes.Info, nil } -// UpdateConfig allows updating the transaction configuration func (h *TxHelper) UpdateConfig(config *TxHelperConfig) { - // Merge provided fields with existing config to avoid zeroing defaults if h.config == nil { h.config = &TxConfig{} } - // ChainID - if config.ChainID != "" { - h.config.ChainID = config.ChainID - } - // Keyring - if config.Keyring != nil { + keyChanged := false + + if config.Keyring != nil && config.Keyring != h.config.Keyring { h.config.Keyring = config.Keyring + keyChanged = true } - // KeyName - if config.KeyName != "" { + if config.KeyName != "" && config.KeyName != h.config.KeyName { h.config.KeyName = config.KeyName + keyChanged = true + } + + if config.ChainID != "" { + h.config.ChainID = config.ChainID } - // GasLimit if config.GasLimit != 0 { h.config.GasLimit = config.GasLimit } - // GasAdjustment if config.GasAdjustment != 0 { h.config.GasAdjustment = config.GasAdjustment } - // GasPadding if config.GasPadding != 0 { h.config.GasPadding = config.GasPadding } - // FeeDenom if config.FeeDenom != "" { h.config.FeeDenom = config.FeeDenom } - // GasPrice if config.GasPrice != "" { h.config.GasPrice = config.GasPrice } + + // If key has changed, reset sequence tracking so we re-init on next tx + if keyChanged { + h.seqInit = false + h.accountNumber = 0 + h.nextSequence = 0 + } } // GetConfig returns the current transaction configuration From abcc7d4dee59e2ca7ffbd9483acad5045643c9b2 Mon Sep 17 00:00:00 2001 From: Matee Ullah Malik Date: Fri, 23 Jan 2026 18:23:12 +0500 Subject: [PATCH 2/2] Patch account sequence bug --- pkg/lumera/modules/tx/helper.go | 51 ++++++++++++++++++--- pkg/lumera/modules/tx/helper_test.go | 67 ++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 pkg/lumera/modules/tx/helper_test.go diff --git a/pkg/lumera/modules/tx/helper.go b/pkg/lumera/modules/tx/helper.go index 1c7a9dc4..3ce99147 100644 --- a/pkg/lumera/modules/tx/helper.go +++ b/pkg/lumera/modules/tx/helper.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/auth" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -19,6 +20,8 @@ type TxHelper struct { txmod Module config *TxConfig + mu sync.Mutex + accountNumber uint64 nextSequence uint64 seqInit bool @@ -77,6 +80,9 @@ func (h *TxHelper) ExecuteTransaction( msgCreator func(creator string) (types.Msg, error), ) (*sdktx.BroadcastTxResponse, error) { + h.mu.Lock() + defer h.mu.Unlock() + // --- Step 1: Resolve creator address --- key, err := h.config.Keyring.Key(h.config.KeyName) if err != nil { @@ -95,6 +101,9 @@ func (h *TxHelper) ExecuteTransaction( if err != nil { return nil, fmt.Errorf("failed to fetch initial account info: %w", err) } + if accInfoRes == nil || accInfoRes.Info == nil { + return nil, fmt.Errorf("empty account info response for creator %s", creator) + } h.accountNumber = accInfoRes.Info.AccountNumber h.nextSequence = accInfoRes.Info.Sequence @@ -129,18 +138,27 @@ func (h *TxHelper) ExecuteTransaction( // Check if this is a sequence mismatch error if !isSequenceMismatch(err) { - return nil, err // unrelated error → bail out + return resp, err // unrelated error → bail out (preserve response for debugging) } // If retry unavailable, bubble error if attempt == maxAttempts { - return nil, fmt.Errorf("sequence mismatch after retry: %w", err) + return resp, fmt.Errorf("sequence mismatch after retry: %w", err) + } + + // --- Retry logic: prefer expected sequence from the error --- + if expectedSeq, ok := parseExpectedSequence(err); ok { + h.nextSequence = expectedSeq + continue } - // --- Retry logic: resync from chain --- + // Fallback: resync from chain state. accInfoRes, err2 := h.authmod.AccountInfoByAddress(ctx, creator) if err2 != nil { - return nil, fmt.Errorf("failed to resync account info after mismatch: %w", err2) + return resp, fmt.Errorf("failed to resync account info after mismatch: %w", err2) + } + if accInfoRes == nil || accInfoRes.Info == nil { + return resp, fmt.Errorf("empty account info response for creator %s after mismatch", creator) } h.accountNumber = accInfoRes.Info.AccountNumber @@ -155,12 +173,30 @@ func isSequenceMismatch(err error) bool { return false } - msg := err.Error() + msg := strings.ToLower(err.Error()) return strings.Contains(msg, "incorrect account sequence") || strings.Contains(msg, "account sequence mismatch") || - (strings.Contains(msg, "expected") && strings.Contains(msg, "got")) + strings.Contains(msg, "wrong sequence") +} +func parseExpectedSequence(err error) (uint64, bool) { + if err == nil { + return 0, false + } + + msg := strings.ToLower(err.Error()) + idx := strings.Index(msg, "expected ") + if idx == -1 { + return 0, false + } + + var expected, got uint64 + if _, scanErr := fmt.Sscanf(msg[idx:], "expected %d, got %d", &expected, &got); scanErr == nil { + return expected, true + } + + return 0, false } // ExecuteTransactionWithMsgs processes a transaction with pre-created messages and account info @@ -199,6 +235,9 @@ func (h *TxHelper) GetAccountInfo(ctx context.Context) (*authtypes.BaseAccount, } func (h *TxHelper) UpdateConfig(config *TxHelperConfig) { + h.mu.Lock() + defer h.mu.Unlock() + if h.config == nil { h.config = &TxConfig{} } diff --git a/pkg/lumera/modules/tx/helper_test.go b/pkg/lumera/modules/tx/helper_test.go new file mode 100644 index 00000000..57a83740 --- /dev/null +++ b/pkg/lumera/modules/tx/helper_test.go @@ -0,0 +1,67 @@ +package tx + +import ( + "fmt" + "testing" +) + +func TestIsSequenceMismatch(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err error + want bool + }{ + { + name: "grpc simulation mismatch", + err: fmt.Errorf( + "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'", + ), + want: true, + }, + { + name: "broadcast raw_log mismatch", + err: fmt.Errorf( + "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", + ), + want: true, + }, + { + name: "unrelated expected/got", + err: fmt.Errorf("expected 5, got 4"), + want: false, + }, + { + name: "nil", + err: nil, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := isSequenceMismatch(tt.err); got != tt.want { + t.Fatalf("isSequenceMismatch() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseExpectedSequence(t *testing.T) { + t.Parallel() + + err := fmt.Errorf("account sequence mismatch, expected 7855, got 7854: incorrect account sequence") + if got, ok := parseExpectedSequence(err); !ok || got != 7855 { + t.Fatalf("parseExpectedSequence() = (%d, %v), want (7855, true)", got, ok) + } + + if _, ok := parseExpectedSequence(fmt.Errorf("incorrect account sequence")); ok { + t.Fatalf("parseExpectedSequence() unexpectedly matched message without expected/got") + } + + if _, ok := parseExpectedSequence(nil); ok { + t.Fatalf("parseExpectedSequence() unexpectedly matched nil error") + } +}