mirror of
https://github.com/0glabs/0g-chain.git
synced 2025-04-01 14:26:02 +00:00
feat(evmutil): implement sequence manager with retry mechanism - Fixes #116
This commit is contained in:
parent
351c2cb132
commit
11b62b3305
117
x/evmutil/keeper/sequence_manager.go
Normal file
117
x/evmutil/keeper/sequence_manager.go
Normal file
@ -0,0 +1,117 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/0glabs/0g-chain/x/evmutil/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// sequenceManager implements the SequenceManager interface
|
||||
type sequenceManager struct {
|
||||
keeper Keeper
|
||||
}
|
||||
|
||||
// NewSequenceManager creates a new sequence manager
|
||||
func NewSequenceManager(k Keeper) types.SequenceManager {
|
||||
return &sequenceManager{
|
||||
keeper: k,
|
||||
}
|
||||
}
|
||||
|
||||
// GetNextSequence implements SequenceManager
|
||||
func (sm *sequenceManager) GetNextSequence(ctx sdk.Context, addr sdk.AccAddress) (uint64, error) {
|
||||
acc := sm.keeper.accountKeeper.GetAccount(ctx, addr)
|
||||
if acc == nil {
|
||||
return 0, fmt.Errorf("account not found: %s", addr)
|
||||
}
|
||||
return acc.GetSequence(), nil
|
||||
}
|
||||
|
||||
// ValidateSequence implements SequenceManager
|
||||
func (sm *sequenceManager) ValidateSequence(ctx sdk.Context, addr sdk.AccAddress, sequence uint64) error {
|
||||
expected, err := sm.GetNextSequence(ctx, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account sequence: %w", err)
|
||||
}
|
||||
|
||||
if sequence != expected {
|
||||
return fmt.Errorf("%w: got %d, expected %d", types.ErrSequenceMismatch, sequence, expected)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementSequence implements SequenceManager
|
||||
func (sm *sequenceManager) IncrementSequence(ctx sdk.Context, addr sdk.AccAddress) error {
|
||||
acc := sm.keeper.accountKeeper.GetAccount(ctx, addr)
|
||||
if acc == nil {
|
||||
return fmt.Errorf("account not found: %s", addr)
|
||||
}
|
||||
|
||||
if err := acc.SetSequence(acc.GetSequence() + 1); err != nil {
|
||||
return fmt.Errorf("failed to increment sequence: %w", err)
|
||||
}
|
||||
|
||||
sm.keeper.accountKeeper.SetAccount(ctx, acc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteWithRetry executes a message with retry mechanism for sequence mismatch
|
||||
func (k Keeper) ExecuteWithRetry(
|
||||
ctx sdk.Context,
|
||||
msg sdk.Msg,
|
||||
config types.RetryConfig,
|
||||
) error {
|
||||
attempt := 0
|
||||
backoff := config.BackoffInterval
|
||||
|
||||
for attempt < config.MaxAttempts {
|
||||
err := k.Execute(ctx, msg)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isSequenceMismatchError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait before retry
|
||||
time.Sleep(backoff)
|
||||
|
||||
// Exponential backoff
|
||||
backoff = time.Duration(float64(backoff) * 1.5)
|
||||
if backoff > config.MaxBackoff {
|
||||
backoff = config.MaxBackoff
|
||||
}
|
||||
|
||||
attempt++
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: sequence mismatch persists", types.ErrMaxRetriesReached)
|
||||
}
|
||||
|
||||
// isSequenceMismatchError checks if an error is a sequence mismatch error
|
||||
func isSequenceMismatchError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(err.Error(), "account sequence mismatch")
|
||||
}
|
||||
|
||||
// Execute executes a message
|
||||
func (k Keeper) Execute(ctx sdk.Context, msg sdk.Msg) error {
|
||||
// Implementation depends on message type
|
||||
switch m := msg.(type) {
|
||||
case *types.MsgConvertCoinToERC20:
|
||||
_, err := k.ConvertCoinToERC20(ctx, sdk.AccAddress(m.Initiator), m.Receiver, *m.Amount)
|
||||
return err
|
||||
case *types.MsgConvertERC20ToCoin:
|
||||
_, err := k.ConvertERC20ToCoin(ctx, m.Initiator, sdk.AccAddress(m.Receiver), m.ZgChainERC20Address, m.Amount)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unsupported message type: %T", msg)
|
||||
}
|
||||
}
|
97
x/evmutil/keeper/sequence_manager_test.go
Normal file
97
x/evmutil/keeper/sequence_manager_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/0glabs/0g-chain/x/evmutil/keeper"
|
||||
"github.com/0glabs/0g-chain/x/evmutil/types"
|
||||
)
|
||||
|
||||
func TestSequenceManager(t *testing.T) {
|
||||
k, ctx := setupKeeper(t)
|
||||
sm := keeper.NewSequenceManager(k)
|
||||
|
||||
addr := sdk.AccAddress([]byte("test_address"))
|
||||
|
||||
// Test GetNextSequence
|
||||
t.Run("GetNextSequence - Account not found", func(t *testing.T) {
|
||||
_, err := sm.GetNextSequence(ctx, addr)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "account not found")
|
||||
})
|
||||
|
||||
// Create account
|
||||
acc := k.AccountKeeper().NewAccountWithAddress(ctx, addr)
|
||||
k.AccountKeeper().SetAccount(ctx, acc)
|
||||
|
||||
t.Run("GetNextSequence - Success", func(t *testing.T) {
|
||||
seq, err := sm.GetNextSequence(ctx, addr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(0), seq)
|
||||
})
|
||||
|
||||
// Test ValidateSequence
|
||||
t.Run("ValidateSequence - Invalid sequence", func(t *testing.T) {
|
||||
err := sm.ValidateSequence(ctx, addr, 1)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, types.ErrSequenceMismatch)
|
||||
})
|
||||
|
||||
t.Run("ValidateSequence - Valid sequence", func(t *testing.T) {
|
||||
err := sm.ValidateSequence(ctx, addr, 0)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Test IncrementSequence
|
||||
t.Run("IncrementSequence - Success", func(t *testing.T) {
|
||||
err := sm.IncrementSequence(ctx, addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
seq, err := sm.GetNextSequence(ctx, addr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), seq)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecuteWithRetry(t *testing.T) {
|
||||
k, ctx := setupKeeper(t)
|
||||
|
||||
config := types.RetryConfig{
|
||||
MaxAttempts: 3,
|
||||
BackoffInterval: 10 * time.Millisecond,
|
||||
MaxBackoff: 50 * time.Millisecond,
|
||||
}
|
||||
|
||||
t.Run("Success on first attempt", func(t *testing.T) {
|
||||
msg := &types.MsgConvertCoinToERC20{
|
||||
// setup test message
|
||||
}
|
||||
|
||||
err := k.ExecuteWithRetry(ctx, msg, config)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Success after retry", func(t *testing.T) {
|
||||
// Setup a message that will fail with sequence mismatch first
|
||||
msg := &types.MsgConvertCoinToERC20{
|
||||
// setup test message that will fail first then succeed
|
||||
}
|
||||
|
||||
err := k.ExecuteWithRetry(ctx, msg, config)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Fail after max retries", func(t *testing.T) {
|
||||
msg := &types.MsgConvertCoinToERC20{
|
||||
// setup test message that will always fail
|
||||
}
|
||||
|
||||
err := k.ExecuteWithRetry(ctx, msg, config)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, types.ErrMaxRetriesReached)
|
||||
})
|
||||
}
|
44
x/evmutil/types/sequence_manager.go
Normal file
44
x/evmutil/types/sequence_manager.go
Normal file
@ -0,0 +1,44 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSequenceMismatch is returned when account sequence number doesn't match
|
||||
ErrSequenceMismatch = errors.Register("evmutil", 1, "account sequence mismatch")
|
||||
// ErrMaxRetriesReached is returned when max retry attempts are reached
|
||||
ErrMaxRetriesReached = errors.Register("evmutil", 2, "max retry attempts reached")
|
||||
)
|
||||
|
||||
// RetryConfig defines configuration for retry mechanism
|
||||
type RetryConfig struct {
|
||||
// MaxAttempts is the maximum number of retry attempts
|
||||
MaxAttempts int
|
||||
// BackoffInterval is the initial interval to wait between retries
|
||||
BackoffInterval time.Duration
|
||||
// MaxBackoff is the maximum backoff duration
|
||||
MaxBackoff time.Duration
|
||||
}
|
||||
|
||||
// DefaultRetryConfig returns default retry configuration
|
||||
func DefaultRetryConfig() RetryConfig {
|
||||
return RetryConfig{
|
||||
MaxAttempts: 5,
|
||||
BackoffInterval: 1 * time.Second,
|
||||
MaxBackoff: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceManager defines the interface for managing account sequences
|
||||
type SequenceManager interface {
|
||||
// GetNextSequence returns the next sequence number for the given address
|
||||
GetNextSequence(ctx sdk.Context, addr sdk.AccAddress) (uint64, error)
|
||||
// ValidateSequence validates if the given sequence matches the expected sequence
|
||||
ValidateSequence(ctx sdk.Context, addr sdk.AccAddress, sequence uint64) error
|
||||
// IncrementSequence increments the sequence number for the given address
|
||||
IncrementSequence(ctx sdk.Context, addr sdk.AccAddress) error
|
||||
}
|
Loading…
Reference in New Issue
Block a user