ADR 024: SignBytes and validator types in privval

Context

Currently, the messages exchanged between tendermint and a (potentially remote) signer/validator, namely votes, proposals, and heartbeats, are encoded as a JSON string (e.g., via Vote.SignBytes(...)) and then signed . JSON encoding is sub-optimal for both, hardware wallets and for usage in ethereum smart contracts. Both is laid down in detail in issue#1622.

Also, there are currently no differences between sign-request and -replies. Also, there is no possibility for a remote signer to include an error code or message in case something went wrong. The messages exchanged between tendermint and a remote signer currently live in privval/socket.go and encapsulate the corresponding types in types.

Decision

  • restructure vote, proposal, and heartbeat such that their encoding is easily parseable by hardware devices and smart contracts using a binary encoding format (amino in this case)
  • split up the messages exchanged between tendermint and remote signers into requests and responses (see details below)
  • include an error type in responses

Overview

  1. +--------------+ +----------------+
  2. | | SignXRequest | |
  3. |Remote signer |<---------------------+ tendermint |
  4. | (e.g. KMS) | | |
  5. | +--------------------->| |
  6. +--------------+ SignedXReply +----------------+
  7. SignXRequest {
  8. x: X
  9. }
  10. SignedXReply {
  11. x: X
  12. sig: Signature // []byte
  13. err: Error{
  14. code: int
  15. desc: string
  16. }
  17. }

TODO: Alternatively, the type X might directly include the signature. A lot of places expect a vote with a signature and do not necessarily deal with “Replies”. Still exploring what would work best here. This would look like (exemplified using X = Vote):

  1. Vote {
  2. // all fields besides signature
  3. }
  4. SignedVote {
  5. Vote Vote
  6. Signature []byte
  7. }
  8. SignVoteRequest {
  9. Vote Vote
  10. }
  11. SignedVoteReply {
  12. Vote SignedVote
  13. Err Error
  14. }

Note: There was a related discussion around including a fingerprint of, or, the whole public-key into each sign-request to tell the signer which corresponding private-key to use to sign the message. This is particularly relevant in the context of the KMS but is currently not considered in this ADR.

Vote

As explained in issue#1622 Vote will be changed to contain the following fields (notation in protobuf-like syntax for easy readability):

  1. // vanilla protobuf / amino encoded
  2. message Vote {
  3. Version fixed32
  4. Height sfixed64
  5. Round sfixed32
  6. VoteType fixed32
  7. Timestamp Timestamp // << using protobuf definition
  8. BlockID BlockID // << as already defined
  9. ChainID string // at the end because length could vary a lot
  10. }
  11. // this is an amino registered type; like currently privval.SignVoteMsg:
  12. // registered with "tendermint/socketpv/SignVoteRequest"
  13. message SignVoteRequest {
  14. Vote vote
  15. }
  16. // amino registered type
  17. // registered with "tendermint/socketpv/SignedVoteReply"
  18. message SignedVoteReply {
  19. Vote Vote
  20. Signature Signature
  21. Err Error
  22. }
  23. // we will use this type everywhere below
  24. message Error {
  25. Type uint // error code
  26. Description string // optional description
  27. }

The ChainID gets moved into the vote message directly. Previously, it was injected using the Signable interface method SignBytes(chainID string) []byte. Also, the signature won’t be included directly, only in the corresponding SignedVoteReply message.

Proposal

  1. // vanilla protobuf / amino encoded
  2. message Proposal {
  3. Height sfixed64
  4. Round sfixed32
  5. Timestamp Timestamp // << using protobuf definition
  6. BlockPartsHeader PartSetHeader // as already defined
  7. POLRound sfixed32
  8. POLBlockID BlockID // << as already defined
  9. }
  10. // amino registered with "tendermint/socketpv/SignProposalRequest"
  11. message SignProposalRequest {
  12. Proposal proposal
  13. }
  14. // amino registered with "tendermint/socketpv/SignProposalReply"
  15. message SignProposalReply {
  16. Prop Proposal
  17. Sig Signature
  18. Err Error // as defined above
  19. }

Heartbeat

TODO: clarify if heartbeat also needs a fixed offset and update the fields accordingly:

  1. message Heartbeat {
  2. ValidatorAddress Address
  3. ValidatorIndex int
  4. Height int64
  5. Round int
  6. Sequence int
  7. }
  8. // amino registered with "tendermint/socketpv/SignHeartbeatRequest"
  9. message SignHeartbeatRequest {
  10. Hb Heartbeat
  11. }
  12. // amino registered with "tendermint/socketpv/SignHeartbeatReply"
  13. message SignHeartbeatReply {
  14. Hb Heartbeat
  15. Sig Signature
  16. Err Error // as defined above
  17. }

PubKey

TBA - this needs further thoughts: e.g. what todo like in the case of the KMS which holds several keys? How does it know with which key to reply?

SignBytes

SignBytes will not require a ChainID parameter:

  1. type Signable interface {
  2. SignBytes() []byte
  3. }

And the implementation for vote, heartbeat, proposal will look like:

  1. // type T is one of vote, sign, proposal
  2. func (tp *T) SignBytes() []byte {
  3. bz, err := cdc.MarshalBinary(tp)
  4. if err != nil {
  5. panic(err)
  6. }
  7. return bz
  8. }

Status

DRAFT

Consequences

Positive

The most relevant positive effect is that the signing bytes can easily be parsed by a hardware module and a smart contract. Besides that:

  • clearer separation between requests and responses
  • added error messages enable better error handling

Negative

  • relatively huge change / refactoring touching quite some code
  • lot’s of places assume a Vote with a signature included -> they will need to
  • need to modify some interfaces

Neutral

not even the swiss are neutral