-
Notifications
You must be signed in to change notification settings - Fork 260
feat(sync): sync from trusted height #3050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The documentation for the new trusted sync feature appears to be inconsistent with the implementation in
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_heightCommand-line Flags:
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` | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||
|
|
@@ -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 { | ||||||
|
|
@@ -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() | ||||||
| } | ||||||
|
|
@@ -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 | ||||||
|
|
@@ -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: | ||||||
|
|
@@ -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 { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| 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. | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new logic for initializing from a trusted height looks mostly correct, but there's a potential critical issue with a subsequent call outside this block. After this block,
execReplayer.SyncToHeightis called at line 421. When initializing from a trusted height, this can lead to a failure.The replayer is initialized with the original genesis information and will likely try to replay blocks from
genesis.InitialHeight. However, for a trusted sync, the store will not contain these intermediate blocks, causingGetBlockDatato fail.Since
s.exec.InitChainis already called to set the executor's state to the trusted height, the subsequent replayer call seems unnecessary and problematic for this case. Consider restructuring the logic to skip theexecReplayer.SyncToHeightcall when the state is initialized from a trusted height.