Tendermint uses public private key cryptography for validator signing. When a block is proposed and voted on validators sign a message representing acceptance of a block, rejection is signaled via a nil vote. These signatures are also used to verify previous blocks are correct if a node is syncing. Currently, Tendermint requires each signature to be verified individually, this leads to a slow down of block times.
Batch Verification is the process of taking many messages, keys, and signatures adding them together and verifying them all at once. The public key can be the same in which case it would mean a single user is signing many messages. In our case each public key is unique, each validator has their own and contribute a unique message. The algorithm can vary from curve to curve but the performance benefit, over single verifying messages, public keys and signatures is shared.
Adopt Batch Verification.
A new interface will be introduced. This interface will have three methods NewBatchVerifier
, Add
and VerifyBatch
.
type BatchVerifier interface {
Add(key crypto.Pubkey, signature, message []byte) error // Add appends an entry into the BatchVerifier.
Verify() bool // Verify verifies all the entries in the BatchVerifier. If the verification fails it is unknown which entry failed and each entry will need to be verified individually.
}
NewBatchVerifier
creates a new verifier. This verifier will be populated with entries to be verified.Add
adds an entry to the Verifier. Add accepts a public key and two slice of bytes (signature and message).Verify
verifies all the entires. At the end of Verify if the underlying API does not reset the Verifier to its initial state (empty), it should be done here. This prevents accidentally reusing the verifier with entries from a previous verification.Above there is mention of an entry. An entry can be constructed in many ways depending on the needs of the underlying curve. A simple approach would be:
type entry struct {
pubKey crypto.Pubkey
signature []byte
message []byte
}
The main reason this approach is being taken is to prevent simple mistakes. Some APIs allow the user to create three slices and pass them to the VerifyBatch
function but this relies on the user to safely generate all the slices (see example below). We would like to minimize the possibility of making a mistake.
func Verify(keys []crypto.Pubkey, signatures, messages[][]byte) bool
This change will not affect any users in anyway other than faster verification times.
This new api will be used for verification in both consensus and block syncing. Within the current Verify functions there will be a check to see if the key types supports the BatchVerification API. If it does it will execute batch verification, if not single signature verification will be used.
The process within consensus will be to wait for 2/3+ of the votes to be received, once they are received Verify()
will be called to batch verify all the messages. The messages that come in after 2/3+ has been verified will be individually verified.
The process for block sync & light client verification will be to verify only 2/3+ in a batch style. Since these processes are not participating in consensus there is no need to wait for more messages.
If batch verifications fails for any reason, it will not be known which entry caused the failure. Verification will need to revert to single signature verification.
Starting out, only ed25519 will support batch verification.
Implemented
Ed25519 Library Ed25519 spec Signature Aggregation for votes Proposer-based timestamps