Skip to content
Open
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
24 changes: 24 additions & 0 deletions ejector/ejector_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,30 @@ func (e *RootEjectorConfig) Verify() error {
return nil
}

// HasSufficientOnChainMirror checks for cross-domain correctness between the ejector config and
// the EjectionsManager contract's param values.
//
// If improperly set, there'd be performance degradations in the ejector's processing timeline
// where retried ejections could be attempted prematurely as well as finalization claim txs
func (e *EjectorConfig) HasSufficientOnChainMirror(cooldown, finalizationDelay uint64) error {

cooldownSeconds, delaySeconds := time.Duration(cooldown)*time.Second, time.Duration(finalizationDelay)*time.Second

if e.EjectionRetryDelay < cooldownSeconds {
return fmt.Errorf("EjectionRetryDelay must be >= the on-chain cooldown period to prevent premature ejection retry attempts. Current config value: %s, required minimum: %s",
e.EjectionRetryDelay.String(),
cooldownSeconds.String())
}

if e.EjectionFinalizationDelay < delaySeconds {
return fmt.Errorf("EjectionFinalizationDelay must be >= the on-chain finalization delay period to prevent premature ejection finalization attempts. Current config value: %s, required minimum: %s",
e.EjectionFinalizationDelay.String(),
delaySeconds.String())
}

return nil
}

var _ config.VerifiableConfig = (*EjectorSecretConfig)(nil)

// Configuration for secrets used by the ejector.
Expand Down
110 changes: 110 additions & 0 deletions ejector/ejector_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package ejector

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestEjectorConfig_HasSufficientOnChainMirror(t *testing.T) {
tests := []struct {
name string
ejectionRetryDelay time.Duration
ejectionFinalizationDelay time.Duration
onChainCooldown uint64 // seconds
onChainFinalizationDelay uint64 // seconds
expectError bool
errorContains string
}{
{
name: "valid configuration",
ejectionRetryDelay: 48 * time.Hour,
ejectionFinalizationDelay: 2 * time.Hour,
onChainCooldown: uint64((24 * time.Hour).Seconds()),
onChainFinalizationDelay: uint64((1 * time.Hour).Seconds()),
expectError: false,
},
{
name: "valid configuration - delays exactly equal to on-chain values",
ejectionRetryDelay: 24 * time.Hour,
ejectionFinalizationDelay: 1 * time.Hour,
onChainCooldown: uint64((24 * time.Hour).Seconds()),
onChainFinalizationDelay: uint64((1 * time.Hour).Seconds()),
expectError: false,
},
{
name: "invalid - ejection retry delay too small",
ejectionRetryDelay: 12 * time.Hour,
ejectionFinalizationDelay: 2 * time.Hour,
onChainCooldown: uint64((24 * time.Hour).Seconds()),
onChainFinalizationDelay: uint64((1 * time.Hour).Seconds()),
expectError: true,
errorContains: "EjectionRetryDelay must be >= the on-chain cooldown period",
},
{
name: "invalid - ejection finalization delay too small",
ejectionRetryDelay: 48 * time.Hour,
ejectionFinalizationDelay: 30 * time.Minute,
onChainCooldown: uint64((24 * time.Hour).Seconds()),
onChainFinalizationDelay: uint64((1 * time.Hour).Seconds()),
expectError: true,
errorContains: "EjectionFinalizationDelay must be >= the on-chain finalization delay period",
},
{
name: "invalid - both delays too small",
ejectionRetryDelay: 12 * time.Hour,
ejectionFinalizationDelay: 30 * time.Minute,
onChainCooldown: uint64((24 * time.Hour).Seconds()),
onChainFinalizationDelay: uint64((1 * time.Hour).Seconds()),
expectError: true,
errorContains: "EjectionRetryDelay must be >= the on-chain cooldown period",
},
{
name: "edge case - zero on-chain values",
ejectionRetryDelay: 1 * time.Second,
ejectionFinalizationDelay: 1 * time.Second,
onChainCooldown: 0,
onChainFinalizationDelay: 0,
expectError: false,
},
{
name: "edge case - off by one second on retry delay",
ejectionRetryDelay: 24*time.Hour - 1*time.Second,
ejectionFinalizationDelay: 2 * time.Hour,
onChainCooldown: uint64((24 * time.Hour).Seconds()),
onChainFinalizationDelay: uint64((1 * time.Hour).Seconds()),
expectError: true,
errorContains: "EjectionRetryDelay must be >= the on-chain cooldown period",
},
{
name: "edge case - off by one second on finalization delay",
ejectionRetryDelay: 48 * time.Hour,
ejectionFinalizationDelay: 1*time.Hour - 1*time.Second,
onChainCooldown: uint64((24 * time.Hour).Seconds()),
onChainFinalizationDelay: uint64((1 * time.Hour).Seconds()),
expectError: true,
errorContains: "EjectionFinalizationDelay must be >= the on-chain finalization delay period",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &EjectorConfig{
EjectionRetryDelay: tt.ejectionRetryDelay,
EjectionFinalizationDelay: tt.ejectionFinalizationDelay,
}

err := config.HasSufficientOnChainMirror(tt.onChainCooldown, tt.onChainFinalizationDelay)

if tt.expectError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
33 changes: 33 additions & 0 deletions ejector/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"github.com/Layr-Labs/eigenda/common"
"github.com/Layr-Labs/eigenda/common/config"
"github.com/Layr-Labs/eigenda/common/geth"
contractEigenDAEjectionManager "github.com/Layr-Labs/eigenda/contracts/bindings/IEigenDAEjectionManager"
"github.com/Layr-Labs/eigenda/core/eth"
"github.com/Layr-Labs/eigenda/core/eth/directory"
"github.com/Layr-Labs/eigenda/ejector"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
Expand Down Expand Up @@ -89,6 +91,37 @@ func run(ctx context.Context) error {
return fmt.Errorf("failed to get ejection manager address: %w", err)
}

// TODO(ethenotethan): Figure out tighter abstraction or alternative call sites

caller, err := contractEigenDAEjectionManager.NewContractIEigenDAEjectionManagerCaller(
ejectionContractAddress, gethClient)
if err != nil {
return fmt.Errorf("failed to create ejection manager caller: %w", err)
}

cooldownSeconds, err := caller.EjectionCooldown(&bind.CallOpts{Context: ctx})
if err != nil {
return fmt.Errorf("failed to read `cooldown` value from ejection manager contract: %w", err)
}

finalizationDelaySeconds, err := caller.EjectionDelay(&bind.CallOpts{Context: ctx})
if err != nil {
return fmt.Errorf("failed to read `delay` value from ejection manager contract: %w", err)
}

// NOTE: this check is only performed as a precursor invariant during
// app bootstrap. the app main loop doesn't perform this check which can
// result in the onchain (cooldown, delay) parameters changing and invalidating
// the ejector's config.
//
// plainly restarting the ejector in this event is a sufficient mitigation considering
// the service is only currently ran by EigenCloud who also controls the
// ownership role on the EjectionsManager contract responsible for updating onchain params.
err = ejectorConfig.HasSufficientOnChainMirror(cooldownSeconds, finalizationDelaySeconds)
if err != nil {
return fmt.Errorf("failed to comply with current EjectionManager contract params: %w", err)
}

registryCoordinatorAddress, err := contractDirectory.GetContractAddress(ctx, directory.RegistryCoordinator)
if err != nil {
return fmt.Errorf("failed to get registry coordinator address: %w", err)
Expand Down
Loading