diff --git a/abci/extend_vote.go b/abci/extend_vote.go index efa2cc46..2947f65d 100644 --- a/abci/extend_vote.go +++ b/abci/extend_vote.go @@ -120,10 +120,6 @@ func (h *VoteExtHandler) constructVoteExtBody(ctx sdk.Context, req *abci.Request return voteexthandler.Body{}, errors.Wrap(types.ErrFailedToComputeInitialValidatorSetRoot, err.Error()) }*/ - prevStateRoot := h.stateRoots[req.GetHeight()-1] - initStateRoot := h.stateRoots[req.GetHeight()] - - var extBody voteexthandler.Body /*if len(validatorUpdates) != 0 { // Apply validator set updates to the initial validator set and create merkle tree from the new validator set @@ -157,20 +153,14 @@ func (h *VoteExtHandler) constructVoteExtBody(ctx sdk.Context, req *abci.Request } }*/ - extBody = voteexthandler.Body{ - InitialValidatorSetRoot: hardcoded[:], - InitialBlockHeight: req.GetHeight() - 1, - InitialStateRoot: prevStateRoot, - NewValidatorSetRoot: hardcoded[:], - NewBlockHeight: req.GetHeight(), - NewStateRoot: initStateRoot, - } - - return extBody, nil + return h.buildExpectedVoteExtBody(req.GetHeight()) } func (h *VoteExtHandler) ExtendVoteHandler() sdk.ExtendVoteHandler { return func(ctx sdk.Context, req *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) { + if req.GetHeight() < 3 { + return &abci.ResponseExtendVote{VoteExtension: []byte{}}, nil + } // Initialize poseidon hash poseidonHash := poseidon.CreatePoseidon(*field.Fp, constants.PoseidonParamsKimchiFp) diff --git a/abci/helpers.go b/abci/helpers.go index 7b908770..5aca897d 100644 --- a/abci/helpers.go +++ b/abci/helpers.go @@ -1,11 +1,11 @@ package vote_ext import ( - "encoding/json" /* "math/big" "sort" */ + "strconv" "sync" "cosmossdk.io/errors" @@ -61,7 +61,7 @@ func verifySchnorr(voteExt MinaSignatureVoteExt, pubKey keys.PublicKey, ctx sdk. if err := sig.UnmarshalBytes(voteExt.Signature); err != nil { return errors.Wrap(types.ErrInvalidSigEncoding, "") } - // Verify signature; if ok, keep the vote in memory. + // Verify the vote-extension signature against the reconstructed body hash. if !pubKey.Verify(sig, extBodyHashInput, types.DevnetNetworkID) { return errors.Wrap(types.ErrInvalidSignature, "") } @@ -195,6 +195,62 @@ func (h *VoteExtHandler) computeValidatorSetMerkleRoot(validators []ValidatorInf */ +func cloneBytes(b []byte) []byte { + if b == nil { + return nil + } + + return append([]byte(nil), b...) +} + +func (h *VoteExtHandler) storeStateRoot(height int64, root []byte) { + h.mu.Lock() + defer h.mu.Unlock() + + if h.stateRoots == nil { + h.stateRoots = make(map[int64][]byte) + } + + h.stateRoots[height] = cloneBytes(root) +} + +func (h *VoteExtHandler) getStateRoot(height int64) ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() + + root, ok := h.stateRoots[height] + if !ok { + return nil, false + } + + return cloneBytes(root), true +} + +func (h *VoteExtHandler) buildExpectedVoteExtBody(height int64) (voteexthandler.Body, error) { + if height < 3 { + return voteexthandler.Body{}, errors.Wrap(types.ErrFailedToGetVoteExtBody, "vote extensions start at height 3") + } + + initialStateRoot, ok := h.getStateRoot(height - 2) + if !ok { + return voteexthandler.Body{}, errors.Wrap(types.ErrFailedToGetVoteExtBody, "missing state root for committed height "+strconv.FormatInt(height-2, 10)) + } + + newStateRoot, ok := h.getStateRoot(height - 1) + if !ok { + return voteexthandler.Body{}, errors.Wrap(types.ErrFailedToGetVoteExtBody, "missing state root for committed height "+strconv.FormatInt(height-1, 10)) + } + + return voteexthandler.Body{ + InitialValidatorSetRoot: hardcoded[:], + InitialBlockHeight: height - 2, + InitialStateRoot: initialStateRoot, + NewValidatorSetRoot: hardcoded[:], + NewBlockHeight: height - 1, + NewStateRoot: newStateRoot, + }, nil +} + // storeVote saves the extension in-memory for later proposal processing. func (h *VoteExtHandler) storeVote(height uint64, minaAddress string, ext []byte) { h.mu.Lock() @@ -205,7 +261,7 @@ func (h *VoteExtHandler) storeVote(height uint64, minaAddress string, ext []byte if _, ok := h.votes[height]; !ok { h.votes[height] = make(map[string][]byte) } - h.votes[height][minaAddress] = ext + h.votes[height][minaAddress] = cloneBytes(ext) } // fetchVotes returns a COPY of the map for the given height. @@ -215,7 +271,7 @@ func (h *VoteExtHandler) fetchVotes(height uint64) map[string][]byte { res := make(map[string][]byte) if m, ok := h.votes[height]; ok { for k, v := range m { - res[k] = v + res[k] = cloneBytes(v) } } return res @@ -227,24 +283,3 @@ func (h *VoteExtHandler) deleteVotes(height uint64) { defer h.mu.Unlock() delete(h.votes, height) } - -func (h *VoteExtHandler) getVoteExtBody(height uint64) (types.Body, error) { - h.mu.Lock() - defer h.mu.Unlock() - - ownMinaAddress, err := h.MinaPrivateKey.PublicKey.ToAddress() - if err != nil { - return types.Body{}, errors.Wrap(types.ErrFailedToConvertPubKeyToAddr, "nodes own secondary pubkey") - } - - for _, vote := range h.votes[height] { - var ve MinaSignatureVoteExt - if err := json.Unmarshal(vote, &ve); err != nil { - continue // skip malformed entry - } - if ve.MinaAddress == ownMinaAddress { - return ve.VoteExtBody, nil - } - } - return types.Body{}, errors.Wrap(types.ErrMissingVoteExt, "") -} diff --git a/abci/pre_blocker.go b/abci/pre_blocker.go index 721b0cfe..74f460cf 100644 --- a/abci/pre_blocker.go +++ b/abci/pre_blocker.go @@ -18,7 +18,6 @@ func (h *VoteExtHandler) PreBlocker() sdk.PreBlocker { // If height is 1, we won't have any votes thus skip the proposal if req.GetHeight() == 1 { - h.stateRoots[req.GetHeight()] = ctx.BlockHeader().AppHash return &sdk.ResponsePreBlock{}, nil } diff --git a/abci/prepare_proposal.go b/abci/prepare_proposal.go index a275d4d9..4e5ba552 100644 --- a/abci/prepare_proposal.go +++ b/abci/prepare_proposal.go @@ -12,7 +12,8 @@ import ( // PrepareProposalHandler injects the collected vote-extensions (for height-1) // as the very first transaction of the proposal block. // A simple JSON payload prefixed by "VOTEEXT:" is used; this is *not* part of -// consensus state and will be verified by ProcessProposal on peers. +// consensus state and will be verified by ProcessProposal on peers. The state +// root cache is seeded in ProcessProposal, not in PrepareProposal. func (h *VoteExtHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { @@ -21,8 +22,6 @@ func (h *VoteExtHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { targetHeight := uint64(req.GetHeight() - 1) votes := h.fetchVotes(targetHeight) if len(votes) == 0 { - h.stateRoots[req.GetHeight()] = ctx.BlockHeader().AppHash - return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil } @@ -40,8 +39,6 @@ func (h *VoteExtHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { txs = append(txs, extTx) txs = append(txs, req.Txs...) - h.stateRoots[req.GetHeight()] = ctx.BlockHeader().AppHash - return &abci.ResponsePrepareProposal{Txs: txs}, nil } } diff --git a/abci/process_proposal.go b/abci/process_proposal.go index 49e261aa..7ade651b 100644 --- a/abci/process_proposal.go +++ b/abci/process_proposal.go @@ -39,7 +39,7 @@ func (h *VoteExtHandler) reconstructVoteExtBody(req *abci.RequestProcessProposal if err := json.Unmarshal(voteBytes, &ve); err != nil { continue // skip malformed entry } - h.storeVote(uint64(req.GetHeight()), ve.MinaAddress, voteBytes) + h.storeVote(data.Height, ve.MinaAddress, voteBytes) } // Create our Mina address from our local Mina public key. @@ -67,9 +67,15 @@ func (h *VoteExtHandler) reconstructVoteExtBody(req *abci.RequestProcessProposal // is rejected. This shifts the ≥⅔ voting-power requirement to CometBFT itself: // a block that does not include ≥⅔ of the network's vote-extensions will be // rejected automatically because fewer than ⅔ of validators will `ACCEPT` it. +// It also seeds the state-root cache with committed root(H-1) before the vote +// extension stage starts for height H. func (h *VoteExtHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { return func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { + if req.GetHeight() >= 2 { + h.storeStateRoot(req.GetHeight()-1, ctx.BlockHeader().AppHash) + } + // If height is 1, we won't have any votes thus skip the proposal if req.GetHeight() == 1 { return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil diff --git a/abci/verify_extend_vote.go b/abci/verify_extend_vote.go index 60b2ae1f..2d4c342d 100644 --- a/abci/verify_extend_vote.go +++ b/abci/verify_extend_vote.go @@ -19,6 +19,14 @@ import ( func (h *VoteExtHandler) VerifyVoteExtensionHandler() sdk.VerifyVoteExtensionHandler { return func(ctx sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) { + if req.GetHeight() < 3 { + if len(req.VoteExtension) == 0 { + return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil + } + + return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_REJECT}, errors.Wrap(types.ErrMalformedVoteExtPayload, "vote extensions start at height 3") + } + // Unmarshal the extension payload var voteExt MinaSignatureVoteExt if err := json.Unmarshal(req.VoteExtension, &voteExt); err != nil { @@ -35,6 +43,8 @@ func (h *VoteExtHandler) VerifyVoteExtensionHandler() sdk.VerifyVoteExtensionHan return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_REJECT}, err } + h.storeVote(uint64(req.GetHeight()), voteExt.MinaAddress, req.VoteExtension) + return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil } } @@ -97,13 +107,13 @@ func (h *VoteExtHandler) verifyExtensionSig(ctx sdk.Context, req *abci.RequestVe return err } - h.storeVote(uint64(req.GetHeight()), voteExt.MinaAddress, req.VoteExtension) return nil } -// checks the validity of extension body for VerifyVoteExtensionHandler +// checkValidityOfVoteExtBody reconstructs the expected vote body from the +// shared committed-root cache instead of reading this node's local vote cache. func (h *VoteExtHandler) checkValidityOfVoteExtBody(ctx sdk.Context, req *abci.RequestVerifyVoteExtension, voteExt MinaSignatureVoteExt) error { - extBody, err := h.getVoteExtBody(uint64(req.GetHeight())) + extBody, err := h.buildExpectedVoteExtBody(req.GetHeight()) if err != nil { return errors.Wrap(types.ErrFailedToGetVoteExtBody, "failed to get vote extension body for height "+strconv.FormatUint(uint64(req.GetHeight()), 10)) } diff --git a/x/voteexthandler/types/vote_ext_body.pb.go b/x/voteexthandler/types/vote_ext_body.pb.go index a5f25732..5628d86a 100644 --- a/x/voteexthandler/types/vote_ext_body.pb.go +++ b/x/voteexthandler/types/vote_ext_body.pb.go @@ -5,11 +5,10 @@ package types import ( fmt "fmt" + proto "github.com/cosmos/gogoproto/proto" io "io" math "math" math_bits "math/bits" - - proto "github.com/cosmos/gogoproto/proto" ) // Reference imports to suppress errors if they are not otherwise used.