diff --git a/pkg/config/config.go b/pkg/config/config.go index d7309d7c3..8feeeb4a7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -45,6 +45,8 @@ const ( FlagMaxPendingHeadersAndData = FlagPrefixEvnode + "node.max_pending_headers_and_data" // FlagLazyBlockTime is a flag for specifying the maximum interval between blocks in lazy aggregation mode FlagLazyBlockTime = FlagPrefixEvnode + "node.lazy_block_interval" + // FlagReadinessWindowSeconds configures the time window (in seconds) used to calculate readiness threshold + FlagReadinessWindowSeconds = FlagPrefixEvnode + "node.readiness_window_seconds" // FlagReadinessMaxBlocksBehind configures how many blocks behind best-known head is still considered ready FlagReadinessMaxBlocksBehind = FlagPrefixEvnode + "node.readiness_max_blocks_behind" // FlagClearCache is a flag for clearing the cache @@ -198,6 +200,7 @@ type NodeConfig struct { TrustedHash string `mapstructure:"trusted_hash" yaml:"trusted_hash" comment:"Initial trusted hash used to bootstrap the header exchange service. Allows nodes to start synchronizing from a specific trusted point in the chain instead of genesis. When provided, the node will fetch the corresponding header/block from peers using this hash and use it as a starting point for synchronization. If not provided, the node will attempt to fetch the genesis block instead."` // Readiness / health configuration + ReadinessWindowSeconds uint64 `mapstructure:"readiness_window_seconds" yaml:"readiness_window_seconds" comment:"Time window in seconds used to calculate ReadinessMaxBlocksBehind based on block time. Default: 15 seconds."` ReadinessMaxBlocksBehind uint64 `mapstructure:"readiness_max_blocks_behind" yaml:"readiness_max_blocks_behind" comment:"How many blocks behind best-known head the node can be and still be considered ready. 0 means must be exactly at head."` } @@ -314,6 +317,7 @@ func AddFlags(cmd *cobra.Command) { cmd.Flags().Bool(FlagLazyAggregator, def.Node.LazyMode, "produce blocks only when transactions are available or after lazy block time") cmd.Flags().Uint64(FlagMaxPendingHeadersAndData, def.Node.MaxPendingHeadersAndData, "maximum headers or data pending DA confirmation before pausing block production (0 for no limit)") cmd.Flags().Duration(FlagLazyBlockTime, def.Node.LazyBlockInterval.Duration, "maximum interval between blocks in lazy aggregation mode") + cmd.Flags().Uint64(FlagReadinessWindowSeconds, def.Node.ReadinessWindowSeconds, "time window in seconds for calculating readiness threshold based on block time (default: 15s)") cmd.Flags().Uint64(FlagReadinessMaxBlocksBehind, def.Node.ReadinessMaxBlocksBehind, "how many blocks behind best-known head the node can be and still be considered ready (0 = must be at head)") // Data Availability configuration flags diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 3f9b85913..4b4119db2 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -62,6 +62,7 @@ func TestAddFlags(t *testing.T) { assertFlagValue(t, flags, FlagLazyAggregator, DefaultConfig().Node.LazyMode) assertFlagValue(t, flags, FlagMaxPendingHeadersAndData, DefaultConfig().Node.MaxPendingHeadersAndData) assertFlagValue(t, flags, FlagLazyBlockTime, DefaultConfig().Node.LazyBlockInterval.Duration) + assertFlagValue(t, flags, FlagReadinessWindowSeconds, DefaultConfig().Node.ReadinessWindowSeconds) assertFlagValue(t, flags, FlagReadinessMaxBlocksBehind, DefaultConfig().Node.ReadinessMaxBlocksBehind) // DA flags @@ -103,7 +104,7 @@ func TestAddFlags(t *testing.T) { assertFlagValue(t, flags, FlagRPCAddress, DefaultConfig().RPC.Address) // Count the number of flags we're explicitly checking - expectedFlagCount := 38 // Update this number if you add more flag checks above + expectedFlagCount := 39 // Update this number if you add more flag checks above // Get the actual number of flags (both regular and persistent) actualFlagCount := 0 diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 4f18d9644..0e20e2a87 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -36,8 +36,23 @@ func DefaultRootDirWithName(appName string) string { return filepath.Join(home, "."+appName) } +// calculateReadinessMaxBlocksBehind calculates how many blocks represent the readiness window +// based on the given block time and window duration in seconds. This allows for normal +// batch-sync latency while detecting stuck nodes. +func calculateReadinessMaxBlocksBehind(blockTime time.Duration, windowSeconds uint64) uint64 { + if blockTime == 0 { + return 30 // fallback to safe default if blockTime is not set + } + if windowSeconds == 0 { + windowSeconds = 15 // fallback to default 15s window + } + return uint64(time.Duration(windowSeconds) * time.Second / blockTime) +} + // DefaultConfig keeps default values of NodeConfig func DefaultConfig() Config { + defaultBlockTime := DurationWrapper{1 * time.Second} + defaultReadinessWindowSeconds := uint64(15) return Config{ RootDir: DefaultRootDir, DBPath: "data", @@ -47,12 +62,13 @@ func DefaultConfig() Config { }, Node: NodeConfig{ Aggregator: false, - BlockTime: DurationWrapper{1 * time.Second}, + BlockTime: defaultBlockTime, LazyMode: false, LazyBlockInterval: DurationWrapper{60 * time.Second}, Light: false, TrustedHash: "", - ReadinessMaxBlocksBehind: 3, + ReadinessWindowSeconds: defaultReadinessWindowSeconds, + ReadinessMaxBlocksBehind: calculateReadinessMaxBlocksBehind(defaultBlockTime.Duration, defaultReadinessWindowSeconds), }, DA: DAConfig{ Address: "http://localhost:7980", diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index f177a404c..6b3848e8c 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -409,8 +409,8 @@ func TestHealthReadyEndpoint(t *testing.T) { }{ {name: "at_head", local: 100, bestKnown: 100, peers: 1, expectedCode: http.StatusOK}, {name: "within_1_block", local: 99, bestKnown: 100, peers: 1, expectedCode: http.StatusOK}, - {name: "within_3_blocks", local: 97, bestKnown: 100, peers: 1, expectedCode: http.StatusOK}, - {name: "just_over_3_blocks", local: 96, bestKnown: 100, peers: 1, expectedCode: http.StatusServiceUnavailable}, + {name: "within_15_blocks", local: 85, bestKnown: 100, peers: 1, expectedCode: http.StatusOK}, + {name: "just_over_15_blocks", local: 84, bestKnown: 100, peers: 1, expectedCode: http.StatusServiceUnavailable}, {name: "local_ahead", local: 101, bestKnown: 100, peers: 1, expectedCode: http.StatusOK}, {name: "no_blocks_yet", local: 0, bestKnown: 100, peers: 1, expectedCode: http.StatusServiceUnavailable}, {name: "unknown_best_known", local: 100, bestKnown: 0, peers: 1, expectedCode: http.StatusServiceUnavailable},