-
Notifications
You must be signed in to change notification settings - Fork 260
feat: add key rotation #3282
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
base: main
Are you sure you want to change the base?
feat: add key rotation #3282
Changes from all commits
da7b970
6a0f367
6ad13f2
45a7d75
8db31f9
6b2db4b
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 |
|---|---|---|
|
|
@@ -121,14 +121,6 @@ func NewExecutor( | |
| return nil, errors.New("signer cannot be nil") | ||
| } | ||
|
|
||
| addr, err := signer.GetAddress() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get address: %w", err) | ||
| } | ||
|
|
||
| if !bytes.Equal(addr, genesis.ProposerAddress) { | ||
| return nil, common.ErrNotProposer | ||
| } | ||
| } | ||
| if raftNode != nil && reflect.ValueOf(raftNode).IsNil() { | ||
| raftNode = nil | ||
|
|
@@ -242,15 +234,22 @@ func (e *Executor) initializeState() error { | |
| } | ||
|
|
||
| state = types.State{ | ||
| ChainID: e.genesis.ChainID, | ||
| InitialHeight: e.genesis.InitialHeight, | ||
| LastBlockHeight: e.genesis.InitialHeight - 1, | ||
| LastBlockTime: e.genesis.StartTime, | ||
| AppHash: stateRoot, | ||
| ChainID: e.genesis.ChainID, | ||
| InitialHeight: e.genesis.InitialHeight, | ||
| LastBlockHeight: e.genesis.InitialHeight - 1, | ||
| LastBlockTime: e.genesis.StartTime, | ||
| AppHash: stateRoot, | ||
| NextProposerAddress: e.initialProposerAddress(e.ctx), | ||
| // DA start height is usually 0 at InitChain unless it is a re-genesis or a based sequencer. | ||
| DAHeight: e.genesis.DAStartHeight, | ||
| } | ||
| } | ||
| if len(state.NextProposerAddress) == 0 { | ||
| state.NextProposerAddress = e.initialProposerAddress(e.ctx) | ||
| } | ||
| if err := e.assertConfiguredSigner(state.NextProposerAddress); err != nil { | ||
| return err | ||
| } | ||
|
Comment on lines
+237
to
+252
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm NextState() semantics when NextProposerAddress arg is empty (unchanged vs overwrite)
ast-grep --lang go --pattern 'func (s State) NextState($$$) ($$$) {
$$$
}'
# Where else is State.NextProposerAddress written?
rg -nP --type=go -C2 '\bNextProposerAddress\s*[:=]' types blockRepository: evstack/ev-node Length of output: 4654 🏁 Script executed: # 1. Get full NextState() method signature and implementation
sed -n '55,70p' types/state.go
# 2. Find all calls to NextState() to see what proposer values are passed
rg -nP --type=go '\.NextState\(' block typesRepository: evstack/ev-node Length of output: 1034 🏁 Script executed: # Find initialProposerAddress() implementation
ast-grep --lang go --pattern 'func ($_ $_) initialProposerAddress($$$) ($$$) {
$$$
}'
# Also check if there's block version being set in initializeState
sed -n '230,260p' block/internal/executing/executor.goRepository: evstack/ev-node Length of output: 1291 🏁 Script executed: # Find initialProposerAddress() method definition
rg -nP --type=go -A10 'func \(.*\) initialProposerAddress' block/internal/executing/executor.go
# Also check syncer for the same method
rg -nP --type=go -A10 'func \(.*\) initialProposerAddress' block/internal/syncing/syncer.goRepository: evstack/ev-node Length of output: 1023 🏁 Script executed: # Check State version field and any version 11 references
rg -nP --type=go 'Version.*=' types/state.go | head -20
rg -nP '11' types/state.go
# Check if there's any ADR or migration docs
fd -e md -e txt | xargs grep -l -i 'proposer\|rotation\|version.*11' 2>/dev/null | head -10Repository: evstack/ev-node Length of output: 542 🏁 Script executed: # Check ADR-012 for proposer rotation details
sed -n '1,200p' docs/adr/adr-012-based-sequencing.md | head -150
# Also search for "NextProposerAddress" in ADRs to understand design decisions
rg -l 'NextProposerAddress' docs/adr/Repository: evstack/ev-node Length of output: 5959 🏁 Script executed: # Check ADR-023 for execution-owned proposer rotation details
cat docs/adr/adr-023-execution-owned-proposer-rotation.mdRepository: evstack/ev-node Length of output: 4995 Document operational requirements for proposer rotation in migration/ADR consequences. The code correctly implements execution-owned proposer rotation per ADR-023. Two documentation gaps should be addressed:
🤖 Prompt for AI Agents |
||
|
|
||
| if e.raftNode != nil { | ||
| // Ensure node is fully synced before producing any blocks | ||
|
|
@@ -379,6 +378,32 @@ func (e *Executor) initializeState() error { | |
| return nil | ||
| } | ||
|
|
||
| func (e *Executor) initialProposerAddress(ctx context.Context) []byte { | ||
| if e.exec != nil { | ||
| info, err := e.exec.GetExecutionInfo(ctx) | ||
| if err != nil { | ||
| e.logger.Warn().Err(err).Msg("failed to get execution info for proposer, falling back to genesis proposer") | ||
| } else if len(info.NextProposerAddress) > 0 { | ||
| return append([]byte(nil), info.NextProposerAddress...) | ||
| } | ||
| } | ||
| return append([]byte(nil), e.genesis.ProposerAddress...) | ||
| } | ||
|
|
||
| func (e *Executor) assertConfiguredSigner(expectedProposer []byte) error { | ||
| if e.config.Node.BasedSequencer { | ||
| return nil | ||
| } | ||
| addr, err := e.signer.GetAddress() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get address: %w", err) | ||
| } | ||
| if !bytes.Equal(addr, expectedProposer) { | ||
| return common.ErrNotProposer | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // executionLoop handles block production and aggregation | ||
| func (e *Executor) executionLoop() { | ||
| e.logger.Info().Msg("starting execution loop") | ||
|
|
@@ -696,6 +721,10 @@ func (e *Executor) RetrieveBatch(ctx context.Context) (*BatchData, error) { | |
| func (e *Executor) CreateBlock(ctx context.Context, height uint64, batchData *BatchData) (*types.SignedHeader, *types.Data, error) { | ||
| currentState := e.getLastState() | ||
| headerTime := uint64(e.genesis.StartTime.UnixNano()) | ||
| proposerAddress := currentState.NextProposerAddress | ||
| if len(proposerAddress) == 0 { | ||
| proposerAddress = e.genesis.ProposerAddress | ||
| } | ||
|
|
||
| var lastHeaderHash types.Hash | ||
| var lastDataHash types.Hash | ||
|
|
@@ -736,14 +765,21 @@ func (e *Executor) CreateBlock(ctx context.Context, height uint64, batchData *Ba | |
| if err != nil { | ||
| return nil, nil, fmt.Errorf("failed to get public key: %w", err) | ||
| } | ||
| addr, err := e.signer.GetAddress() | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("failed to get address: %w", err) | ||
| } | ||
| if !bytes.Equal(addr, proposerAddress) { | ||
| return nil, nil, common.ErrNotProposer | ||
| } | ||
|
|
||
| validatorHash, err = e.options.ValidatorHasherProvider(e.genesis.ProposerAddress, pubKey) | ||
| validatorHash, err = e.options.ValidatorHasherProvider(proposerAddress, pubKey) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("failed to get validator hash: %w", err) | ||
| } | ||
| } else { | ||
| var err error | ||
| validatorHash, err = e.options.ValidatorHasherProvider(e.genesis.ProposerAddress, nil) | ||
| validatorHash, err = e.options.ValidatorHasherProvider(proposerAddress, nil) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("failed to get validator hash: %w", err) | ||
| } | ||
|
|
@@ -763,13 +799,13 @@ func (e *Executor) CreateBlock(ctx context.Context, height uint64, batchData *Ba | |
| }, | ||
| LastHeaderHash: lastHeaderHash, | ||
| AppHash: currentState.AppHash, | ||
| ProposerAddress: e.genesis.ProposerAddress, | ||
| ProposerAddress: proposerAddress, | ||
| ValidatorHash: validatorHash, | ||
| }, | ||
| Signature: lastSignature, | ||
| Signer: types.Signer{ | ||
| PubKey: pubKey, | ||
| Address: e.genesis.ProposerAddress, | ||
| Address: proposerAddress, | ||
| }, | ||
| } | ||
|
|
||
|
|
@@ -813,14 +849,14 @@ func (e *Executor) ApplyBlock(ctx context.Context, header types.Header, data *ty | |
| // Execute transactions | ||
| execCtx := context.WithValue(ctx, types.HeaderContextKey, header) | ||
|
|
||
| newAppHash, err := e.executeTxsWithRetry(execCtx, rawTxs, header, currentState) | ||
| result, err := e.executeTxsWithRetry(execCtx, rawTxs, header, currentState) | ||
| if err != nil { | ||
| e.sendCriticalError(fmt.Errorf("failed to execute transactions: %w", err)) | ||
| return types.State{}, fmt.Errorf("failed to execute transactions: %w", err) | ||
| } | ||
|
|
||
| // Create new state | ||
| newState, err := currentState.NextState(header, newAppHash) | ||
| newState, err := currentState.NextState(header, result.UpdatedStateRoot, result.NextProposerAddress) | ||
| if err != nil { | ||
| return types.State{}, fmt.Errorf("failed to create next state: %w", err) | ||
| } | ||
|
|
@@ -851,12 +887,12 @@ func (e *Executor) signHeader(ctx context.Context, header *types.Header) (types. | |
|
|
||
| // executeTxsWithRetry executes transactions with retry logic. | ||
| // NOTE: the function retries the execution client call regardless of the error. Some execution clients errors are irrecoverable, and will eventually halt the node, as expected. | ||
| func (e *Executor) executeTxsWithRetry(ctx context.Context, rawTxs [][]byte, header types.Header, currentState types.State) ([]byte, error) { | ||
| func (e *Executor) executeTxsWithRetry(ctx context.Context, rawTxs [][]byte, header types.Header, currentState types.State) (coreexecutor.ExecuteResult, error) { | ||
| for attempt := 1; attempt <= common.MaxRetriesBeforeHalt; attempt++ { | ||
| newAppHash, err := e.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), currentState.AppHash) | ||
| result, err := e.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), currentState.AppHash) | ||
| if err != nil { | ||
| if attempt == common.MaxRetriesBeforeHalt { | ||
| return nil, fmt.Errorf("failed to execute transactions: %w", err) | ||
| return coreexecutor.ExecuteResult{}, fmt.Errorf("failed to execute transactions: %w", err) | ||
| } | ||
|
|
||
| e.logger.Error().Err(err). | ||
|
|
@@ -869,14 +905,14 @@ func (e *Executor) executeTxsWithRetry(ctx context.Context, rawTxs [][]byte, hea | |
| case <-time.After(common.MaxRetriesTimeout): | ||
| continue | ||
| case <-e.ctx.Done(): | ||
| return nil, fmt.Errorf("context cancelled during retry: %w", e.ctx.Err()) | ||
| return coreexecutor.ExecuteResult{}, fmt.Errorf("context cancelled during retry: %w", e.ctx.Err()) | ||
| } | ||
| } | ||
|
|
||
| return newAppHash, nil | ||
| return result, nil | ||
| } | ||
|
|
||
| return nil, nil | ||
| return coreexecutor.ExecuteResult{}, nil | ||
| } | ||
|
|
||
| // sendCriticalError sends a critical error to the error channel without blocking | ||
|
|
||
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.
Seed replay with the same initial proposer source as normal startup.
Syncer.initializeState()now prefersGetExecutionInfo().NextProposerAddressand only falls back to genesis when execution returns empty, but replay still hardcodess.genesis.ProposerAddresshere. If execution selects a different proposer at genesis, replaying the first block will compute a different proposer chain and can trip the newNextProposerAddressconsistency check on restart.🛠️ Suggested fix
🤖 Prompt for AI Agents