Repository navigation
chore: Better check for stored meta not stale #3221
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
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -756,24 +756,33 @@ func (c *EngineClient) reconcileExecutionAtHeight(ctx context.Context, height ui | |||||||||||||||||||||||||||||||||||||||||||||||||||
| // If we have a started execution with a payloadID, validate it still exists before resuming. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // After node restart, the EL's payload cache is ephemeral and the payloadID may be stale. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if execMeta.Stage == ExecStageStarted && len(execMeta.PayloadID) == 8 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var pid engine.PayloadID | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| copy(pid[:], execMeta.PayloadID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| requestedTxHash := hashTxs(txs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if execMeta.Timestamp != timestamp.Unix() || !bytes.Equal(execMeta.TxHash, requestedTxHash) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.logger.Debug(). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uint64("height", height). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Int64("execmeta_timestamp", execMeta.Timestamp). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Int64("requested_timestamp", timestamp.Unix()). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Msg("ExecuteTxs: ignoring stale in-progress execution for different block inputs") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var pid engine.PayloadID | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| copy(pid[:], execMeta.PayloadID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate payload still exists by attempting to retrieve it | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _, err = c.engineClient.GetPayload(ctx, pid); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.logger.Info(). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate payload still exists by attempting to retrieve it | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _, err = c.engineClient.GetPayload(ctx, pid); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.logger.Info(). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uint64("height", height). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Str("stage", execMeta.Stage). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Msg("ExecuteTxs: found in-progress execution with payloadID, returning payloadID for resume") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, &pid, true, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Payload is stale (expired or node restarted) - proceed with fresh execution | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.logger.Debug(). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uint64("height", height). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Str("stage", execMeta.Stage). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Msg("ExecuteTxs: found in-progress execution with payloadID, returning payloadID for resume") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, &pid, true, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Str("payloadID", pid.String()). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(err). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Msg("ExecuteTxs: stale ExecMeta payloadID no longer valid in EL, will re-execute") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Don't return - fall through to fresh execution | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Payload is stale (expired or node restarted) - proceed with fresh execution | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.logger.Debug(). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uint64("height", height). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Str("payloadID", pid.String()). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(err). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Msg("ExecuteTxs: stale ExecMeta payloadID no longer valid in EL, will re-execute") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Don't return - fall through to fresh execution | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1023,13 +1032,7 @@ func (c *EngineClient) saveExecMeta(ctx context.Context, height uint64, timestam | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Compute tx hash for sanity checks on retry | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(txs) > 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| h := sha256.New() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, tx := range txs { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| h.Write(tx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| execMeta.TxHash = h.Sum(nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| execMeta.TxHash = hashTxs(txs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := c.store.SaveExecMeta(ctx, execMeta); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.logger.Warn().Err(err).Uint64("height", height).Msg("saveExecMeta: failed to save exec meta") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1042,6 +1045,19 @@ func (c *EngineClient) saveExecMeta(ctx context.Context, height uint64, timestam | |||||||||||||||||||||||||||||||||||||||||||||||||||
| Msg("saveExecMeta: saved execution metadata") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| func hashTxs(txs [][]byte) []byte { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(txs) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| h := sha256.New() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, tx := range txs { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| h.Write(tx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return h.Sum(nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1048
to
+1059
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. Make the tx-list hash unambiguous.
Possible fix func hashTxs(txs [][]byte) []byte {
if len(txs) == 0 {
return nil
}
h := sha256.New()
for _, tx := range txs {
- h.Write(tx)
+ txHash := sha256.Sum256(tx)
+ h.Write(txHash[:])
}
return h.Sum(nil)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetLatestHeight returns the current block height of the execution layer | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *EngineClient) GetLatestHeight(ctx context.Context) (uint64, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| header, err := c.ethClient.HeaderByNumber(ctx, nil) // nil = latest block | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package evm | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "math/big" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/beacon/engine" | ||
| "github.com/ethereum/go-ethereum/core/types" | ||
| ds "github.com/ipfs/go-datastore" | ||
| dssync "github.com/ipfs/go-datastore/sync" | ||
| "github.com/rs/zerolog" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestReconcileExecutionAtHeight_StartedExecMeta(t *testing.T) { | ||
| specs := map[string]struct { | ||
| execMetaTimestamp int64 | ||
| execMetaTxs [][]byte | ||
| requestedTxs [][]byte | ||
| requestedTime time.Time | ||
| expectFound bool | ||
| expectPayloadID bool | ||
| expectGetPayloads int | ||
| }{ | ||
| "resume_when_inputs_match": { | ||
| execMetaTimestamp: 1700000012, | ||
| execMetaTxs: [][]byte{[]byte("tx-1")}, | ||
| requestedTxs: [][]byte{[]byte("tx-1")}, | ||
| requestedTime: time.Unix(1700000012, 0), | ||
| expectFound: true, | ||
| expectPayloadID: true, | ||
| expectGetPayloads: 1, | ||
| }, | ||
| "ignore_when_timestamp_differs": { | ||
| execMetaTimestamp: 1700000010, | ||
| execMetaTxs: [][]byte{[]byte("tx-1")}, | ||
| requestedTxs: [][]byte{[]byte("tx-1")}, | ||
| requestedTime: time.Unix(1700000012, 0), | ||
| expectFound: false, | ||
| expectPayloadID: false, | ||
| expectGetPayloads: 0, | ||
| }, | ||
| "ignore_when_txs_differ": { | ||
| execMetaTimestamp: 1700000012, | ||
| execMetaTxs: [][]byte{[]byte("tx-old")}, | ||
| requestedTxs: [][]byte{[]byte("tx-new")}, | ||
| requestedTime: time.Unix(1700000012, 0), | ||
| expectFound: false, | ||
| expectPayloadID: false, | ||
| expectGetPayloads: 0, | ||
| }, | ||
| } | ||
| for name, spec := range specs { | ||
| t.Run(name, func(t *testing.T) { | ||
| store := NewEVMStore(dssync.MutexWrap(ds.NewMapDatastore())) | ||
| payloadID := engine.PayloadID{1, 2, 3, 4, 5, 6, 7, 8} | ||
| require.NoError(t, store.SaveExecMeta(t.Context(), &ExecMeta{ | ||
| Height: 12, | ||
| PayloadID: payloadID[:], | ||
| TxHash: hashTxs(spec.execMetaTxs), | ||
| Timestamp: spec.execMetaTimestamp, | ||
| Stage: ExecStageStarted, | ||
| })) | ||
|
|
||
| engineRPC := &mockReconcileEngineRPCClient{ | ||
| payloads: map[engine.PayloadID]*engine.ExecutionPayloadEnvelope{ | ||
| payloadID: {}, | ||
| }, | ||
| } | ||
| client := &EngineClient{ | ||
| engineClient: engineRPC, | ||
| ethClient: mockReconcileEthRPCClient{}, | ||
| store: store, | ||
| logger: zerolog.Nop(), | ||
| } | ||
|
|
||
| stateRoot, gotPayloadID, found, err := client.reconcileExecutionAtHeight(t.Context(), 12, spec.requestedTime, spec.requestedTxs) | ||
|
|
||
| require.NoError(t, err) | ||
| require.Nil(t, stateRoot) | ||
| require.Equal(t, spec.expectFound, found) | ||
| require.Equal(t, spec.expectPayloadID, gotPayloadID != nil) | ||
| if spec.expectPayloadID { | ||
| require.Equal(t, payloadID, *gotPayloadID) | ||
| } | ||
| require.Equal(t, spec.expectGetPayloads, engineRPC.getPayloadCalls) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| type mockReconcileEngineRPCClient struct { | ||
| payloads map[engine.PayloadID]*engine.ExecutionPayloadEnvelope | ||
| getPayloadCalls int | ||
| } | ||
|
|
||
| func (m *mockReconcileEngineRPCClient) ForkchoiceUpdated(_ context.Context, _ engine.ForkchoiceStateV1, _ map[string]any) (*engine.ForkChoiceResponse, error) { | ||
| return nil, errors.New("unexpected ForkchoiceUpdated call") | ||
| } | ||
|
|
||
| func (m *mockReconcileEngineRPCClient) GetPayload(_ context.Context, payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { | ||
| m.getPayloadCalls++ | ||
| payload, ok := m.payloads[payloadID] | ||
| if !ok { | ||
| return nil, errors.New("payload not found") | ||
| } | ||
|
|
||
| return payload, nil | ||
| } | ||
|
|
||
| func (m *mockReconcileEngineRPCClient) NewPayload(_ context.Context, _ *engine.ExecutableData, _ []string, _ string, _ [][]byte) (*engine.PayloadStatusV1, error) { | ||
| return nil, errors.New("unexpected NewPayload call") | ||
| } | ||
|
|
||
| type mockReconcileEthRPCClient struct{} | ||
|
|
||
| func (mockReconcileEthRPCClient) HeaderByNumber(_ context.Context, _ *big.Int) (*types.Header, error) { | ||
| return nil, errors.New("header not found") | ||
| } | ||
|
|
||
| func (mockReconcileEthRPCClient) GetTxs(_ context.Context) ([]string, error) { | ||
| return nil, errors.New("unexpected GetTxs call") | ||
| } |
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.
ExecStageStartedresumes still ignore parent-chain changes.This gate only checks timestamp and tx hash before reusing
pid. After a rollback/reorg, the parent atheight-1can change while both still match, so we can resume and later promote a payload that was built on the old parent. Please also validate the current parent hash before returningpid;ExecMetaalready has aParentHashfield you can use.🤖 Prompt for AI Agents