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
19 changes: 0 additions & 19 deletions docs/learn/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ This document provides a comprehensive reference for all configuration options a
- [Maximum Pending Blocks](#maximum-pending-blocks)
- [Lazy Mode (Lazy Aggregator)](#lazy-mode-lazy-aggregator)
- [Lazy Block Interval](#lazy-block-interval)
- [Trusted Hash](#trusted-hash)
- [Data Availability Configuration (`da`)](#data-availability-configuration-da)
- [DA Service Address](#da-service-address)
- [DA Authentication Token](#da-authentication-token)
Expand Down Expand Up @@ -275,24 +274,6 @@ _Example:_ `--rollkit.node.lazy_block_interval 1m`
_Default:_ `"30s"`
_Constant:_ `FlagLazyBlockTime`

### Trusted Hash

**Description:**
The initial trusted hash used to bootstrap the header exchange service. This allows nodes to start synchronizing from a specific, trusted point in the chain history instead of from the genesis block. When provided, the node will fetch the corresponding header/block from peers using this hash. If not provided, the node attempts to sync from genesis.

**YAML:**

```yaml
node:
trusted_hash: "YOUR_TRUSTED_HASH_HEX_STRING"
```

**Command-line Flag:**
`--rollkit.node.trusted_hash <string>`
_Example:_ `--rollkit.node.trusted_hash ABCDEF012345...`
_Default:_ `""` (empty, sync from genesis)
_Constant:_ `FlagTrustedHash`

## Data Availability Configuration (`da`)

Parameters for connecting and interacting with the Data Availability (DA) layer, which Evolve uses to publish block data.
Expand Down
7 changes: 3 additions & 4 deletions docs/learn/specs/header-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ The Executor component (in aggregator nodes) broadcasts headers and data in para
- Chain IDs for pubsub topics are also separated:
- Headers: `{chainID}-headerSync` creates topic like `/gm-headerSync/header-sub/v0.0.1`
- Data: `{chainID}-dataSync` creates topic like `/gm-dataSync/header-sub/v0.0.1`
- Both stores must be initialized with genesis items before starting:
- Header store needs genesis header
- Data store needs genesis data (if applicable)
- Genesis items can be loaded via `NodeConfig.TrustedHash` or P2P network query
- Both stores must contain at least one item before the syncer starts:
- On first boot, the services fetch the configured genesis height from peers
- On restart, each store reuses its latest item to derive the initial height requested from peers
- Sync services work only when connected to P2P network via `P2PConfig.Seeds`
- Node context is passed to all components for graceful shutdown
- Headers and data are linked through DataHash but synced independently
Expand Down
74 changes: 2 additions & 72 deletions node/full_node_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,9 @@ func TestSingleSequencerTwoFullNodesBlockSyncSpeed(t *testing.T) {

// TestDataExchange verifies data exchange and synchronization between nodes in various network topologies.
//
// This test runs three sub-tests:
// This test runs two sub-tests:
// 1. Single sequencer and single full
// 2. Single sequencer and two full nodes.
// 3. Single sequencer and single full node with trusted hash.
//
// Each sub-test checks data exchange and synchronization to ensure correct data propagation and consistency across nodes.
func TestDataExchange(t *testing.T) {
Expand All @@ -273,17 +272,13 @@ func TestDataExchange(t *testing.T) {
t.Run("SingleSequencerTwoFullNodes", func(t *testing.T) {
testSingleSequencerTwoFullNodes(t, Data)
})
t.Run("SingleSequencerSingleFullNodeTrustedHash", func(t *testing.T) {
testSingleSequencerSingleFullNodeTrustedHash(t, Data)
})
}

// TestHeaderExchange verifies header exchange and synchronization between nodes in various network topologies.
//
// This test runs three sub-tests:
// This test runs two sub-tests:
// 1. Single sequencer and single full
// 2. Single sequencer and two full nodes.
// 3. Single sequencer and single full node with trusted hash.
//
// Each sub-test checks header exchange and synchronization to ensure correct header propagation and consistency across nodes.
func TestHeaderExchange(t *testing.T) {
Expand All @@ -293,9 +288,6 @@ func TestHeaderExchange(t *testing.T) {
t.Run("SingleSequencerTwoFullNodes", func(t *testing.T) {
testSingleSequencerTwoFullNodes(t, Header)
})
t.Run("SingleSequencerSingleFullNodeTrustedHash", func(t *testing.T) {
testSingleSequencerSingleFullNodeTrustedHash(t, Header)
})
}

// testSingleSequencerSingleFullNode sets up a single sequencer and a single full node, starts the sequencer, waits for it to produce a block, then starts the full
Expand Down Expand Up @@ -392,68 +384,6 @@ func testSingleSequencerTwoFullNodes(t *testing.T, source Source) {
shutdownAndWait(t, cancels, &runningWg, 5*time.Second)
}

// testSingleSequencerSingleFullNodeTrustedHash sets up a single sequencer and a single full node with a trusted hash, starts the sequencer, waits for it to produce a block, then starts the full node with the trusted hash.
// It waits for both nodes to reach a target block height (using the provided 'source' to determine block inclusion), verifies that both nodes are fully synced, and then shuts them down.
func testSingleSequencerSingleFullNodeTrustedHash(t *testing.T, source Source) {
require := require.New(t)

// Set up one sequencer and one full node
config := getTestConfig(t, 1)
numNodes := 2
nodes, cleanups := createNodesWithCleanup(t, numNodes, config)
for _, cleanup := range cleanups {
defer cleanup()
}

ctxs, cancels := createNodeContexts(numNodes)
var runningWg sync.WaitGroup
errChan := make(chan error, numNodes)

// Start the sequencer first
startNodeInBackground(t, nodes, ctxs, &runningWg, 0, errChan)

// Wait for the sequencer to produce at first block
require.NoError(waitForFirstBlock(nodes[0], source))

// Get the hash of the first block (using the correct source)
var trustedHash string
switch source {
case Data:
trustedHashValue, err := nodes[0].dSyncService.Store().GetByHeight(ctxs[0], 1)
require.NoError(err)
trustedHash = trustedHashValue.Hash().String()
case Header:
trustedHashValue, err := nodes[0].hSyncService.Store().GetByHeight(ctxs[0], 1)
require.NoError(err)
trustedHash = trustedHashValue.Hash().String()
default:
t.Fatalf("unsupported source for trusted hash test: %v", source)
}

// Set the trusted hash in the full node
nodeConfig := nodes[1].nodeConfig
nodeConfig.Node.TrustedHash = trustedHash

// Add a small delay to ensure P2P services are fully ready
time.Sleep(500 * time.Millisecond)

// Start the full node
startNodeInBackground(t, nodes, ctxs, &runningWg, 1, errChan)

blocksToWaitFor := uint64(3)
// Wait for both nodes to reach at least blocksToWaitFor blocks
for _, nodeItem := range nodes {
requireEmptyChan(t, errChan)
require.NoError(waitForAtLeastNBlocks(nodeItem, blocksToWaitFor, source))
}

// Verify both nodes are synced using the helper
require.NoError(verifyNodesSynced(nodes[0], nodes[1], source))

// Cancel all node contexts to signal shutdown and wait
shutdownAndWait(t, cancels, &runningWg, 5*time.Second)
}

// TestTwoChainsInOneNamespace verifies that two chains in the same namespace can coexist without any issues.
func TestTwoChainsInOneNamespace(t *testing.T) {
cases := []struct {
Expand Down
2 changes: 0 additions & 2 deletions pkg/cmd/run_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ func TestParseFlags(t *testing.T) {
"--rollkit.node.lazy_block_interval", "2m",
"--rollkit.node.light",
"--rollkit.node.max_pending_headers_and_data", "100",
"--rollkit.node.trusted_hash", "abcdef1234567890",
"--rollkit.da.submit_options", "custom-options",
// Instrumentation flags
"--rollkit.instrumentation.prometheus", "true",
Expand Down Expand Up @@ -125,7 +124,6 @@ func TestParseFlags(t *testing.T) {
{"LazyBlockTime", nodeConfig.Node.LazyBlockInterval.Duration, 2 * time.Minute},
{"Light", nodeConfig.Node.Light, true},
{"MaxPendingHeadersAndData", nodeConfig.Node.MaxPendingHeadersAndData, uint64(100)},
{"TrustedHash", nodeConfig.Node.TrustedHash, "abcdef1234567890"},
{"DASubmitOptions", nodeConfig.DA.SubmitOptions, "custom-options"},
{"Prometheus", nodeConfig.Instrumentation.Prometheus, true},
{"PrometheusListenAddr", nodeConfig.Instrumentation.PrometheusListenAddr, ":26665"},
Expand Down
6 changes: 0 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ const (
FlagLight = FlagPrefixEvnode + "node.light"
// FlagBlockTime is a flag for specifying the block time
FlagBlockTime = FlagPrefixEvnode + "node.block_time"
// FlagTrustedHash is a flag for specifying the trusted hash
FlagTrustedHash = FlagPrefixEvnode + "node.trusted_hash"
// FlagLazyAggregator is a flag for enabling lazy aggregation mode that only produces blocks when transactions are available
FlagLazyAggregator = FlagPrefixEvnode + "node.lazy_mode"
// FlagMaxPendingHeadersAndData is a flag to limit and pause block production when too many headers or data are waiting for DA confirmation
Expand Down Expand Up @@ -192,9 +190,6 @@ type NodeConfig struct {
LazyMode bool `mapstructure:"lazy_mode" yaml:"lazy_mode" comment:"Enables lazy aggregation mode, where blocks are only produced when transactions are available or after LazyBlockTime. Optimizes resources by avoiding empty block creation during periods of inactivity."`
LazyBlockInterval DurationWrapper `mapstructure:"lazy_block_interval" yaml:"lazy_block_interval" comment:"Maximum interval between blocks in lazy aggregation mode (LazyAggregator). Ensures blocks are produced periodically even without transactions to keep the chain active. Generally larger than BlockTime."`

// Header configuration
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."`
Expand Down Expand Up @@ -309,7 +304,6 @@ func AddFlags(cmd *cobra.Command) {
cmd.Flags().Bool(FlagAggregator, def.Node.Aggregator, "run node in aggregator mode")
cmd.Flags().Bool(FlagLight, def.Node.Light, "run light client")
cmd.Flags().Duration(FlagBlockTime, def.Node.BlockTime.Duration, "block time (for aggregator mode)")
cmd.Flags().String(FlagTrustedHash, def.Node.TrustedHash, "initial trusted hash to start the header exchange service")
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")
Expand Down
4 changes: 1 addition & 3 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ func TestDefaultConfig(t *testing.T) {
assert.Equal(t, uint64(0), def.Node.MaxPendingHeadersAndData)
assert.Equal(t, false, def.Node.LazyMode)
assert.Equal(t, 60*time.Second, def.Node.LazyBlockInterval.Duration)
assert.Equal(t, "", def.Node.TrustedHash)
assert.Equal(t, "file", def.Signer.SignerType)
assert.Equal(t, "config", def.Signer.SignerPath)
assert.Equal(t, "127.0.0.1:7331", def.RPC.Address)
Expand All @@ -56,7 +55,6 @@ func TestAddFlags(t *testing.T) {
assertFlagValue(t, flags, FlagAggregator, DefaultConfig().Node.Aggregator)
assertFlagValue(t, flags, FlagLight, DefaultConfig().Node.Light)
assertFlagValue(t, flags, FlagBlockTime, DefaultConfig().Node.BlockTime.Duration)
assertFlagValue(t, flags, FlagTrustedHash, DefaultConfig().Node.TrustedHash)
assertFlagValue(t, flags, FlagLazyAggregator, DefaultConfig().Node.LazyMode)
assertFlagValue(t, flags, FlagMaxPendingHeadersAndData, DefaultConfig().Node.MaxPendingHeadersAndData)
assertFlagValue(t, flags, FlagLazyBlockTime, DefaultConfig().Node.LazyBlockInterval.Duration)
Expand Down Expand Up @@ -101,7 +99,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 := 37 // Update this number if you add more flag checks above

// Get the actual number of flags (both regular and persistent)
actualFlagCount := 0
Expand Down
1 change: 0 additions & 1 deletion pkg/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ func DefaultConfig() Config {
LazyMode: false,
LazyBlockInterval: DurationWrapper{60 * time.Second},
Light: false,
TrustedHash: "",
ReadinessWindowSeconds: defaultReadinessWindowSeconds,
ReadinessMaxBlocksBehind: calculateReadinessMaxBlocksBehind(defaultBlockTime.Duration, defaultReadinessWindowSeconds),
},
Expand Down
35 changes: 18 additions & 17 deletions pkg/sync/sync_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sync

import (
"context"
"encoding/hex"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -309,31 +308,33 @@ func (syncService *SyncService[H]) startSubscriber(ctx context.Context) error {
}

// initFromP2PWithRetry initializes the syncer from P2P with a retry mechanism.
// If trusted hash is available, it fetches the trusted header/block (by hash) from peers.
// Otherwise, it tries to fetch the genesis header/block by height.
// It inspects the local store to determine the first height to request:
// - when the store already contains items, it reuses the latest height as the starting point;
// - otherwise, it falls back to the configured genesis height.
func (syncService *SyncService[H]) initFromP2PWithRetry(ctx context.Context, peerIDs []peer.ID) error {
if len(peerIDs) == 0 {
return nil
}

tryInit := func(ctx context.Context) (bool, error) {
var (
trusted H
err error
trusted H
err error
heightToQuery uint64
)

if syncService.conf.Node.TrustedHash != "" {
trustedHashBytes, err := hex.DecodeString(syncService.conf.Node.TrustedHash)
if err != nil {
return false, fmt.Errorf("failed to parse the trusted hash for initializing the store: %w", err)
}
if trusted, err = syncService.ex.Get(ctx, trustedHashBytes); err != nil {
return false, fmt.Errorf("failed to fetch the trusted header/block for initializing the store: %w", err)
}
} else {
if trusted, err = syncService.ex.GetByHeight(ctx, syncService.genesis.InitialHeight); err != nil {
return false, fmt.Errorf("failed to fetch the genesis: %w", err)
}
head, headErr := syncService.store.Head(ctx)
switch {
case errors.Is(headErr, header.ErrNotFound), errors.Is(headErr, header.ErrEmptyStore):
heightToQuery = syncService.genesis.InitialHeight
case headErr != nil:
return false, fmt.Errorf("failed to inspect local store head: %w", headErr)
default:
heightToQuery = head.Height()
}

if trusted, err = syncService.ex.GetByHeight(ctx, heightToQuery); err != nil {
return false, fmt.Errorf("failed to fetch height %d from peers: %w", heightToQuery, err)
}

if _, err := syncService.initStore(ctx, trusted); err != nil {
Expand Down
Loading