-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
feat!: [Comet v0.38 Integration] Vote Extensions #15766
Changes from 30 commits
3df3084
8019c2e
5a069e7
7c1174f
f894f57
1010bc9
dbee603
19e3c9c
45ec586
0d9bf07
36fc06d
9ed60b5
e9738e6
2dc6194
5782ff4
6532c97
d67c9e0
10a99ba
b5aa99b
1061ff5
d12a8ea
25e32dc
137267d
dd57ca6
2f56bbf
2e32ced
136403f
75873b6
5d72bc4
01505fc
ace8648
4d8c1b7
3400180
925ee71
6a3104a
7d5ae38
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 |
---|---|---|
|
@@ -45,6 +45,7 @@ func (app *BaseApp) InitChain(_ context.Context, req *abci.RequestInitChain) (*a | |
// On a new chain, we consider the init chain block height as 0, even though | ||
// req.InitialHeight is 1 by default. | ||
initHeader := cmtproto.Header{ChainID: req.ChainId, Time: req.Time} | ||
app.initialHeight = req.InitialHeight | ||
|
||
app.logger.Info("InitChain", "initialHeight", req.InitialHeight, "chainID", req.ChainId) | ||
|
||
|
@@ -56,13 +57,12 @@ func (app *BaseApp) InitChain(_ context.Context, req *abci.RequestInitChain) (*a | |
if req.InitialHeight > 1 { | ||
initHeader.Height = req.InitialHeight | ||
if err := app.cms.SetInitialVersion(req.InitialHeight); err != nil { | ||
panic(err) | ||
} | ||
return nil, err | ||
} | ||
|
||
// initialize states with a correct header | ||
app.setState(runTxModeFinalize, initHeader) | ||
app.setState(runTxModeCheck, initHeader) | ||
app.setState(execModeFinalize, initHeader) | ||
app.setState(execModeCheck, initHeader) | ||
|
||
// Store the consensus params in the BaseApp's param store. Note, this must be | ||
// done after the finalizeBlockState and context have been set as it's persisted | ||
|
@@ -408,14 +408,14 @@ func (app *BaseApp) legacyEndBlock(req *abci.RequestFinalizeBlock) sdk.LegacyRes | |
// will contain relevant error information. Regardless of tx execution outcome, | ||
// the ResponseCheckTx will contain relevant gas execution context. | ||
func (app *BaseApp) CheckTx(_ context.Context, req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { | ||
var mode runTxMode | ||
var mode execMode | ||
|
||
switch { | ||
case req.Type == abci.CheckTxType_New: | ||
mode = runTxModeCheck | ||
mode = execModeCheck | ||
|
||
case req.Type == abci.CheckTxType_Recheck: | ||
mode = runTxModeReCheck | ||
mode = execModeReCheck | ||
|
||
default: | ||
return nil, fmt.Errorf("unknown RequestCheckTx type: %s", req.Type) | ||
|
@@ -453,11 +453,19 @@ func (app *BaseApp) PrepareProposal(_ context.Context, req *abci.RequestPrepareP | |
return nil, errors.New("PrepareProposal method not set") | ||
} | ||
|
||
// always reset state given that PrepareProposal can timeout and be called again | ||
emptyHeader := cmtproto.Header{ChainID: app.chainID} | ||
app.setState(runTxPrepareProposal, emptyHeader) | ||
// Always reset state given that PrepareProposal can timeout and be called | ||
// again in a subsequent round. | ||
header := cmtproto.Header{ | ||
ChainID: app.chainID, | ||
Height: req.Height, | ||
Time: req.Time, | ||
ProposerAddress: req.ProposerAddress, | ||
NextValidatorsHash: req.NextValidatorsHash, | ||
} | ||
app.setState(execModePrepareProposal, header) | ||
|
||
// CometBFT must never call PrepareProposal with a height of 0. | ||
// | ||
// Ref: https://github.com/cometbft/cometbft/blob/059798a4f5b0c9f52aa8655fa619054a0154088c/spec/core/state.md?plain=1#L37-L38 | ||
if req.Height < 1 { | ||
return nil, errors.New("PrepareProposal called with invalid height") | ||
|
@@ -488,7 +496,8 @@ func (app *BaseApp) PrepareProposal(_ context.Context, req *abci.RequestPrepareP | |
|
||
resp, err = app.prepareProposal(app.prepareProposalState.ctx, req) | ||
if err != nil { | ||
return nil, err | ||
app.logger.Error("failed to prepare proposal", "height", req.Height, "error", err) | ||
return &abci.ResponsePrepareProposal{Txs: [][]byte{}}, nil | ||
} | ||
|
||
return resp, nil | ||
|
@@ -520,9 +529,25 @@ func (app *BaseApp) ProcessProposal(_ context.Context, req *abci.RequestProcessP | |
return nil, errors.New("ProcessProposal called with invalid height") | ||
} | ||
|
||
// always reset state given that ProcessProposal can timeout and be called again | ||
emptyHeader := cmtproto.Header{ChainID: app.chainID} | ||
app.setState(runTxProcessProposal, emptyHeader) | ||
// Always reset state given that ProcessProposal can timeout and be called | ||
// again in a subsequent round. | ||
header := cmtproto.Header{ | ||
ChainID: app.chainID, | ||
Height: req.Height, | ||
Time: req.Time, | ||
ProposerAddress: req.ProposerAddress, | ||
NextValidatorsHash: req.NextValidatorsHash, | ||
} | ||
app.setState(execModeProcessProposal, header) | ||
|
||
// Since the application can get access to FinalizeBlock state and write to it, | ||
// we must be sure to reset it in case ProcessProposal timeouts and is called | ||
// again in a subsequent round. However, we only want to do this after we've | ||
// processed the first block, as we want to avoid overwriting the finalizeState | ||
// after state changes during InitChain. | ||
if req.Height > app.initialHeight { | ||
app.setState(execModeFinalize, header) | ||
} | ||
|
||
app.processProposalState.ctx = app.getContextForProposal(app.processProposalState.ctx, req.Height). | ||
WithVoteInfos(app.voteInfos). | ||
|
@@ -550,12 +575,114 @@ func (app *BaseApp) ProcessProposal(_ context.Context, req *abci.RequestProcessP | |
|
||
resp, err = app.processProposal(app.processProposalState.ctx, req) | ||
if err != nil { | ||
return nil, err | ||
app.logger.Error("failed to process proposal", "height", req.Height, "error", err) | ||
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
// ExtendVote implements the ExtendVote ABCI method and returns a ResponseExtendVote. | ||
// It calls the application's ExtendVote handler which is responsible for performing | ||
// application-specific business logic when sending a pre-commit for the NEXT | ||
// block height. The extensions response may be non-deterministic but must always | ||
// be returned, even if empty. | ||
// | ||
// Agreed upon vote extensions are made available to the proposer of the next | ||
// height and are committed in the subsequent height, i.e. H+2. An error is | ||
// returned if vote extensions are not enabled or if extendVote fails or panics. | ||
// | ||
// Note, an error returned from ExtendVote will not cause the CometBFT to panic, | ||
// so it is safe to return errors upon failure. | ||
alexanderbez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func (app *BaseApp) ExtendVote(_ context.Context, req *abci.RequestExtendVote) (resp *abci.ResponseExtendVote, err error) { | ||
// Always reset state given that ExtendVote and VerifyVoteExtension can timeout | ||
// and be called again in a subsequent round. | ||
emptyHeader := cmtproto.Header{ChainID: app.chainID, Height: req.Height} | ||
app.setState(execModeVoteExtension, emptyHeader) | ||
|
||
// If vote extensions are not enabled, as a safety precaution, we return an | ||
// error. | ||
cp := app.GetConsensusParams(app.voteExtensionState.ctx) | ||
if cp.Abci != nil && cp.Abci.VoteExtensionsEnableHeight <= 0 { | ||
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. Do we need a consensus module migration here? Or can we capture this somehow so we don't forget 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. An app would have to set the |
||
return nil, fmt.Errorf("vote extensions are not enabled; unexpected call to ExtendVote at height %d", req.Height) | ||
} | ||
|
||
if app.extendVote == nil { | ||
return nil, errors.New("application ExtendVote handler not set") | ||
} | ||
|
||
app.voteExtensionState.ctx = app.voteExtensionState.ctx. | ||
WithConsensusParams(cp). | ||
WithBlockGasMeter(storetypes.NewInfiniteGasMeter()). | ||
WithBlockHeight(req.Height). | ||
WithHeaderHash(req.Hash) | ||
|
||
// add a deferred recover handler in case extendVote panics | ||
defer func() { | ||
if err := recover(); err != nil { | ||
|
||
app.logger.Error( | ||
"panic recovered in ExtendVote", | ||
"height", req.Height, | ||
"hash", fmt.Sprintf("%X", req.Hash), | ||
"panic", err, | ||
) | ||
err = fmt.Errorf("recovered application panic in ExtendVote: %w", err) | ||
|
||
} | ||
}() | ||
|
||
resp, err = app.extendVote(app.voteExtensionState.ctx, req) | ||
if err != nil { | ||
app.logger.Error("failed to extend vote", "height", req.Height, "error", err) | ||
return nil, fmt.Errorf("failed to extend vote: %w", err) | ||
} | ||
|
||
return resp, err | ||
} | ||
|
||
// VerifyVoteExtension implements the VerifyVoteExtension ABCI method and returns | ||
// a ResponseVerifyVoteExtension. It calls the applications' VerifyVoteExtension | ||
// handler which is responsible for performing application-specific business | ||
// logic in verifying a vote extension from another validator during the pre-commit | ||
// phase. The response MUST be deterministic. An error is returned if vote | ||
// extensions are not enabled or if verifyVoteExt fails or panics. | ||
// | ||
// Note, an error returned from VerifyVoteExtension will not cause the CometBFT | ||
// to panic, so it is safe to return errors upon failure. | ||
func (app *BaseApp) VerifyVoteExtension(_ context.Context, req *abci.RequestVerifyVoteExtension) (resp *abci.ResponseVerifyVoteExtension, err error) { | ||
// If vote extensions are not enabled, as a safety precaution, we return an | ||
// error. | ||
cp := app.GetConsensusParams(app.voteExtensionState.ctx) | ||
if cp.Abci != nil && cp.Abci.VoteExtensionsEnableHeight <= 0 { | ||
return nil, fmt.Errorf("vote extensions are not enabled; unexpected call to VerifyVoteExtension at height %d", req.Height) | ||
} | ||
|
||
if app.verifyVoteExt == nil { | ||
return nil, errors.New("application VerifyVoteExtension handler not set") | ||
} | ||
|
||
// add a deferred recover handler in case verifyVoteExt panics | ||
defer func() { | ||
if err := recover(); err != nil { | ||
|
||
app.logger.Error( | ||
"panic recovered in VerifyVoteExtension", | ||
"height", req.Height, | ||
"hash", fmt.Sprintf("%X", req.Hash), | ||
"validator", fmt.Sprintf("%X", req.ValidatorAddress), | ||
"panic", err, | ||
) | ||
err = fmt.Errorf("recovered application panic in VerifyVoteExtension: %w", err) | ||
|
||
} | ||
}() | ||
|
||
resp, err = app.verifyVoteExt(app.voteExtensionState.ctx, req) | ||
if err != nil { | ||
app.logger.Error("failed to verify vote extension", "height", req.Height, "error", err) | ||
return nil, fmt.Errorf("failed to verify vote extension: %w", err) | ||
} | ||
|
||
return resp, err | ||
} | ||
|
||
func (app *BaseApp) legacyDeliverTx(tx []byte) *abci.ExecTxResult { | ||
gInfo := sdk.GasInfo{} | ||
resultStr := "successful" | ||
|
@@ -584,7 +711,7 @@ func (app *BaseApp) legacyDeliverTx(tx []byte) *abci.ExecTxResult { | |
telemetry.SetGauge(float32(gInfo.GasWanted), "tx", "gas", "wanted") | ||
}() | ||
|
||
gInfo, result, anteEvents, err := app.runTx(runTxModeFinalize, tx) | ||
gInfo, result, anteEvents, err := app.runTx(execModeFinalize, tx) | ||
if err != nil { | ||
resultStr = "failed" | ||
resp = sdkerrors.ResponseExecTxResultWithEvents( | ||
|
@@ -633,7 +760,7 @@ func (app *BaseApp) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBl | |
// already be initialized in InitChain. Otherwise app.finalizeBlockState will be | ||
// nil, since it is reset on Commit. | ||
if app.finalizeBlockState == nil { | ||
app.setState(runTxModeFinalize, header) | ||
app.setState(execModeFinalize, header) | ||
} else { | ||
// In the first block, app.finalizeBlockState.ctx will already be initialized | ||
// by InitChain. Context is now updated with Header information. | ||
|
@@ -719,7 +846,7 @@ func (app *BaseApp) Commit(_ context.Context, _ *abci.RequestCommit) (*abci.Resp | |
// | ||
// NOTE: This is safe because CometBFT holds a lock on the mempool for | ||
// Commit. Use the header from this latest block. | ||
app.setState(runTxModeCheck, header) | ||
app.setState(execModeCheck, header) | ||
|
||
app.finalizeBlockState = nil | ||
|
||
|
@@ -915,7 +1042,7 @@ func (app *BaseApp) FilterPeerByID(info string) *abci.ResponseQuery { | |
// ProcessProposal. We use finalizeBlockState on the first block to be able to | ||
// access any state changes made in InitChain. | ||
func (app *BaseApp) getContextForProposal(ctx sdk.Context, height int64) sdk.Context { | ||
if height == 1 { | ||
if height == app.initialHeight { | ||
ctx, _ = app.finalizeBlockState.ctx.CacheContext() | ||
|
||
// clear all context data set during InitChain to avoid inconsistent behavior | ||
|
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.
Okay, now I get what you meant the other day. So it's not possible that ProcessProposal gets called more than once during the first block?
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.
It is possible. Why does that matter? This conditional is irrelevant to the round number in the first block.
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.
Then I'm not so sure I understood you the other day lol. In other words, why this wouldn't cause an issue? I understand that we would be having multiple writes to state without discarding what was done in the previous processProposal.
So step 3 would be seeing the results of steps 1+2, but I think it should only be seeing results of step 1.
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.
That's not the full correct understanding of what's happening. What is actually happening is, in the context of the first block:
So step (3) would be a on a fresh cached copy of
FinalizeState
from step (1).The big caveat here though is that a chain should NOT write to
FinalizeState
on the first block. I'll update the godoc. This isn't a concern really though because vote extensions can be used until at least the block after the 1st block.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.
Ahh thanks for taking the time to explain me, I was a bit too worried 😅