From d6c16e6f52f9d2b08dcb5b7ed0737934564c1b83 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 4 Feb 2026 15:52:32 +0100 Subject: [PATCH 1/5] feat(sync): sync from trusted height --- block/internal/syncing/syncer.go | 97 ++++++++++++++++++++++++++------ docs/learn/config.md | 47 ++++++++++++---- pkg/config/config.go | 23 ++++++-- pkg/config/config_test.go | 56 +++++++++++++++++- pkg/config/defaults.go | 6 +- pkg/sync/sync_service.go | 56 +++++++++++++++++- 6 files changed, 247 insertions(+), 38 deletions(-) diff --git a/block/internal/syncing/syncer.go b/block/internal/syncing/syncer.go index 6df6b2c22..e0c39e769 100644 --- a/block/internal/syncing/syncer.go +++ b/block/internal/syncing/syncer.go @@ -30,6 +30,33 @@ import ( var _ BlockSyncer = (*Syncer)(nil) +// getTrustedHeader loads and verifies the trusted header from the store +func (s *Syncer) getTrustedHeader(ctx context.Context) (*types.SignedHeader, error) { + if s.config.P2P.TrustedHeight == 0 { + return nil, fmt.Errorf("trusted_height is not configured") + } + + // Load the signed header from the store + header, err := s.store.GetHeader(ctx, s.config.P2P.TrustedHeight) + if err != nil { + return nil, fmt.Errorf("failed to load trusted header at height %d: %w", s.config.P2P.TrustedHeight, err) + } + + // Verify the header hash matches the trusted hash + expectedHash := s.config.P2P.TrustedHeaderHash + actualHash := header.Hash().String() + if actualHash != expectedHash { + return nil, fmt.Errorf("trusted header hash mismatch at height %d: expected %s, got %s", + s.config.P2P.TrustedHeight, expectedHash, actualHash) + } + + s.logger.Info().Uint64("height", s.config.P2P.TrustedHeight). + Str("hash", actualHash). + Msg("trusted header loaded and verified") + + return header, nil +} + // forcedInclusionGracePeriodConfig contains internal configuration for forced inclusion grace periods. type forcedInclusionGracePeriodConfig struct { // basePeriod is the base number of additional epochs allowed for including forced inclusion transactions @@ -304,25 +331,59 @@ 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 + trustedHeader, err := s.getTrustedHeader(s.ctx) + if err != nil { + return fmt.Errorf("failed to load trusted header: %w", err) + } - 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, + // 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, + } } } if state.DAHeight != 0 && state.DAHeight < s.genesis.DAStartHeight { diff --git a/docs/learn/config.md b/docs/learn/config.md index ba900a163..87caa828d 100644 --- a/docs/learn/config.md +++ b/docs/learn/config.md @@ -977,23 +977,46 @@ signer: `--rollkit.signer.signer_path ` _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 ` -_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 ` - Block height to trust for sync initialization +- `--evnode.sync.trusted_header_hash ` - 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..." +``` -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` diff --git a/pkg/config/config.go b/pkg/config/config.go index e03a277ce..de16e4b1b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -97,6 +97,10 @@ 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" // Instrumentation configuration flags @@ -272,10 +276,12 @@ 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. This should be the hex-encoded hash of the header at trusted_height. Prevents against history rewrites during sync."` } // SignerConfig contains all signer configuration parameters @@ -373,6 +379,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 == "" { + return fmt.Errorf("trusted_height (%d) is set but trusted_header_hash is empty. When using trusted_height, trusted_header_hash must also be provided for security verification", + c.P2P.TrustedHeight) + } + if err := c.Raft.Validate(); err != nil { return err } @@ -459,6 +472,8 @@ 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 (hex-encoded)") // RPC configuration flags cmd.Flags().String(FlagRPCAddress, def.RPC.Address, "RPC server address (host:port)") diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1834e1b40..82cce4007 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -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 := 65 // Update this number if you add more flag checks above // Get the actual number of flags (both regular and persistent) actualFlagCount := 0 @@ -513,3 +513,57 @@ 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 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 + + err := cfg.Validate() + + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 0de2f4bc2..fd9a6ed6f 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -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, diff --git a/pkg/sync/sync_service.go b/pkg/sync/sync_service.go index 8567e7976..783cd540a 100644 --- a/pkg/sync/sync_service.go +++ b/pkg/sync/sync_service.go @@ -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 is the expected hash of the trusted header + trustedHeaderHash string } // NewDataSyncService returns a new DataSyncService. @@ -198,6 +203,10 @@ 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 + // 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 { @@ -331,11 +340,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 @@ -346,7 +363,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: @@ -405,6 +427,38 @@ 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 { + 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 + expectedHash := syncService.trustedHeaderHash + actualHash := trusted.Hash().String() + if actualHash != expectedHash { + return fmt.Errorf("trusted header hash mismatch at height %d: expected %s, got %s", + syncService.trustedHeight, expectedHash, 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. From 5ef2633832c2efca877c80139a14e88f094e231b Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 4 Feb 2026 17:28:23 +0100 Subject: [PATCH 2/5] add data hash --- block/internal/syncing/syncer.go | 31 ++----------------------------- pkg/config/config.go | 7 ++++--- pkg/config/config_test.go | 3 ++- pkg/sync/sync_service.go | 14 +++++++------- 4 files changed, 15 insertions(+), 40 deletions(-) diff --git a/block/internal/syncing/syncer.go b/block/internal/syncing/syncer.go index e0c39e769..ee162482f 100644 --- a/block/internal/syncing/syncer.go +++ b/block/internal/syncing/syncer.go @@ -30,33 +30,6 @@ import ( var _ BlockSyncer = (*Syncer)(nil) -// getTrustedHeader loads and verifies the trusted header from the store -func (s *Syncer) getTrustedHeader(ctx context.Context) (*types.SignedHeader, error) { - if s.config.P2P.TrustedHeight == 0 { - return nil, fmt.Errorf("trusted_height is not configured") - } - - // Load the signed header from the store - header, err := s.store.GetHeader(ctx, s.config.P2P.TrustedHeight) - if err != nil { - return nil, fmt.Errorf("failed to load trusted header at height %d: %w", s.config.P2P.TrustedHeight, err) - } - - // Verify the header hash matches the trusted hash - expectedHash := s.config.P2P.TrustedHeaderHash - actualHash := header.Hash().String() - if actualHash != expectedHash { - return nil, fmt.Errorf("trusted header hash mismatch at height %d: expected %s, got %s", - s.config.P2P.TrustedHeight, expectedHash, actualHash) - } - - s.logger.Info().Uint64("height", s.config.P2P.TrustedHeight). - Str("hash", actualHash). - Msg("trusted header loaded and verified") - - return header, nil -} - // forcedInclusionGracePeriodConfig contains internal configuration for forced inclusion grace periods. type forcedInclusionGracePeriodConfig struct { // basePeriod is the base number of additional epochs allowed for including forced inclusion transactions @@ -337,9 +310,9 @@ func (s *Syncer) initializeState() error { s.logger.Info().Uint64("trusted_height", s.config.P2P.TrustedHeight).Msg("initializing state from trusted height") // Load and verify the trusted header - trustedHeader, err := s.getTrustedHeader(s.ctx) + trustedHeader, err := s.store.GetHeader(s.ctx, s.config.P2P.TrustedHeight) if err != nil { - return fmt.Errorf("failed to load trusted header: %w", err) + return fmt.Errorf("failed to load trusted header at height %d: %w", s.config.P2P.TrustedHeight, err) } // Initialize new chain state from the trusted header diff --git a/pkg/config/config.go b/pkg/config/config.go index de16e4b1b..860ba713a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -281,7 +281,8 @@ type P2PConfig struct { 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. This should be the hex-encoded hash of the header at trusted_height. Prevents against history rewrites during sync."` + 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 @@ -381,8 +382,8 @@ func (c *Config) Validate() error { } // Validate trusted height configuration - if c.P2P.TrustedHeight > 0 && c.P2P.TrustedHeaderHash == "" { - return fmt.Errorf("trusted_height (%d) is set but trusted_header_hash is empty. When using trusted_height, trusted_header_hash must also be provided for security verification", + 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) } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 82cce4007..06b3a9a93 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -527,7 +527,7 @@ func TestTrustedHeightValidation(t *testing.T) { trustedHeight: 100, trustedHash: "", expectError: true, - errorMsg: "trusted_height (100) is set but trusted_header_hash is empty", + errorMsg: "trusted_height (100) is set but trusted_header_hash or trusted_data_hash is empty", }, { name: "trusted height with valid hash should pass", @@ -555,6 +555,7 @@ func TestTrustedHeightValidation(t *testing.T) { cfg.RootDir = t.TempDir() cfg.P2P.TrustedHeight = tt.trustedHeight cfg.P2P.TrustedHeaderHash = tt.trustedHash + cfg.P2P.TrustedDataHash = tt.trustedHash err := cfg.Validate() diff --git a/pkg/sync/sync_service.go b/pkg/sync/sync_service.go index 783cd540a..629b71c6d 100644 --- a/pkg/sync/sync_service.go +++ b/pkg/sync/sync_service.go @@ -62,8 +62,8 @@ type SyncService[H store.EntityWithDAHint[H]] struct { // trustedHeight tracks the configured trusted height for sync initialization trustedHeight uint64 - // trustedHeaderHash is the expected hash of the trusted header - trustedHeaderHash string + // trustedHeaderHash, trustedDataHash is the expected hash of the trusted header + trustedHeaderHash, trustedDataHash string } // NewDataSyncService returns a new DataSyncService. @@ -206,6 +206,7 @@ func (syncService *SyncService[H]) Start(ctx context.Context) error { // 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. @@ -331,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() } @@ -439,11 +440,10 @@ func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Conte } // Verify the hash matches - expectedHash := syncService.trustedHeaderHash actualHash := trusted.Hash().String() - if actualHash != expectedHash { - return fmt.Errorf("trusted header hash mismatch at height %d: expected %s, got %s", - syncService.trustedHeight, expectedHash, actualHash) + 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). From 3833be55671300cf2bfec33bd2b4856f573aac3e Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 4 Feb 2026 17:49:12 +0100 Subject: [PATCH 3/5] add flag --- pkg/config/config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 860ba713a..3ebddb8a0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -101,6 +101,8 @@ const ( 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 @@ -474,7 +476,8 @@ func AddFlags(cmd *cobra.Command) { 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 (hex-encoded)") + 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)") From 907681b43eda8854f3b640daffc7e4cb1d8eaa27 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 4 Feb 2026 18:10:07 +0100 Subject: [PATCH 4/5] fix --- pkg/config/config_test.go | 2 +- pkg/sync/sync_service.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 06b3a9a93..53e9f8e49 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -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 := 65 // 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 diff --git a/pkg/sync/sync_service.go b/pkg/sync/sync_service.go index 629b71c6d..1f723c45a 100644 --- a/pkg/sync/sync_service.go +++ b/pkg/sync/sync_service.go @@ -441,7 +441,7 @@ func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Conte // Verify the hash matches actualHash := trusted.Hash().String() - if actualHash != syncService.trustedHeaderHash || actualHash != syncService.trustedDataHash { + 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) } From 8a0e65dd86ad23f9b03792619990aeee79a3634f Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Thu, 5 Feb 2026 07:53:32 +0100 Subject: [PATCH 5/5] updates --- block/internal/syncing/syncer.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/block/internal/syncing/syncer.go b/block/internal/syncing/syncer.go index ee162482f..4abcd6887 100644 --- a/block/internal/syncing/syncer.go +++ b/block/internal/syncing/syncer.go @@ -309,11 +309,14 @@ func (s *Syncer) initializeState() error { 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 - trustedHeader, err := s.store.GetHeader(s.ctx, s.config.P2P.TrustedHeight) + // 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(