diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index 26fbf6337..962b5698f 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -234,6 +234,14 @@ func (cli *grpcClient) ProcessProposal(ctx context.Context, req *types.RequestPr return cli.client.ProcessProposal(ctx, types.ToRequestProcessProposal(req).GetProcessProposal(), grpc.WaitForReady(true)) } +func (cli *grpcClient) ExtendVote(ctx context.Context, req *types.RequestExtendVote) (*types.ResponseExtendVote, error) { + return cli.client.ExtendVote(ctx, types.ToRequestExtendVote(req).GetExtendVote(), grpc.WaitForReady(true)) +} + +func (cli *grpcClient) VerifyVoteExtension(ctx context.Context, req *types.RequestVerifyVoteExtension) (*types.ResponseVerifyVoteExtension, error) { + return cli.client.VerifyVoteExtension(ctx, types.ToRequestVerifyVoteExtension(req).GetVerifyVoteExtension(), grpc.WaitForReady(true)) +} + func (cli *grpcClient) FinalizeBlock(ctx context.Context, req *types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error) { return cli.client.FinalizeBlock(ctx, types.ToRequestFinalizeBlock(req).GetFinalizeBlock(), grpc.WaitForReady(true)) } diff --git a/abci/client/local_client.go b/abci/client/local_client.go index fe6e0cfd9..fec449321 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -164,6 +164,20 @@ func (app *localClient) ProcessProposal(ctx context.Context, req *types.RequestP return app.Application.ProcessProposal(ctx, req) } +func (app *localClient) ExtendVote(ctx context.Context, req *types.RequestExtendVote) (*types.ResponseExtendVote, error) { + app.mtx.Lock() + defer app.mtx.Unlock() + + return app.Application.ExtendVote(ctx, req) +} + +func (app *localClient) VerifyVoteExtension(ctx context.Context, req *types.RequestVerifyVoteExtension) (*types.ResponseVerifyVoteExtension, error) { + app.mtx.Lock() + defer app.mtx.Unlock() + + return app.Application.VerifyVoteExtension(ctx, req) +} + func (app *localClient) FinalizeBlock(ctx context.Context, req *types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error) { app.mtx.Lock() defer app.mtx.Unlock() diff --git a/abci/client/socket_client.go b/abci/client/socket_client.go index 061cfc0a5..3817dafbb 100644 --- a/abci/client/socket_client.go +++ b/abci/client/socket_client.go @@ -379,6 +379,28 @@ func (cli *socketClient) ProcessProposal(ctx context.Context, req *types.Request return reqRes.Response.GetProcessProposal(), cli.Error() } +func (cli *socketClient) ExtendVote(ctx context.Context, req *types.RequestExtendVote) (*types.ResponseExtendVote, error) { + reqRes, err := cli.queueRequest(ctx, types.ToRequestExtendVote(req)) + if err != nil { + return nil, err + } + if err := cli.Flush(ctx); err != nil { + return nil, err + } + return reqRes.Response.GetExtendVote(), cli.Error() +} + +func (cli *socketClient) VerifyVoteExtension(ctx context.Context, req *types.RequestVerifyVoteExtension) (*types.ResponseVerifyVoteExtension, error) { + reqRes, err := cli.queueRequest(ctx, types.ToRequestVerifyVoteExtension(req)) + if err != nil { + return nil, err + } + if err := cli.Flush(ctx); err != nil { + return nil, err + } + return reqRes.Response.GetVerifyVoteExtension(), cli.Error() +} + func (cli *socketClient) FinalizeBlock(ctx context.Context, req *types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error) { reqRes, err := cli.queueRequest(ctx, types.ToRequestFinalizeBlock(req)) if err != nil { @@ -461,6 +483,10 @@ func resMatchesReq(req *types.Request, res *types.Response) (ok bool) { _, ok = res.Value.(*types.Response_ListSnapshots) case *types.Request_OfferSnapshot: _, ok = res.Value.(*types.Response_OfferSnapshot) + case *types.Request_ExtendVote: + _, ok = res.Value.(*types.Response_ExtendVote) + case *types.Request_VerifyVoteExtension: + _, ok = res.Value.(*types.Response_VerifyVoteExtension) case *types.Request_PrepareProposal: _, ok = res.Value.(*types.Response_PrepareProposal) case *types.Request_ProcessProposal: diff --git a/abci/example/kvstore/README.md b/abci/example/kvstore/README.md index e97debb1d..e9e38b53c 100644 --- a/abci/example/kvstore/README.md +++ b/abci/example/kvstore/README.md @@ -1,6 +1,6 @@ # KVStore -The KVStoreApplication is a simple merkle key-value store. +The KVStoreApplication is a simple merkle key-value store. Transactions of the form `key=value` are stored as key-value pairs in the tree. Transactions without an `=` sign set the value to the key. The app has no replay protection (other than what the mempool provides). diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 1053e0d7f..94cd551f0 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -288,6 +288,18 @@ func (s *SocketServer) handleRequest(ctx context.Context, req *types.Request) (* return nil, err } return types.ToResponseApplySnapshotChunk(res), nil + case *types.Request_ExtendVote: + res, err := s.app.ExtendVote(ctx, r.ExtendVote) + if err != nil { + return nil, err + } + return types.ToResponseExtendVote(res), nil + case *types.Request_VerifyVoteExtension: + res, err := s.app.VerifyVoteExtension(ctx, r.VerifyVoteExtension) + if err != nil { + return nil, err + } + return types.ToResponseVerifyVoteExtension(res), nil default: return nil, fmt.Errorf("unknown request from client: %T", req) } diff --git a/abci/types/application.go b/abci/types/application.go index b08862647..b33da73dc 100644 --- a/abci/types/application.go +++ b/abci/types/application.go @@ -20,6 +20,8 @@ type Application interface { ProcessProposal(context.Context, *RequestProcessProposal) (*ResponseProcessProposal, error) // Deliver the decided block with its txs to the Application FinalizeBlock(context.Context, *RequestFinalizeBlock) (*ResponseFinalizeBlock, error) + ExtendVote(context.Context, *RequestExtendVote) (*ResponseExtendVote, error) + VerifyVoteExtension(context.Context, *RequestVerifyVoteExtension) (*ResponseVerifyVoteExtension, error) // Commit the state and return the application Merkle root hash Commit(context.Context, *RequestCommit) (*ResponseCommit, error) @@ -94,6 +96,14 @@ func (BaseApplication) ProcessProposal(_ context.Context, req *RequestProcessPro return &ResponseProcessProposal{Status: ResponseProcessProposal_ACCEPT}, nil } +func (BaseApplication) ExtendVote(_ context.Context, req *RequestExtendVote) (*ResponseExtendVote, error) { + return &ResponseExtendVote{}, nil +} + +func (BaseApplication) VerifyVoteExtension(_ context.Context, req *RequestVerifyVoteExtension) (*ResponseVerifyVoteExtension, error) { + return &ResponseVerifyVoteExtension{Result: OK}, nil +} + func (BaseApplication) FinalizeBlock(_ context.Context, req *RequestFinalizeBlock) (*ResponseFinalizeBlock, error) { txs := make([]*ExecTxResult, len(req.Txs)) for i := range req.Txs { diff --git a/abci/types/messages.go b/abci/types/messages.go index 5f863b87b..2ba9cad66 100644 --- a/abci/types/messages.go +++ b/abci/types/messages.go @@ -105,6 +105,18 @@ func ToRequestProcessProposal(req *RequestProcessProposal) *Request { } } +func ToRequestExtendVote(req *RequestExtendVote) *Request { + return &Request{ + Value: &Request_ExtendVote{req}, + } +} + +func ToRequestVerifyVoteExtension(req *RequestVerifyVoteExtension) *Request { + return &Request{ + Value: &Request_VerifyVoteExtension{req}, + } +} + func ToRequestFinalizeBlock(req *RequestFinalizeBlock) *Request { return &Request{ Value: &Request_FinalizeBlock{req}, @@ -197,6 +209,18 @@ func ToResponseProcessProposal(res *ResponseProcessProposal) *Response { } } +func ToResponseExtendVote(res *ResponseExtendVote) *Response { + return &Response{ + Value: &Response_ExtendVote{res}, + } +} + +func ToResponseVerifyVoteExtension(res *ResponseVerifyVoteExtension) *Response { + return &Response{ + Value: &Response_VerifyVoteExtension{res}, + } +} + func ToResponseFinalizeBlock(res *ResponseFinalizeBlock) *Response { return &Response{ Value: &Response_FinalizeBlock{res}, diff --git a/consensus/msgs_test.go b/consensus/msgs_test.go index 122a2a411..fd7584af8 100644 --- a/consensus/msgs_test.go +++ b/consensus/msgs_test.go @@ -339,6 +339,11 @@ func TestConsMsgsVectors(t *testing.T) { } pbProposal := proposal.ToProto() + ext := types.VoteExtension{ + AppDataToSign: []byte("signed"), + AppDataSelfAuthenticating: []byte("auth"), + } + v := &types.Vote{ ValidatorAddress: []byte("add_more_exclamation"), ValidatorIndex: 1, @@ -347,6 +352,7 @@ func TestConsMsgsVectors(t *testing.T) { Timestamp: date, Type: tmproto.PrecommitType, BlockID: bi, + VoteExtension: ext, } vpb := v.ToProto() @@ -383,7 +389,7 @@ func TestConsMsgsVectors(t *testing.T) { "2a36080110011a3008011204746573741a26080110011a206164645f6d6f72655f6578636c616d6174696f6e5f6d61726b735f636f64652d"}, {"Vote", &tmcons.Message{Sum: &tmcons.Message_Vote{ Vote: &tmcons.Vote{Vote: vpb}}}, - "32700a6e0802100122480a206164645f6d6f72655f6578636c616d6174696f6e5f6d61726b735f636f64652d1224080112206164645f6d6f72655f6578636c616d6174696f6e5f6d61726b735f636f64652d2a0608c0b89fdc0532146164645f6d6f72655f6578636c616d6174696f6e3801"}, + "3280010a7e0802100122480a206164645f6d6f72655f6578636c616d6174696f6e5f6d61726b735f636f64652d1224080112206164645f6d6f72655f6578636c616d6174696f6e5f6d61726b735f636f64652d2a0608c0b89fdc0532146164645f6d6f72655f6578636c616d6174696f6e38014a0e0a067369676e6564120461757468"}, {"HasVote", &tmcons.Message{Sum: &tmcons.Message_HasVote{ HasVote: &tmcons.HasVote{Height: 1, Round: 1, Type: tmproto.PrevoteType, Index: 1}}}, "3a080801100118012001"}, diff --git a/consensus/state.go b/consensus/state.go index b1b64d7ef..516a28471 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -2231,6 +2231,16 @@ func (cs *State) signVote( v := vote.ToProto() err := cs.privValidator.SignVote(cs.state.ChainID, v) + + switch msgType { + case tmproto.PrecommitType: + // if the signedMessage type is for a precommit, add VoteExtension + ext, err := cs.blockExec.ExtendVote(vote) + if err != nil { + return nil, err + } + vote.VoteExtension = ext + } vote.Signature = v.Signature vote.Timestamp = v.Timestamp @@ -2259,7 +2269,11 @@ func (cs *State) voteTime() time.Time { } // sign the vote and publish on internalMsgQueue -func (cs *State) signAddVote(msgType tmproto.SignedMsgType, hash []byte, header types.PartSetHeader) *types.Vote { +func (cs *State) signAddVote( + msgType tmproto.SignedMsgType, + hash []byte, + header types.PartSetHeader, +) *types.Vote { if cs.privValidator == nil { // the node does not have a key return nil } diff --git a/privval/msgs_test.go b/privval/msgs_test.go index 9a2cebf1c..23a5d86a5 100644 --- a/privval/msgs_test.go +++ b/privval/msgs_test.go @@ -35,6 +35,10 @@ func exampleVote() *types.Vote { }, ValidatorAddress: crypto.AddressHash([]byte("validator_address")), ValidatorIndex: 56789, + VoteExtension: types.VoteExtension { + AppDataToSign: []byte("app_data_to_sign"), + AppDataSelfAuthenticating: []byte("app_data_self_authenticating"), + }, } } @@ -84,8 +88,8 @@ func TestPrivvalVectors(t *testing.T) { {"pubKey request", &privproto.PubKeyRequest{}, "0a00"}, {"pubKey response", &privproto.PubKeyResponse{PubKey: ppk, Error: nil}, "12240a220a20556a436f1218d30942efe798420f51dc9b6a311b929c578257457d05c5fcf230"}, {"pubKey response with error", &privproto.PubKeyResponse{PubKey: cryptoproto.PublicKey{}, Error: remoteError}, "12140a0012100801120c697427732061206572726f72"}, - {"Vote Request", &privproto.SignVoteRequest{Vote: votepb}, "1a760a74080110031802224a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a2a0608f49a8ded0532146af1f4111082efb388211bc72c55bcd61e9ac3d538d5bb03"}, - {"Vote Response", &privproto.SignedVoteResponse{Vote: *votepb, Error: nil}, "22760a74080110031802224a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a2a0608f49a8ded0532146af1f4111082efb388211bc72c55bcd61e9ac3d538d5bb03"}, + {"Vote Request", &privproto.SignVoteRequest{Vote: votepb}, "1aa8010aa501080110031802224a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a2a0608f49a8ded0532146af1f4111082efb388211bc72c55bcd61e9ac3d538d5bb034a2f0a0f6170705f646174615f7369676e6564121c6170705f646174615f73656c665f61757468656e7469636174696e67"}, + {"Vote Response", &privproto.SignedVoteResponse{Vote: *votepb, Error: nil}, "22a8010aa501080110031802224a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a2a0608f49a8ded0532146af1f4111082efb388211bc72c55bcd61e9ac3d538d5bb034a2f0a0f6170705f646174615f7369676e6564121c6170705f646174615f73656c665f61757468656e7469636174696e67"}, {"Vote Response with error", &privproto.SignedVoteResponse{Vote: tmproto.Vote{}, Error: remoteError}, "22250a11220212002a0b088092b8c398feffffff0112100801120c697427732061206572726f72"}, {"Proposal Request", &privproto.SignProposalRequest{Proposal: proposalpb}, "2a700a6e08011003180220022a4a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a320608f49a8ded053a10697427732061207369676e6174757265"}, {"Proposal Response", &privproto.SignedProposalResponse{Proposal: *proposalpb, Error: nil}, "32700a6e08011003180220022a4a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a320608f49a8ded053a10697427732061207369676e6174757265"}, diff --git a/proto/tendermint/abci/types.proto b/proto/tendermint/abci/types.proto index 241decdf1..cbbb46991 100644 --- a/proto/tendermint/abci/types.proto +++ b/proto/tendermint/abci/types.proto @@ -30,6 +30,8 @@ service ABCI { returns (ResponseApplySnapshotChunk); rpc PrepareProposal(RequestPrepareProposal) returns (ResponsePrepareProposal); rpc ProcessProposal(RequestProcessProposal) returns (ResponseProcessProposal); + rpc ExtendVote(RequestExtendVote) returns (ResponseExtendVote); + rpc VerifyVoteExtension(RequestVerifyVoteExtension) returns (ResponseVerifyVoteExtension); rpc FinalizeBlock(RequestFinalizeBlock) returns (ResponseFinalizeBlock); } @@ -38,20 +40,22 @@ service ABCI { message Request { oneof value { - RequestEcho echo = 1; - RequestFlush flush = 2; - RequestInfo info = 3; - RequestInitChain init_chain = 5; - RequestQuery query = 6; - RequestCheckTx check_tx = 8; - RequestCommit commit = 11; - RequestListSnapshots list_snapshots = 12; - RequestOfferSnapshot offer_snapshot = 13; - RequestLoadSnapshotChunk load_snapshot_chunk = 14; - RequestApplySnapshotChunk apply_snapshot_chunk = 15; - RequestPrepareProposal prepare_proposal = 16; - RequestProcessProposal process_proposal = 17; - RequestFinalizeBlock finalize_block = 20; + RequestEcho echo = 1; + RequestFlush flush = 2; + RequestInfo info = 3; + RequestInitChain init_chain = 5; + RequestQuery query = 6; + RequestCheckTx check_tx = 8; + RequestCommit commit = 11; + RequestListSnapshots list_snapshots = 12; + RequestOfferSnapshot offer_snapshot = 13; + RequestLoadSnapshotChunk load_snapshot_chunk = 14; + RequestApplySnapshotChunk apply_snapshot_chunk = 15; + RequestPrepareProposal prepare_proposal = 16; + RequestProcessProposal process_proposal = 17; + RequestExtendVote extend_vote = 18; + RequestVerifyVoteExtension verify_vote_extension = 19; + RequestFinalizeBlock finalize_block = 20; } reserved 4, 7, 9, 10; // SetOption, BeginBlock, DeliverTx, EndBlock } @@ -121,6 +125,16 @@ message RequestApplySnapshotChunk { string sender = 3; } +// Extends a vote with application-side injection +message RequestExtendVote { + types.Vote vote = 1; +} + +// Verify the vote extension +message RequestVerifyVoteExtension { + types.Vote vote = 1; +} + message RequestPrepareProposal { // the modified transactions cannot exceed this size. int64 max_tx_bytes = 1; @@ -167,21 +181,23 @@ message RequestFinalizeBlock { message Response { oneof value { - ResponseException exception = 1; - ResponseEcho echo = 2; - ResponseFlush flush = 3; - ResponseInfo info = 4; - ResponseInitChain init_chain = 6; - ResponseQuery query = 7; - ResponseCheckTx check_tx = 9; - ResponseCommit commit = 12; - ResponseListSnapshots list_snapshots = 13; - ResponseOfferSnapshot offer_snapshot = 14; - ResponseLoadSnapshotChunk load_snapshot_chunk = 15; - ResponseApplySnapshotChunk apply_snapshot_chunk = 16; - ResponsePrepareProposal prepare_proposal = 17; - ResponseProcessProposal process_proposal = 18; - ResponseFinalizeBlock finalize_block = 21; + ResponseException exception = 1; + ResponseEcho echo = 2; + ResponseFlush flush = 3; + ResponseInfo info = 4; + ResponseInitChain init_chain = 6; + ResponseQuery query = 7; + ResponseCheckTx check_tx = 9; + ResponseCommit commit = 12; + ResponseListSnapshots list_snapshots = 13; + ResponseOfferSnapshot offer_snapshot = 14; + ResponseLoadSnapshotChunk load_snapshot_chunk = 15; + ResponseApplySnapshotChunk apply_snapshot_chunk = 16; + ResponsePrepareProposal prepare_proposal = 17; + ResponseProcessProposal process_proposal = 18; + ResponseExtendVote extend_vote = 19; + ResponseVerifyVoteExtension verify_vote_extension = 20; + ResponseFinalizeBlock finalize_block = 21; } reserved 5, 8, 10, 11; // SetOption, BeginBlock, DeliverTx, EndBlock } @@ -285,6 +301,21 @@ message ResponseApplySnapshotChunk { } } +message ResponseExtendVote { + tendermint.types.VoteExtension vote_extension = 1; +} + +message ResponseVerifyVoteExtension { + Result result = 1; + + enum Result { + UNKNOWN = 0; // Unknown result, treat as ACCEPT by default + ACCEPT = 1; // Vote extension verified, include the vote + SLASH = 2; // Vote extension verification aborted, continue but slash validator + REJECT = 3; // Vote extension invalidated + } +} + message ResponsePrepareProposal { repeated bytes txs = 1; } diff --git a/proto/tendermint/types/canonical.proto b/proto/tendermint/types/canonical.proto index e88fd6ffe..b7a66d4d2 100644 --- a/proto/tendermint/types/canonical.proto +++ b/proto/tendermint/types/canonical.proto @@ -34,4 +34,5 @@ message CanonicalVote { CanonicalBlockID block_id = 4 [(gogoproto.customname) = "BlockID"]; google.protobuf.Timestamp timestamp = 5 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; string chain_id = 6 [(gogoproto.customname) = "ChainID"]; + VoteExtensionToSign vote_extension = 7; } diff --git a/proto/tendermint/types/types.proto b/proto/tendermint/types/types.proto index bf3bc1600..c2ec36e5d 100644 --- a/proto/tendermint/types/types.proto +++ b/proto/tendermint/types/types.proto @@ -102,6 +102,19 @@ message Vote { bytes validator_address = 6; int32 validator_index = 7; bytes signature = 8; + VoteExtension vote_extension = 9; +} + +// VoteExtension is app-defined additional information to the validator votes. +message VoteExtension { + bytes app_data_to_sign = 1; + bytes app_data_self_authenticating = 2; +} + +// VoteExtensionToSign is a subset of VoteExtension that is signed by the validators private key. +// VoteExtensionToSign is extracted from an existing VoteExtension. +message VoteExtensionToSign { + bytes app_data_to_sign = 1; } // Commit contains the evidence that a block was committed by a set of validators. @@ -119,6 +132,7 @@ message CommitSig { google.protobuf.Timestamp timestamp = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; bytes signature = 4; + VoteExtensionToSign vote_extension = 5; } message Proposal { diff --git a/proxy/app_conn.go b/proxy/app_conn.go index 7a0efef61..6a0dbf474 100644 --- a/proxy/app_conn.go +++ b/proxy/app_conn.go @@ -20,6 +20,8 @@ type AppConnConsensus interface { InitChain(context.Context, *types.RequestInitChain) (*types.ResponseInitChain, error) PrepareProposal(context.Context, *types.RequestPrepareProposal) (*types.ResponsePrepareProposal, error) ProcessProposal(context.Context, *types.RequestProcessProposal) (*types.ResponseProcessProposal, error) + ExtendVote(context.Context, *types.RequestExtendVote) (*types.ResponseExtendVote, error) + VerifyVoteExtension(context.Context, *types.RequestVerifyVoteExtension) (*types.ResponseVerifyVoteExtension, error) FinalizeBlock(context.Context, *types.RequestFinalizeBlock) (*types.ResponseFinalizeBlock, error) Commit(context.Context) (*types.ResponseCommit, error) } @@ -97,6 +99,14 @@ func (app *appConnConsensus) Commit(ctx context.Context) (*types.ResponseCommit, return app.appConn.Commit(ctx, &types.RequestCommit{}) } +func (app *appConnConsensus) ExtendVoteSync(req types.RequestExtendVote) (*types.ResponseExtendVote, error) { + return app.appConn.ExtendVoteSync(req) +} + +func (app *appConnConsensus) VerifyVoteExtensionSync(req types.RequestVerifyVoteExtension) (*types.ResponseVerifyVoteExtension, error) { + return app.appConn.VerifyVoteExtensionSync(req) +} + //------------------------------------------------ // Implements AppConnMempool (subset of abcicli.Client) diff --git a/state/execution.go b/state/execution.go index 8ef696975..8931cb142 100644 --- a/state/execution.go +++ b/state/execution.go @@ -293,6 +293,19 @@ func (blockExec *BlockExecutor) ApplyBlock( return state, nil } +func (blockExec *BlockExecutor) ExtendVote(vote *types.Vote) (types.VoteExtension, error) { + req := abci.RequestExtendVote{ + Vote: vote.ToProto(), + } + + resp, err := blockExec.proxyApp.ExtendVoteSync(req) + if err != nil { + return types.VoteExtension{}, err + } + + return types.VoteExtensionFromProto(resp.VoteExtension), nil +} + // Commit locks the mempool, runs the ABCI Commit message, and updates the // mempool. // It returns the result of calling abci.Commit (the AppHash) and the height to retain (if any). @@ -473,7 +486,7 @@ func updateState( nextVersion := state.Version - // NOTE: the AppHash has not been populated. + // NOTE: the AppHash and the VoteExtension has not been populated. // It will be filled on state.Save. return State{ Version: nextVersion, diff --git a/state/execution_test.go b/state/execution_test.go index 1c575d437..d02727d06 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -178,11 +178,15 @@ func TestFinalizeBlockValidators(t *testing.T) { commitSig0 = types.NewCommitSigForBlock( []byte("Signature1"), state.Validators.Validators[0].Address, - now) + now, + types.VoteExtensionToSign{}, + ) commitSig1 = types.NewCommitSigForBlock( []byte("Signature2"), state.Validators.Validators[1].Address, - now) + now, + types.VoteExtensionToSign{}, + ) absentSig = types.NewCommitSigAbsent() ) diff --git a/types/block.go b/types/block.go index 3677be8af..ee443acd9 100644 --- a/types/block.go +++ b/types/block.go @@ -594,19 +594,21 @@ const ( // CommitSig is a part of the Vote included in a Commit. type CommitSig struct { - BlockIDFlag BlockIDFlag `json:"block_id_flag"` - ValidatorAddress Address `json:"validator_address"` - Timestamp time.Time `json:"timestamp"` - Signature []byte `json:"signature"` + BlockIDFlag BlockIDFlag `json:"block_id_flag"` + ValidatorAddress Address `json:"validator_address"` + Timestamp time.Time `json:"timestamp"` + Signature []byte `json:"signature"` + VoteExtension VoteExtensionToSign `json:"vote_extension"` } // NewCommitSigForBlock returns new CommitSig with BlockIDFlagCommit. -func NewCommitSigForBlock(signature []byte, valAddr Address, ts time.Time) CommitSig { +func NewCommitSigForBlock(signature []byte, valAddr Address, ts time.Time, ext VoteExtensionToSign) CommitSig { return CommitSig{ BlockIDFlag: BlockIDFlagCommit, ValidatorAddress: valAddr, Timestamp: ts, Signature: signature, + VoteExtension: ext, } } @@ -639,12 +641,14 @@ func (cs CommitSig) Absent() bool { // 1. first 6 bytes of signature // 2. first 6 bytes of validator address // 3. block ID flag -// 4. timestamp +// 4. first 6 bytes of the vote extension +// 5. timestamp func (cs CommitSig) String() string { - return fmt.Sprintf("CommitSig{%X by %X on %v @ %s}", + return fmt.Sprintf("CommitSig{%X by %X on %v with %X @ %s}", tmbytes.Fingerprint(cs.Signature), tmbytes.Fingerprint(cs.ValidatorAddress), cs.BlockIDFlag, + tmbytes.Fingerprint(cs.VoteExtension.BytesPacked()), CanonicalTime(cs.Timestamp)) } @@ -793,6 +797,7 @@ func (commit *Commit) GetVote(valIdx int32) *Vote { ValidatorAddress: commitSig.ValidatorAddress, ValidatorIndex: valIdx, Signature: commitSig.Signature, + VoteExtension: commitSig.VoteExtension.ToVoteExtension(), } } diff --git a/types/canonical.go b/types/canonical.go index 49d98405d..04fd78628 100644 --- a/types/canonical.go +++ b/types/canonical.go @@ -51,16 +51,26 @@ func CanonicalizeProposal(chainID string, proposal *tmproto.Proposal) tmproto.Ca } } +func GetVoteExtensionToSign(ext *tmproto.VoteExtension) *tmproto.VoteExtensionToSign { + if ext == nil { + return nil + } + return &tmproto.VoteExtensionToSign{ + AppDataToSign: ext.AppDataToSign, + } +} + // CanonicalizeVote transforms the given Vote to a CanonicalVote, which does // not contain ValidatorIndex and ValidatorAddress fields. func CanonicalizeVote(chainID string, vote *tmproto.Vote) tmproto.CanonicalVote { return tmproto.CanonicalVote{ - Type: vote.Type, - Height: vote.Height, // encoded as sfixed64 - Round: int64(vote.Round), // encoded as sfixed64 - BlockID: CanonicalizeBlockID(vote.BlockID), - Timestamp: vote.Timestamp, - ChainID: chainID, + Type: vote.Type, + Height: vote.Height, // encoded as sfixed64 + Round: int64(vote.Round), // encoded as sfixed64 + BlockID: CanonicalizeBlockID(vote.BlockID), + Timestamp: vote.Timestamp, + ChainID: chainID, + VoteExtension: GetVoteExtensionToSign(vote.VoteExtension), } } diff --git a/types/vote.go b/types/vote.go index 02b3cad3d..7cd206fc4 100644 --- a/types/vote.go +++ b/types/vote.go @@ -24,6 +24,7 @@ var ( ErrVoteInvalidBlockHash = errors.New("invalid block hash") ErrVoteNonDeterministicSignature = errors.New("non-deterministic signature") ErrVoteNil = errors.New("nil vote") + ErrVoteInvalidExtension = errors.New("invalid vote extension") ) type ErrVoteConflictingVotes struct { @@ -45,6 +46,52 @@ func NewConflictingVoteError(vote1, vote2 *Vote) *ErrVoteConflictingVotes { // Address is hex bytes. type Address = crypto.Address +// VoteExtensionToSign is a subset of VoteExtension +// that is signed by the validators private key +type VoteExtensionToSign struct { + AppDataToSign []byte `json:"app_data_to_sign"` +} + +// BytesPacked returns a bytes-packed representation for +// debugging and human identification. This function should +// not be used for any logical operations. +func (ext VoteExtensionToSign) BytesPacked() []byte { + res := make([]byte, len(ext.AppDataToSign)) + copy(res, ext.AppDataToSign) + return res +} + +// ToVoteExtension constructs a VoteExtension from a VoteExtensionToSign +func (ext VoteExtensionToSign) ToVoteExtension() VoteExtension { + return VoteExtension{ + AppDataToSign: ext.AppDataToSign, + } +} + +// VoteExtension is a set of data provided by the application +// that is additionally included in the vote +type VoteExtension struct { + AppDataToSign []byte `json:"app_data_to_sign"` + AppDataSelfAuthenticating []byte `json:"app_data_self_authenticating"` +} + +// ToSign constructs a VoteExtensionToSign from a VoteExtenstion +func (ext VoteExtension) ToSign() VoteExtensionToSign { + return VoteExtensionToSign{ + AppDataToSign: ext.AppDataToSign, + } +} + +// BytesPacked returns a bytes-packed representation for +// debugging and human identification. This function should +// not be used for any logical operations. +func (ext VoteExtension) BytesPacked() []byte { + res := make([]byte, len(ext.AppDataToSign)+len(ext.AppDataSelfAuthenticating)) + copy(res[:len(ext.AppDataToSign)], ext.AppDataToSign) + copy(res[len(ext.AppDataToSign):], ext.AppDataSelfAuthenticating) + return res +} + // Vote represents a prevote, precommit, or commit vote from validators for // consensus. type Vote struct { @@ -56,6 +103,7 @@ type Vote struct { ValidatorAddress Address `json:"validator_address"` ValidatorIndex int32 `json:"validator_index"` Signature []byte `json:"signature"` + VoteExtension VoteExtension `json:"vote_extension"` } // CommitSig converts the Vote to a CommitSig. @@ -79,6 +127,7 @@ func (vote *Vote) CommitSig() CommitSig { ValidatorAddress: vote.ValidatorAddress, Timestamp: vote.Timestamp, Signature: vote.Signature, + VoteExtension: vote.VoteExtension.ToSign(), } } @@ -102,6 +151,7 @@ func VoteSignBytes(chainID string, vote *tmproto.Vote) []byte { func (vote *Vote) Copy() *Vote { voteCopy := *vote + voteCopy.VoteExtension = vote.VoteExtension.Copy() return &voteCopy } @@ -115,7 +165,8 @@ func (vote *Vote) Copy() *Vote { // 6. type string // 7. first 6 bytes of block hash // 8. first 6 bytes of signature -// 9. timestamp +// 9. first 6 bytes of vote extension +// 10. timestamp func (vote *Vote) String() string { if vote == nil { return nilVoteStr @@ -131,7 +182,7 @@ func (vote *Vote) String() string { panic("Unknown vote type") } - return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %X %X @ %s}", + return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %X %X %X @ %s}", vote.ValidatorIndex, tmbytes.Fingerprint(vote.ValidatorAddress), vote.Height, @@ -140,6 +191,7 @@ func (vote *Vote) String() string { typeString, tmbytes.Fingerprint(vote.BlockID.Hash), tmbytes.Fingerprint(vote.Signature), + tmbytes.Fingerprint(vote.VoteExtension.BytesPacked()), CanonicalTime(vote.Timestamp), ) } @@ -198,9 +250,42 @@ func (vote *Vote) ValidateBasic() error { return fmt.Errorf("signature is too big (max: %d)", MaxSignatureSize) } + // XXX: add length verification for vote extension? + return nil } +func (ext VoteExtension) Copy() VoteExtension { + res := VoteExtension{ + AppDataToSign: make([]byte, len(ext.AppDataToSign)), + AppDataSelfAuthenticating: make([]byte, len(ext.AppDataSelfAuthenticating)), + } + copy(res.AppDataToSign, ext.AppDataToSign) + copy(res.AppDataSelfAuthenticating, ext.AppDataSelfAuthenticating) + return res +} + +func (ext VoteExtension) IsEmpty() bool { + if len(ext.AppDataToSign) != 0 { + return false + } + if len(ext.AppDataSelfAuthenticating) != 0 { + return false + } + return true +} + +func (ext VoteExtension) ToProto() *tmproto.VoteExtension { + if ext.IsEmpty() { + return nil + } + + return &tmproto.VoteExtension{ + AppDataToSign: ext.AppDataToSign, + AppDataSelfAuthenticating: ext.AppDataSelfAuthenticating, + } +} + // ToProto converts the handwritten type to proto generated type // return type, nil if everything converts safely, otherwise nil, error func (vote *Vote) ToProto() *tmproto.Vote { @@ -217,6 +302,7 @@ func (vote *Vote) ToProto() *tmproto.Vote { ValidatorAddress: vote.ValidatorAddress, ValidatorIndex: vote.ValidatorIndex, Signature: vote.Signature, + VoteExtension: vote.VoteExtension.ToProto(), } } @@ -236,6 +322,15 @@ func VotesToProto(votes []*Vote) []*tmproto.Vote { return res } +func VoteExtensionFromProto(pext *tmproto.VoteExtension) VoteExtension { + ext := VoteExtension{} + if pext != nil { + ext.AppDataToSign = pext.AppDataToSign + ext.AppDataSelfAuthenticating = pext.AppDataSelfAuthenticating + } + return ext +} + // FromProto converts a proto generetad type to a handwritten type // return type, nil if everything converts safely, otherwise nil, error func VoteFromProto(pv *tmproto.Vote) (*Vote, error) { @@ -257,6 +352,7 @@ func VoteFromProto(pv *tmproto.Vote) (*Vote, error) { vote.ValidatorAddress = pv.ValidatorAddress vote.ValidatorIndex = pv.ValidatorIndex vote.Signature = pv.Signature + vote.VoteExtension = VoteExtensionFromProto(pv.VoteExtension) return vote, vote.ValidateBasic() } diff --git a/types/vote_test.go b/types/vote_test.go index 927b9abc5..ae7dc518a 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -127,6 +127,33 @@ func TestVoteSignBytesTestVectors(t *testing.T) { 0x32, 0xd, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64}, // chainID }, + // containing vote extension + 5: { + "test_chain_id", &Vote{Height: 1, Round: 1, VoteExtension: VoteExtension{ + AppDataToSign: []byte("signed"), + AppDataSelfAuthenticating: []byte("auth"), + }}, + []byte{ + 0x38, // length + 0x11, // (field_number << 3) | wire_type + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // height + 0x19, // (field_number << 3) | wire_type + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // round + // remaning fields: + 0x2a, // (field_number << 3) | wire_type + 0xb, 0x8, 0x80, 0x92, 0xb8, 0xc3, 0x98, 0xfe, 0xff, 0xff, 0xff, 0x1, // timestamp + // (field_number << 3) | wire_type + 0x32, + 0xd, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, // chainID + // (field_number << 3) | wire_type + 0x3a, + 0x8, // length + 0xa, // (field_number << 3) | wire_type + 0x6, // length + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, // AppDataSigned + // SelfAuthenticating data is excluded on signing + }, // chainID + }, } for i, tc := range tests { v := tc.vote.ToProto() @@ -219,13 +246,13 @@ func TestVoteVerify(t *testing.T) { func TestVoteString(t *testing.T) { str := examplePrecommit().String() - expected := `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PRECOMMIT(Precommit) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests + expected := `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PRECOMMIT(Precommit) 8B01023386C3 000000000000 000000000000 @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests if str != expected { t.Errorf("got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str) } str2 := examplePrevote().String() - expected = `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PREVOTE(Prevote) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests + expected = `Vote{56789:6AF1F4111082 12345/02/SIGNED_MSG_TYPE_PREVOTE(Prevote) 8B01023386C3 000000000000 000000000000 @ 2017-12-25T03:00:01.234Z}` //nolint:lll //ignore line length for tests if str2 != expected { t.Errorf("got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str2) }