Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bridgesync/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package bridgesync

import (
"fmt"

"github.com/agglayer/aggkit/config/types"
aggkittypes "github.com/agglayer/aggkit/types"
"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -32,3 +34,11 @@ type Config struct {
// This is separate from HTTP timeouts to allow database operations more time when needed
DBQueryTimeout types.Duration `mapstructure:"DBQueryTimeout"`
}

// Validate checks if the configuration is valid
func (c Config) Validate() error {
if err := c.BlockFinality.Validate(); err != nil {
return fmt.Errorf("invalid BlockFinality configuration: %w", err)
}
return nil
}
47 changes: 47 additions & 0 deletions bridgesync/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package bridgesync

import (
"testing"

aggkittypes "github.com/agglayer/aggkit/types"
"github.com/stretchr/testify/require"
)

func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
config Config
expectedError string
}{
{
name: "valid config",
config: Config{
BlockFinality: aggkittypes.SafeBlock,
},
expectedError: "",
},
{
name: "invalid config with invalid BlockFinality",
config: Config{
BlockFinality: aggkittypes.BlockNumberFinality{
Block: aggkittypes.Latest,
Offset: 1, // Invalid: LatestBlock cannot have positive offset
},
},
expectedError: "invalid BlockFinality configuration:",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()

if tt.expectedError == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
}
})
}
}
7 changes: 7 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@ func runReorgDetectorL1IfNeeded(
components) {
return nil, nil
}
if err := cfg.Validate(); err != nil {
log.Fatalf("invalid ReorgDetectorL1 config: %v", err)
}

rd := newReorgDetector(cfg, l1Client, reorgdetector.L1)
errChan := make(chan error)
Expand Down Expand Up @@ -690,6 +693,10 @@ func runBridgeSyncL1IfNeeded(
return nil
}

if err := cfg.Validate(); err != nil {
log.Fatalf("invalid BridgeL1Sync config: %v", err)
}

bridgeSyncL1, err := bridgesync.NewL1(
ctx,
cfg,
Expand Down
9 changes: 9 additions & 0 deletions reorgdetector/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package reorgdetector

import (
"fmt"
"time"

"github.com/agglayer/aggkit/config/types"
Expand All @@ -23,6 +24,14 @@ type Config struct {
FinalizedBlock aggkittypes.BlockNumberFinality `jsonschema:"enum=LatestBlock, enum=SafeBlock, enum=PendingBlock, enum=FinalizedBlock, enum=EarliestBlock" mapstructure:"FinalizedBlock"` //nolint:lll
}

// Validate checks if the configuration is valid
func (c *Config) Validate() error {
if err := c.FinalizedBlock.Validate(); err != nil {
return fmt.Errorf("invalid FinalizedBlock configuration: %w", err)
}
return nil
}

// GetCheckReorgsInterval returns the interval to check for reorgs in tracked blocks
func (c *Config) GetCheckReorgsInterval() time.Duration {
if c.CheckReorgsInterval.Duration == 0 {
Expand Down
47 changes: 47 additions & 0 deletions reorgdetector/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package reorgdetector

import (
"testing"

aggkittypes "github.com/agglayer/aggkit/types"
"github.com/stretchr/testify/require"
)

func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
config Config
expectedError string
}{
{
name: "valid config",
config: Config{
FinalizedBlock: aggkittypes.SafeBlock,
},
expectedError: "",
},
{
name: "invalid config with invalid FinalizedBlock",
config: Config{
FinalizedBlock: aggkittypes.BlockNumberFinality{
Block: aggkittypes.Latest,
Offset: 1, // Invalid: LatestBlock cannot have positive offset
},
},
expectedError: "invalid FinalizedBlock configuration:",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()

if tt.expectedError == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
}
})
}
}
47 changes: 44 additions & 3 deletions types/block_finality.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const (
EmptyBlockName = ""

blockNameAndOffsetSeparator = "/"

// Maximum positive offset limits for each block finality type
MaxPositiveOffsetLatest = int64(0) // LatestBlock cannot have positive offset (cannot go beyond latest)
MaxPositiveOffsetFinalized = int64(32) // ~1 epoch on Ethereum
MaxPositiveOffsetSafe = int64(64) // ~2 epochs
MaxPositiveOffsetPending = int64(0) // Pending blocks don't exist yet, cannot go forward
)

var (
Expand Down Expand Up @@ -57,9 +63,6 @@ func NewBlockNumberFinality(s string) (BlockNumberFinality, error) {
}
result.Offset = offset
}
if result.Block == Latest && result.Offset > 0 {
return result, fmt.Errorf("invalid block finality: cannot have positive offset with LatestBlock")
}
return result, nil
}

Expand Down Expand Up @@ -120,6 +123,44 @@ func (b BlockNumberFinality) IsLatest() bool {
return b.Block == Latest && b.Offset >= 0
}

