-
Notifications
You must be signed in to change notification settings - Fork 245
fix: add a way to verify current apphashes #2921
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 1 commit
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 |
|---|---|---|
|
|
@@ -110,6 +110,30 @@ func retryWithBackoffOnPayloadStatus(ctx context.Context, fn func() error, maxRe | |
| return fmt.Errorf("max retries (%d) exceeded for %s", maxRetries, operation) | ||
| } | ||
|
|
||
| // appendUniqueHash ensures we only keep unique, non-zero hash candidates while | ||
| // preserving order so we can try canonical first before falling back to legacy. | ||
| func appendUniqueHash(candidates []common.Hash, candidate common.Hash) []common.Hash { | ||
| if candidate == (common.Hash{}) { | ||
| return candidates | ||
| } | ||
| for _, existing := range candidates { | ||
| if existing == candidate { | ||
| return candidates | ||
| } | ||
| } | ||
| return append(candidates, candidate) | ||
| } | ||
|
|
||
| // buildHashCandidates returns a deduplicated list of hash candidates in the | ||
| // order they should be tried. | ||
| func buildHashCandidates(hashes ...common.Hash) []common.Hash { | ||
| candidates := make([]common.Hash, 0, len(hashes)) | ||
| for _, h := range hashes { | ||
| candidates = appendUniqueHash(candidates, h) | ||
| } | ||
| return candidates | ||
| } | ||
|
|
||
| // EngineClient represents a client that interacts with an Ethereum execution engine | ||
| // through the Engine API. It manages connections to both the engine and standard Ethereum | ||
| // APIs, and maintains state related to block processing. | ||
|
|
@@ -185,42 +209,63 @@ func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, ini | |
| return nil, 0, fmt.Errorf("initialHeight must be 1, got %d", initialHeight) | ||
| } | ||
|
|
||
| // Acknowledge the genesis block with retry logic for SYNCING status | ||
| err := retryWithBackoffOnPayloadStatus(ctx, func() error { | ||
| var forkchoiceResult engine.ForkChoiceResponse | ||
| err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV3", | ||
| engine.ForkchoiceStateV1{ | ||
| HeadBlockHash: c.genesisHash, | ||
| SafeBlockHash: c.genesisHash, | ||
| FinalizedBlockHash: c.genesisHash, | ||
| }, | ||
| nil, | ||
| ) | ||
| if err != nil { | ||
| return fmt.Errorf("engine_forkchoiceUpdatedV3 failed: %w", err) | ||
| genesisBlockHash, stateRoot, gasLimit, _, err := c.getBlockInfo(ctx, 0) | ||
| if err != nil { | ||
| return nil, 0, fmt.Errorf("failed to get block info: %w", err) | ||
| } | ||
|
|
||
| candidates := buildHashCandidates(c.genesisHash, genesisBlockHash, stateRoot) | ||
| var selectedGenesisHash common.Hash | ||
|
|
||
| for idx, candidate := range candidates { | ||
| args := engine.ForkchoiceStateV1{ | ||
| HeadBlockHash: candidate, | ||
| SafeBlockHash: candidate, | ||
| FinalizedBlockHash: candidate, | ||
| } | ||
|
|
||
| // Validate payload status | ||
| if err := validatePayloadStatus(forkchoiceResult.PayloadStatus); err != nil { | ||
| err = retryWithBackoffOnPayloadStatus(ctx, func() error { | ||
| var forkchoiceResult engine.ForkChoiceResponse | ||
| err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV3", args, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("engine_forkchoiceUpdatedV3 failed: %w", err) | ||
| } | ||
|
|
||
| if err := validatePayloadStatus(forkchoiceResult.PayloadStatus); err != nil { | ||
| c.logger.Warn(). | ||
| Str("status", forkchoiceResult.PayloadStatus.Status). | ||
| Str("latestValidHash", forkchoiceResult.PayloadStatus.LatestValidHash.Hex()). | ||
| Interface("validationError", forkchoiceResult.PayloadStatus.ValidationError). | ||
| Msg("InitChain: engine_forkchoiceUpdatedV3 returned non-VALID status") | ||
| return err | ||
| } | ||
|
|
||
| return nil | ||
| }, MaxPayloadStatusRetries, InitialRetryBackoff, "InitChain") | ||
|
|
||
| if err == nil { | ||
| selectedGenesisHash = candidate | ||
| break | ||
| } | ||
|
|
||
| if errors.Is(err, ErrInvalidPayloadStatus) && idx+1 < len(candidates) { | ||
| c.logger.Warn(). | ||
| Str("status", forkchoiceResult.PayloadStatus.Status). | ||
| Str("latestValidHash", forkchoiceResult.PayloadStatus.LatestValidHash.Hex()). | ||
| Interface("validationError", forkchoiceResult.PayloadStatus.ValidationError). | ||
| Msg("InitChain: engine_forkchoiceUpdatedV3 returned non-VALID status") | ||
| return err | ||
| Str("blockHash", candidate.Hex()). | ||
| Msg("InitChain: execution engine rejected hash candidate, trying alternate") | ||
| continue | ||
| } | ||
|
|
||
| return nil | ||
| }, MaxPayloadStatusRetries, InitialRetryBackoff, "InitChain") | ||
| if err != nil { | ||
| return nil, 0, err | ||
| } | ||
|
Comment on lines
+220
to
259
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 logic for iterating through hash candidates and retrying |
||
|
|
||
| _, stateRoot, gasLimit, _, err := c.getBlockInfo(ctx, 0) | ||
| if err != nil { | ||
| return nil, 0, fmt.Errorf("failed to get block info: %w", err) | ||
| if selectedGenesisHash == (common.Hash{}) { | ||
| return nil, 0, fmt.Errorf("execution engine rejected all genesis hash candidates") | ||
| } | ||
|
|
||
| c.genesisHash = selectedGenesisHash | ||
| c.currentHeadBlockHash = selectedGenesisHash | ||
| c.currentSafeBlockHash = selectedGenesisHash | ||
| c.currentFinalizedBlockHash = selectedGenesisHash | ||
| c.initialHeight = initialHeight | ||
|
|
||
| return stateRoot[:], gasLimit, nil | ||
|
|
@@ -292,16 +337,12 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight | |
| } | ||
| txsPayload := validTxs | ||
|
|
||
| prevBlockHash, _, prevGasLimit, _, err := c.getBlockInfo(ctx, blockHeight-1) | ||
| prevBlockHash, prevBlockStateRoot, prevGasLimit, _, err := c.getBlockInfo(ctx, blockHeight-1) | ||
| if err != nil { | ||
| return nil, 0, fmt.Errorf("failed to get block info: %w", err) | ||
| } | ||
|
|
||
| args := engine.ForkchoiceStateV1{ | ||
| HeadBlockHash: prevBlockHash, | ||
| SafeBlockHash: prevBlockHash, | ||
| FinalizedBlockHash: prevBlockHash, | ||
| } | ||
| parentHashCandidates := buildHashCandidates(prevBlockHash, prevBlockStateRoot) | ||
|
|
||
| // update forkchoice to get the next payload id | ||
| // Create evolve-compatible payloadtimestamp.Unix() | ||
|
|
@@ -325,46 +366,69 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight | |
|
|
||
| // Call forkchoice update with retry logic for SYNCING status | ||
| var payloadID *engine.PayloadID | ||
| err = retryWithBackoffOnPayloadStatus(ctx, func() error { | ||
| var forkchoiceResult engine.ForkChoiceResponse | ||
| err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV3", args, evPayloadAttrs) | ||
| if err != nil { | ||
| return fmt.Errorf("forkchoice update failed: %w", err) | ||
| for idx, candidate := range parentHashCandidates { | ||
| args := engine.ForkchoiceStateV1{ | ||
| HeadBlockHash: candidate, | ||
| SafeBlockHash: candidate, | ||
| FinalizedBlockHash: candidate, | ||
| } | ||
|
|
||
| // Validate payload status | ||
| if err := validatePayloadStatus(forkchoiceResult.PayloadStatus); err != nil { | ||
| c.logger.Warn(). | ||
| Str("status", forkchoiceResult.PayloadStatus.Status). | ||
| Str("latestValidHash", forkchoiceResult.PayloadStatus.LatestValidHash.Hex()). | ||
| Interface("validationError", forkchoiceResult.PayloadStatus.ValidationError). | ||
| Uint64("blockHeight", blockHeight). | ||
| Msg("ExecuteTxs: engine_forkchoiceUpdatedV3 returned non-VALID status") | ||
| return err | ||
| } | ||
| err = retryWithBackoffOnPayloadStatus(ctx, func() error { | ||
| var forkchoiceResult engine.ForkChoiceResponse | ||
| err := c.engineClient.CallContext(ctx, &forkchoiceResult, "engine_forkchoiceUpdatedV3", args, evPayloadAttrs) | ||
| if err != nil { | ||
| return fmt.Errorf("forkchoice update failed: %w", err) | ||
| } | ||
|
|
||
| if forkchoiceResult.PayloadID == nil { | ||
| c.logger.Error(). | ||
| Str("status", forkchoiceResult.PayloadStatus.Status). | ||
| Str("latestValidHash", forkchoiceResult.PayloadStatus.LatestValidHash.Hex()). | ||
| Interface("validationError", forkchoiceResult.PayloadStatus.ValidationError). | ||
| Interface("forkchoiceState", args). | ||
| Interface("payloadAttributes", evPayloadAttrs). | ||
| Uint64("blockHeight", blockHeight). | ||
| Msg("returned nil PayloadID") | ||
| // Validate payload status | ||
| if err := validatePayloadStatus(forkchoiceResult.PayloadStatus); err != nil { | ||
| c.logger.Warn(). | ||
| Str("status", forkchoiceResult.PayloadStatus.Status). | ||
| Str("latestValidHash", forkchoiceResult.PayloadStatus.LatestValidHash.Hex()). | ||
| Interface("validationError", forkchoiceResult.PayloadStatus.ValidationError). | ||
| Uint64("blockHeight", blockHeight). | ||
| Msg("ExecuteTxs: engine_forkchoiceUpdatedV3 returned non-VALID status") | ||
| return err | ||
| } | ||
|
|
||
| if forkchoiceResult.PayloadID == nil { | ||
| c.logger.Error(). | ||
| Str("status", forkchoiceResult.PayloadStatus.Status). | ||
| Str("latestValidHash", forkchoiceResult.PayloadStatus.LatestValidHash.Hex()). | ||
| Interface("validationError", forkchoiceResult.PayloadStatus.ValidationError). | ||
| Interface("forkchoiceState", args). | ||
| Interface("payloadAttributes", evPayloadAttrs). | ||
| Uint64("blockHeight", blockHeight). | ||
| Msg("returned nil PayloadID") | ||
|
|
||
| return fmt.Errorf("returned nil PayloadID - (status: %s, latestValidHash: %s)", | ||
| forkchoiceResult.PayloadStatus.Status, | ||
| forkchoiceResult.PayloadStatus.LatestValidHash.Hex()) | ||
| } | ||
|
|
||
| payloadID = forkchoiceResult.PayloadID | ||
| return nil | ||
| }, MaxPayloadStatusRetries, InitialRetryBackoff, "ExecuteTxs forkchoice") | ||
|
|
||
| return fmt.Errorf("returned nil PayloadID - (status: %s, latestValidHash: %s)", | ||
| forkchoiceResult.PayloadStatus.Status, | ||
| forkchoiceResult.PayloadStatus.LatestValidHash.Hex()) | ||
| if err == nil { | ||
| break | ||
| } | ||
|
|
||
| if errors.Is(err, ErrInvalidPayloadStatus) && idx+1 < len(parentHashCandidates) { | ||
| c.logger.Warn(). | ||
| Str("blockHash", candidate.Hex()). | ||
| Uint64("blockHeight", blockHeight-1). | ||
| Msg("ExecuteTxs: execution engine rejected parent hash candidate, trying alternate") | ||
| continue | ||
| } | ||
|
|
||
| payloadID = forkchoiceResult.PayloadID | ||
| return nil | ||
| }, MaxPayloadStatusRetries, InitialRetryBackoff, "ExecuteTxs forkchoice") | ||
| if err != nil { | ||
| return nil, 0, err | ||
| } | ||
|
|
||
| if payloadID == nil { | ||
| return nil, 0, fmt.Errorf("engine returned nil PayloadID after trying %d parent hash candidates", len(parentHashCandidates)) | ||
| } | ||
|
|
||
| // get payload | ||
| var payloadResult engine.ExecutionPayloadEnvelope | ||
| err = c.engineClient.CallContext(ctx, &payloadResult, "engine_getPayloadV4", *payloadID) | ||
|
|
||
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.
The logic in this
switchstatement can be simplified by combining the cases that result in the same action. The cases for an emptyAppHashand anAppHashmatching thecurrentStateboth update the header. Grouping them improves readability and reduces redundancy.