Skip to content
Closed
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
73 changes: 55 additions & 18 deletions block/internal/syncing/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,25 +304,62 @@ func (s *Syncer) initializeState() error {
// Load state from store
state, err := s.store.GetState(s.ctx)
if err != nil {
// Initialize new chain state for a fresh full node (no prior state on disk)
// Mirror executor initialization to ensure AppHash matches headers produced by the sequencer.
stateRoot, initErr := s.exec.InitChain(
s.ctx,
s.genesis.StartTime,
s.genesis.InitialHeight,
s.genesis.ChainID,
)
if initErr != nil {
return fmt.Errorf("failed to initialize execution client: %w", initErr)
}
// initializeStateFromTrustedHeight initializes the sync state from a trusted height.
// This allows a syncing node to start from a known, verified block height instead of genesis.
if s.config.P2P.TrustedHeight > 0 {
s.logger.Info().Uint64("trusted_height", s.config.P2P.TrustedHeight).Msg("initializing state from trusted height")

// Load and verify the trusted header from the P2P header store.
// The header is fetched via P2P and stored in the StoreAdapter's pending cache,
// so we must use headerStore.GetByHeight() which checks both the main store and pending cache.
p2pHeader, err := s.headerStore.GetByHeight(s.ctx, s.config.P2P.TrustedHeight)
if err != nil {
return fmt.Errorf("failed to load trusted header at height %d: %w", s.config.P2P.TrustedHeight, err)
}
trustedHeader := p2pHeader.SignedHeader

// Initialize new chain state from the trusted header
stateRoot, initErr := s.exec.InitChain(
s.ctx,
trustedHeader.Time(),
trustedHeader.Height(),
trustedHeader.ChainID(),
)
if initErr != nil {
return fmt.Errorf("failed to initialize execution client: %w", initErr)
}

state = types.State{
Version: types.InitStateVersion,
ChainID: trustedHeader.ChainID(),
InitialHeight: trustedHeader.Height(),
LastBlockHeight: trustedHeader.Height(),
LastBlockTime: trustedHeader.Time(),
LastHeaderHash: trustedHeader.Hash(), // Hash of the trusted header
DAHeight: s.genesis.DAStartHeight,
AppHash: stateRoot,
}
} else {
// Initialize new chain state for a fresh full node (no prior state on disk)
// Mirror executor initialization to ensure AppHash matches headers produced by the sequencer.
stateRoot, initErr := s.exec.InitChain(
s.ctx,
s.genesis.StartTime,
s.genesis.InitialHeight,
s.genesis.ChainID,
)
if initErr != nil {
return fmt.Errorf("failed to initialize execution client: %w", initErr)
}

state = types.State{
ChainID: s.genesis.ChainID,
InitialHeight: s.genesis.InitialHeight,
LastBlockHeight: s.genesis.InitialHeight - 1,
LastBlockTime: s.genesis.StartTime,
DAHeight: s.genesis.DAStartHeight,
AppHash: stateRoot,
state = types.State{
ChainID: s.genesis.ChainID,
InitialHeight: s.genesis.InitialHeight,
LastBlockHeight: s.genesis.InitialHeight - 1,
LastBlockTime: s.genesis.StartTime,
DAHeight: s.genesis.DAStartHeight,
AppHash: stateRoot,
}
}
}
if state.DAHeight != 0 && state.DAHeight < s.genesis.DAStartHeight {
Expand Down
47 changes: 35 additions & 12 deletions docs/learn/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -977,23 +977,46 @@ signer:
`--rollkit.signer.signer_path <string>`
_Example:_ `--rollkit.signer.signer_path ./config`
_Default:_ (Depends on application)
_Constant:_ `FlagSignerPath`

### Signer Passphrase
---

## Sync Configuration (`sync`)

The `sync` section contains options for controlling how the node synchronizes with the network.

### Trusted Height

**Description:**
The passphrase required to decrypt or access the signer key, particularly if using a `file` signer and the key is encrypted, or if the aggregator mode is enabled and requires it. This flag is not directly a field in the `SignerConfig` struct but is used in conjunction with it.
Trusted height allows a syncing node to start synchronization from a known, verified block height instead of from genesis. This can significantly speed up the initial sync process for new nodes. When using trusted height, you must also provide the corresponding header hash for security verification.

This is particularly useful when:

- Joining a long-running network and wanting to skip the history
- Restoring from a backup at a specific height
- Testing with a known good state

**Security Consideration:** When using trusted height, you must provide the `trusted_header_hash` to prevent against history rewrites or malicious nodes trying to sync from an invalid state.

**YAML:**
This is typically not stored in the YAML file for security reasons but provided via flag or environment variable.

**Command-line Flag:**
`--rollkit.signer.passphrase <string>`
_Example:_ `--rollkit.signer.passphrase "mysecretpassphrase"`
_Default:_ `""` (empty)
_Constant:_ `FlagSignerPassphrase`
_Note:_ Be cautious with providing passphrases directly on the command line in shared environments due to history logging. Environment variables or secure input methods are often preferred.
```yaml
sync:
trusted_height: 100000 # Block height to trust for sync initialization
trusted_header_hash: "a1b2c3d4e5f6..." # Hex-encoded hash of the header at trusted_height
```

---
**Command-line Flags:**

- `--evnode.sync.trusted_height <uint64>` - Block height to trust for sync initialization
- `--evnode.sync.trusted_header_hash <string>` - Hash of the trusted header for security verification (hex-encoded)

**Example:**

```bash
testapp start \
--evnode.sync.trusted_height 100000 \
--evnode.sync.trusted_header_hash "abc123def456..."
```
Comment on lines +1002 to +1019
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The documentation for the new trusted sync feature appears to be inconsistent with the implementation in pkg/config/config.go.

  1. YAML Configuration: The documentation places trusted_height and trusted_header_hash under a sync: section. However, the code defines these fields within P2PConfig, which is mapped to a p2p: section in YAML.
  2. Command-line Flags: The documentation lists the flags as --evnode.sync.trusted_height and --evnode.sync.trusted_header_hash. The implementation defines them as --evnode.p2p.trusted_height and --evnode.p2p.trusted_header_hash.

To avoid user confusion, the documentation should be updated to reflect the implementation. Here is a suggested correction:

**YAML:**

```yaml
p2p:
  trusted_height: 100000 # Block height to trust for sync initialization
  trusted_header_hash: "a1b2c3d4e5f6..." # Hex-encoded hash of the header at trusted_height

Command-line Flags:

  • --evnode.p2p.trusted_height <uint64> - Block height to trust for sync initialization
  • --evnode.p2p.trusted_header_hash <string> - Hash of the trusted header for security verification (hex-encoded)

Example:

testapp start \
  --evnode.p2p.trusted_height 100000 \
  --evnode.p2p.trusted_header_hash "abc123def456..."


This reference should help you configure your Evolve node effectively. Always refer to the specific version of Evolve you are using, as options and defaults may change over time.
_Default:_ `0` (disabled - sync from genesis)
_Constant:_ `FlagTrustedHeight`, `FlagTrustedHeaderHash`
27 changes: 23 additions & 4 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ const (
FlagP2PBlockedPeers = FlagPrefixEvnode + "p2p.blocked_peers"
// FlagP2PAllowedPeers is a flag for specifying the P2P allowed peers
FlagP2PAllowedPeers = FlagPrefixEvnode + "p2p.allowed_peers"
// FlagTrustedHeight is a flag for specifying the trusted block height to start sync from
FlagTrustedHeight = FlagPrefixEvnode + "p2p.trusted_height"
// FlagTrustedHeaderHash is a flag for specifying the trusted header hash for verification
FlagTrustedHeaderHash = FlagPrefixEvnode + "p2p.trusted_header_hash"
// FlagTrustedDataHash is a flag for specifying the trusted data hash for verification
FlagTrustedDataHash = FlagPrefixEvnode + "p2p.trusted_data_hash"

// Instrumentation configuration flags

Expand Down Expand Up @@ -272,10 +278,13 @@ type LogConfig struct {

// P2PConfig contains all peer-to-peer networking configuration parameters
type P2PConfig struct {
ListenAddress string `mapstructure:"listen_address" yaml:"listen_address" comment:"Address to listen for incoming connections (host:port)"`
Peers string `mapstructure:"peers" yaml:"peers" comment:"Comma-separated list of peers to connect to"`
BlockedPeers string `mapstructure:"blocked_peers" yaml:"blocked_peers" comment:"Comma-separated list of peer IDs to block from connecting"`
AllowedPeers string `mapstructure:"allowed_peers" yaml:"allowed_peers" comment:"Comma-separated list of peer IDs to allow connections from"`
ListenAddress string `mapstructure:"listen_address" yaml:"listen_address" comment:"Address to listen for incoming connections (host:port)"`
Peers string `mapstructure:"peers" yaml:"peers" comment:"Comma-separated list of peers to connect to"`
BlockedPeers string `mapstructure:"blocked_peers" yaml:"blocked_peers" comment:"Comma-separated list of peer IDs to block from connecting"`
AllowedPeers string `mapstructure:"allowed_peers" yaml:"allowed_peers" comment:"Comma-separated list of peer IDs to allow connections from"`
TrustedHeight uint64 `mapstructure:"trusted_height" yaml:"trusted_height" comment:"Block height to trust for sync initialization. When set, sync starts from this height instead of genesis. Must be accompanied by trusted_header_hash for security."`
TrustedHeaderHash string `mapstructure:"trusted_header_hash" yaml:"trusted_header_hash" comment:"Hash of the trusted header for security verification."`
TrustedDataHash string `mapstructure:"trusted_data_hash" yaml:"trusted_data_hash" comment:"Hash of the trusted data for security verification."`
}

// SignerConfig contains all signer configuration parameters
Expand Down Expand Up @@ -373,6 +382,13 @@ func (c *Config) Validate() error {
return fmt.Errorf("LazyBlockInterval (%v) must be greater than BlockTime (%v) in lazy mode",
c.Node.LazyBlockInterval.Duration, c.Node.BlockTime.Duration)
}

// Validate trusted height configuration
if c.P2P.TrustedHeight > 0 && (c.P2P.TrustedHeaderHash == "" || c.P2P.TrustedDataHash == "") {
return fmt.Errorf("trusted_height (%d) is set but trusted_header_hash or trusted_data_hash is empty. When using trusted_height, both trusted_header_hash and trusted_data_hash must also be provided for security verification",
c.P2P.TrustedHeight)
}

if err := c.Raft.Validate(); err != nil {
return err
}
Expand Down Expand Up @@ -459,6 +475,9 @@ func AddFlags(cmd *cobra.Command) {
cmd.Flags().String(FlagP2PPeers, def.P2P.Peers, "Comma separated list of seed nodes to connect to")
cmd.Flags().String(FlagP2PBlockedPeers, def.P2P.BlockedPeers, "Comma separated list of nodes to ignore")
cmd.Flags().String(FlagP2PAllowedPeers, def.P2P.AllowedPeers, "Comma separated list of nodes to whitelist")
cmd.Flags().Uint64(FlagTrustedHeight, def.P2P.TrustedHeight, "block height to trust for sync initialization (0 = start from genesis)")
cmd.Flags().String(FlagTrustedHeaderHash, def.P2P.TrustedHeaderHash, "hash of the trusted header for security verification")
cmd.Flags().String(FlagTrustedDataHash, def.P2P.TrustedDataHash, "hash of the trusted data for security verification")

// RPC configuration flags
cmd.Flags().String(FlagRPCAddress, def.RPC.Address, "RPC server address (host:port)")
Expand Down
57 changes: 56 additions & 1 deletion pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestAddFlags(t *testing.T) {
assertFlagValue(t, flags, FlagRPCEnableDAVisualization, DefaultConfig().RPC.EnableDAVisualization)

// Count the number of flags we're explicitly checking
expectedFlagCount := 63 // Update this number if you add more flag checks above
expectedFlagCount := 66 // Update this number if you add more flag checks above

// Get the actual number of flags (both regular and persistent)
actualFlagCount := 0
Expand Down Expand Up @@ -513,3 +513,58 @@ func TestBasedSequencerValidation(t *testing.T) {
})
}
}

func TestTrustedHeightValidation(t *testing.T) {
tests := []struct {
name string
trustedHeight uint64
trustedHash string
expectError bool
errorMsg string
}{
{
name: "trusted height with empty hash should fail",
trustedHeight: 100,
trustedHash: "",
expectError: true,
errorMsg: "trusted_height (100) is set but trusted_header_hash or trusted_data_hash is empty",
},
{
name: "trusted height with valid hash should pass",
trustedHeight: 100,
trustedHash: "abc123",
expectError: false,
},
{
name: "zero trusted height with empty hash should pass",
trustedHeight: 0,
trustedHash: "",
expectError: false,
},
{
name: "zero trusted height with hash should pass (not validated)",
trustedHeight: 0,
trustedHash: "abc123",
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := DefaultConfig()
cfg.RootDir = t.TempDir()
cfg.P2P.TrustedHeight = tt.trustedHeight
cfg.P2P.TrustedHeaderHash = tt.trustedHash
cfg.P2P.TrustedDataHash = tt.trustedHash

err := cfg.Validate()

if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
require.NoError(t, err)
}
})
}
}
6 changes: 4 additions & 2 deletions pkg/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ func DefaultConfig() Config {
RootDir: DefaultRootDir,
DBPath: "data",
P2P: P2PConfig{
ListenAddress: "/ip4/0.0.0.0/tcp/7676",
Peers: "",
ListenAddress: "/ip4/0.0.0.0/tcp/7676",
Peers: "",
TrustedHeight: 0,
TrustedHeaderHash: "",
},
Node: NodeConfig{
Aggregator: false,
Expand Down
58 changes: 56 additions & 2 deletions pkg/sync/sync_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ type SyncService[H store.EntityWithDAHint[H]] struct {
topicSubscription header.Subscription[H]

storeInitialized atomic.Bool

// trustedHeight tracks the configured trusted height for sync initialization
trustedHeight uint64
// trustedHeaderHash, trustedDataHash is the expected hash of the trusted header
trustedHeaderHash, trustedDataHash string
}

// NewDataSyncService returns a new DataSyncService.
Expand Down Expand Up @@ -198,6 +203,11 @@ func (syncService *SyncService[H]) Start(ctx context.Context) error {
return fmt.Errorf("failed to create syncer: %w", err)
}

// Initialize trusted height configuration
syncService.trustedHeight = syncService.conf.P2P.TrustedHeight
syncService.trustedHeaderHash = syncService.conf.P2P.TrustedHeaderHash
syncService.trustedDataHash = syncService.conf.P2P.TrustedDataHash

// initialize stores from P2P (blocking until genesis is fetched for followers)
// Aggregators (no peers configured) return immediately and initialize on first produced block.
if err := syncService.initFromP2PWithRetry(ctx, peerIDs); err != nil {
Expand Down Expand Up @@ -322,7 +332,7 @@ func (syncService *SyncService[H]) startSubscriber(ctx context.Context) error {
return nil
}

// Height returns the current height stored
// Height returns the current height storeda
func (s *SyncService[H]) Height() uint64 {
return s.store.Height()
}
Expand All @@ -331,11 +341,19 @@ func (s *SyncService[H]) Height() uint64 {
// 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.
// - if trusted height is configured, it fetches from that height first and verifies the hash.
func (syncService *SyncService[H]) initFromP2PWithRetry(ctx context.Context, peerIDs []peer.ID) error {
if len(peerIDs) == 0 {
return nil
}

// If trusted height is configured, fetch from that height first
if syncService.trustedHeight > 0 {
if err := syncService.fetchAndVerifyTrustedHeader(ctx, peerIDs); err != nil {
return fmt.Errorf("failed to fetch trusted header at height %d: %w", syncService.trustedHeight, err)
}
}

tryInit := func(ctx context.Context) (bool, error) {
var (
trusted H
Expand All @@ -346,7 +364,12 @@ func (syncService *SyncService[H]) initFromP2PWithRetry(ctx context.Context, pee
head, headErr := syncService.store.Head(ctx)
switch {
case errors.Is(headErr, header.ErrNotFound), errors.Is(headErr, header.ErrEmptyStore):
heightToQuery = syncService.genesis.InitialHeight
// If we have a trusted header, use its height as the starting point
if syncService.trustedHeight > 0 {
heightToQuery = syncService.trustedHeight
} else {
heightToQuery = syncService.genesis.InitialHeight
}
case headErr != nil:
return false, fmt.Errorf("failed to inspect local store head: %w", headErr)
default:
Expand Down Expand Up @@ -405,6 +428,37 @@ func (syncService *SyncService[H]) initFromP2PWithRetry(ctx context.Context, pee
}
}

// fetchAndVerifyTrustedHeader fetches the header at the trusted height from P2P
// and verifies it matches the trusted hash. If verification passes, it stores the header.
func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Context, peerIDs []peer.ID) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The peerIDs parameter is unused within this function and should be removed to simplify the signature. The call site at line 351 should be updated accordingly by removing the peerIDs argument.

Suggested change
func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Context, peerIDs []peer.ID) error {
func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Context) error {

syncService.logger.Info().Uint64("height", syncService.trustedHeight).Msg("fetching trusted header from P2P")

// Fetch the header from trusted height
trusted, err := syncService.ex.GetByHeight(ctx, syncService.trustedHeight)
if err != nil {
return fmt.Errorf("failed to fetch trusted header at height %d: %w", syncService.trustedHeight, err)
}

// Verify the hash matches
actualHash := trusted.Hash().String()
if actualHash != syncService.trustedHeaderHash && actualHash != syncService.trustedDataHash {
return fmt.Errorf("trusted header hash mismatch at height %d: expected %s or %s, got %s",
syncService.trustedHeight, syncService.trustedHeaderHash, syncService.trustedDataHash, actualHash)
}

syncService.logger.Info().Uint64("height", syncService.trustedHeight).
Str("hash", actualHash).
Msg("trusted header verified and stored")

if err := syncService.store.Append(ctx, trusted); err != nil {
return fmt.Errorf("failed to store trusted header: %w", err)
}

syncService.storeInitialized.Store(true)

return nil
}

// Stop is a part of Service interface.
//
// `store` is closed last because it's used by other services.
Expand Down
Loading