// Validate validates the BlockNumberFinality configuration, ensuring that:
// - The block name is valid (one of LatestBlock, SafeBlock, FinalizedBlock, or PendingBlock)
// - The positive offset does not exceed the maximum allowed for the specific block finality type
// - LatestBlock: cannot have positive offset (limit = 0)
// - PendingBlock: cannot have positive offset (limit = 0) as pending blocks don't exist yet
// - SafeBlock: maximum positive offset is MaxPositiveOffsetSafe
// - FinalizedBlock: maximum positive offset is MaxPositiveOffsetFinalized (most restrictive)
func (b BlockNumberFinality) Validate() error {
if b.Block != Latest && b.Block != Pending && b.Block != Safe && b.Block != Finalized {
return fmt.Errorf(
"invalid block finality: block type must be one of LatestBlock, SafeBlock, "+
"FinalizedBlock, or PendingBlock (got: %s)",
b.String(),
)
}

var maxOffset int64
switch b.Block {
case Latest:
maxOffset = MaxPositiveOffsetLatest
case Pending:
maxOffset = MaxPositiveOffsetPending
case Safe:
maxOffset = MaxPositiveOffsetSafe
case Finalized:
maxOffset = MaxPositiveOffsetFinalized
}

// Validate offset limits (negative or zero offsets are always valid)
if b.Offset > maxOffset {
return fmt.Errorf(
"positive offset %d exceeds maximum allowed %d for %s (got: %s)",
b.Offset, maxOffset, b.Block.String(), b.String(),
)
}
return nil
}

// BlockNumber gets the block number from RPC with offset taken into account
func (b *BlockNumberFinality) BlockNumber(
ctx context.Context,
Expand Down
111 changes: 111 additions & 0 deletions types/block_finality_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,114 @@ func TestBlockNumberFinality_BlockHeader(t *testing.T) {
require.Contains(t, err.Error(), testErr.Error())
})
}

func TestBlockNumberFinality_Validate(t *testing.T) {
tests := []struct {
name string
finality BlockNumberFinality
expectedError string
}{
{
name: "LatestBlock with positive offset should fail",
finality: BlockNumberFinality{Block: Latest, Offset: 1},
expectedError: fmt.Sprintf("positive offset 1 exceeds maximum allowed %d for LatestBlock", MaxPositiveOffsetLatest),
},
{
name: "LatestBlock with zero offset should pass",
finality: BlockNumberFinality{Block: Latest, Offset: 0},
expectedError: "",
},
{
name: "LatestBlock with negative offset should pass",
finality: BlockNumberFinality{Block: Latest, Offset: -5},
expectedError: "",
},
{
name: "PendingBlock with positive offset should fail",
finality: BlockNumberFinality{Block: Pending, Offset: 1},
expectedError: fmt.Sprintf("positive offset 1 exceeds maximum allowed %d for PendingBlock", MaxPositiveOffsetPending),
},
{
name: "PendingBlock with zero offset should pass",
finality: BlockNumberFinality{Block: Pending, Offset: 0},
expectedError: "",
},
{
name: "SafeBlock with offset exceeding limit should fail",
finality: BlockNumberFinality{Block: Safe, Offset: MaxPositiveOffsetSafe + 1},
expectedError: fmt.Sprintf("positive offset %d exceeds maximum allowed %d for SafeBlock", MaxPositiveOffsetSafe+1, MaxPositiveOffsetSafe),
},
{
name: "SafeBlock with offset at limit should pass",
finality: BlockNumberFinality{Block: Safe, Offset: MaxPositiveOffsetSafe},
expectedError: "",
},
{
name: "SafeBlock with offset below limit should pass",
finality: BlockNumberFinality{Block: Safe, Offset: MaxPositiveOffsetSafe - 1},
expectedError: "",
},
{
name: "FinalizedBlock with offset exceeding limit should fail",
finality: BlockNumberFinality{Block: Finalized, Offset: MaxPositiveOffsetFinalized + 1},
expectedError: fmt.Sprintf("positive offset %d exceeds maximum allowed %d for FinalizedBlock", MaxPositiveOffsetFinalized+1, MaxPositiveOffsetFinalized),
},
{
name: "FinalizedBlock with offset at limit should pass",
finality: BlockNumberFinality{Block: Finalized, Offset: MaxPositiveOffsetFinalized},
expectedError: "",
},
{
name: "FinalizedBlock with offset below limit should pass",
finality: BlockNumberFinality{Block: Finalized, Offset: MaxPositiveOffsetFinalized - 1},
expectedError: "",
},
{
name: "SafeBlock with negative offset should pass",
finality: BlockNumberFinality{Block: Safe, Offset: -10},
expectedError: "",
},
{
name: "FinalizedBlock with negative offset should pass",
finality: BlockNumberFinality{Block: Finalized, Offset: -10},
expectedError: "",
},
{
name: "Empty block should fail validation",
finality: BlockNumberFinality{Block: Empty, Offset: 0},
expectedError: "block type must be one of LatestBlock, SafeBlock, FinalizedBlock, or PendingBlock",
},
{
name: "Empty block with positive offset should fail validation",
finality: BlockNumberFinality{Block: Empty, Offset: 100},
expectedError: "block type must be one of LatestBlock, SafeBlock, FinalizedBlock, or PendingBlock",
},
{
name: "Unknown block type should fail validation",
finality: BlockNumberFinality{Block: BlockNumber(999), Offset: 0},
expectedError: "block type must be one of LatestBlock, SafeBlock, FinalizedBlock, or PendingBlock",
},
{
name: "Valid block with zero offset should pass",
finality: BlockNumberFinality{Block: Finalized, Offset: 0},
expectedError: "",
},
{
name: "Valid block with negative offset should pass",
finality: BlockNumberFinality{Block: Safe, Offset: -5},
expectedError: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.finality.Validate()
if tt.expectedError == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedError)
}
})
}
}
Loading