diff --git a/x/evmutil/keeper/sequence_manager.go b/x/evmutil/keeper/sequence_manager.go new file mode 100644 index 00000000..9b999bf1 --- /dev/null +++ b/x/evmutil/keeper/sequence_manager.go @@ -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) + } +} \ No newline at end of file diff --git a/x/evmutil/keeper/sequence_manager_test.go b/x/evmutil/keeper/sequence_manager_test.go new file mode 100644 index 00000000..f3793c64 --- /dev/null +++ b/x/evmutil/keeper/sequence_manager_test.go @@ -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) + }) +} \ No newline at end of file diff --git a/x/evmutil/types/sequence_manager.go b/x/evmutil/types/sequence_manager.go new file mode 100644 index 00000000..0d3515d0 --- /dev/null +++ b/x/evmutil/types/sequence_manager.go @@ -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 +} \ No newline at end of file