-
Notifications
You must be signed in to change notification settings - Fork 0
maintain account sequence #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,35 +72,95 @@ 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) | ||
| } | ||
|
|
||
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking only Incrementing if err == nil {
if resp != nil && resp.TxResponse != nil && resp.TxResponse.Code == 0 {
// SUCCESS → bump local sequence
h.nextSequence++
}
return resp, nil
}Fix it with Roo Code or mention @roomote and request a fix. |
||
| // 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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
TxHelpernow maintains internal state (accountNumber,nextSequence), which makes it not thread-safe. Whileaction_msgcurrently handles synchronization externally, this makesTxHelperdangerous to use in other contexts.Consider adding a
sync.MutextoTxHelperand locking inExecuteTransaction(and other state-modifying methods) to ensure thread safety.Fix it with Roo Code or mention @roomote and request a fix.