feat(evmutil): implement sequence manager with retry mechanism - Fixes #116

This commit is contained in:
0xalkindivv.eth 2025-03-04 16:42:59 +07:00
parent 351c2cb132
commit 11b62b3305
3 changed files with 258 additions and 0 deletions

View 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)
}
}

View 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)
})
}

View 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
}