tenderdash_abci/
signatures.rs

1//! Digital signature processing
2//!
3//! This module contains code for processing digital signatures, including
4//! calculating message hash to be signed, and calculating signature digest.
5//!
6//! The code in this module is based on Tenderdash implementation.
7//!
8//! Two main traits are defined:
9//! - [Signable] - for objects that can be signed/verified by Tenderdash.
10//! - [Hashable] - for objects that can be serialized and hashed by Tenderdash.
11//!
12//! All [Signable] objects are also [Hashable], but not vice versa.
13//! For example, [StateId] is [Hashable], but not [Signable], as it is only
14//! part of some other signed objects.
15//!
16//! When signing or verifying signature, use [Signable::calculate_sign_hash] to
17//! calculate signature digest and provide it as a digest directly to the
18//! signature or verification function.
19
20use std::{
21    string::{String, ToString},
22    vec::Vec,
23};
24
25use bytes::BufMut;
26
27use crate::{
28    Error,
29    merkle::merkle_hash,
30    proto::{
31        prost::Message,
32        types::{
33            BlockId, CanonicalBlockId, CanonicalVote, CanonicalVoteExtension, Commit,
34            SignedMsgType, StateId, ValidatorSet, Vote, VoteExtension, VoteExtensionType,
35        },
36    },
37};
38
39const VOTE_REQUEST_ID_PREFIX: &str = "dpbvote";
40const VOTE_EXTENSION_REQUEST_ID_PREFIX: &str = "dpevote";
41
42/// Object that can be signed/verified by Tenderdash.
43pub trait Signable: Hashable {
44    #[deprecated = "replaced by calculate_sign_hash() to unify naming between core, platform and tenderdash"]
45    fn sign_digest(
46        &self,
47        chain_id: &str,
48        quorum_type: u8,
49        quorum_hash: &[u8; 32],
50        height: i64,
51        round: i32,
52    ) -> Result<Vec<u8>, Error> {
53        self.calculate_sign_hash(chain_id, quorum_type, quorum_hash, height, round)
54    }
55
56    /// Returns message hash that should be provided directly to a
57    /// signing/verification function.
58    fn calculate_sign_hash(
59        &self,
60        chain_id: &str,
61        quorum_type: u8,
62        quorum_hash: &[u8; 32],
63        height: i64,
64        round: i32,
65    ) -> Result<Vec<u8>, Error>;
66}
67
68impl Signable for Commit {
69    fn calculate_sign_hash(
70        &self,
71        chain_id: &str,
72        quorum_type: u8,
73        quorum_hash: &[u8; 32],
74
75        height: i64,
76        round: i32,
77    ) -> Result<Vec<u8>, Error> {
78        if self.quorum_hash.ne(quorum_hash) {
79            return Err(Error::Canonical("quorum hash mismatch".to_string()));
80        }
81
82        let request_id = sign_request_id(VOTE_REQUEST_ID_PREFIX, height, round);
83        let sign_bytes_hash = self.calculate_msg_hash(chain_id, height, round)?;
84
85        let digest = sign_hash(
86            quorum_type,
87            quorum_hash,
88            request_id[..]
89                .try_into()
90                .expect("invalid request ID length"),
91            &sign_bytes_hash,
92        );
93
94        // TODO: Remove once withdrawals are stable
95        tracing::trace!(
96            digest=hex::encode(&digest),
97            ?quorum_type,
98            quorum_hash=hex::encode(quorum_hash),
99            request_id=hex::encode(request_id),
100            commit=?self, "commit digest");
101
102        Ok(digest)
103    }
104}
105
106impl Signable for CanonicalVote {
107    fn calculate_sign_hash(
108        &self,
109        chain_id: &str,
110        quorum_type: u8,
111        quorum_hash: &[u8; 32],
112
113        height: i64,
114        round: i32,
115    ) -> Result<Vec<u8>, Error> {
116        let request_id = sign_request_id(VOTE_REQUEST_ID_PREFIX, height, round);
117        let sign_bytes_hash = self.calculate_msg_hash(chain_id, height, round)?;
118
119        let digest = sign_hash(
120            quorum_type,
121            quorum_hash,
122            request_id[..]
123                .try_into()
124                .expect("invalid request ID length"),
125            &sign_bytes_hash,
126        );
127
128        // TODO: Remove once withdrawals are stable
129        tracing::trace!(
130            digest=hex::encode(&digest),
131            ?quorum_type,
132            quorum_hash=hex::encode(quorum_hash),
133            request_id=hex::encode(request_id),
134            vote=?self, "canonical vote digest");
135
136        Ok(digest)
137    }
138}
139
140impl Signable for VoteExtension {
141    fn calculate_sign_hash(
142        &self,
143        chain_id: &str,
144        quorum_type: u8,
145        quorum_hash: &[u8; 32],
146        height: i64,
147        round: i32,
148    ) -> Result<Vec<u8>, Error> {
149        let (request_id, sign_bytes_hash) = match self.r#type() {
150            VoteExtensionType::ThresholdRecover => {
151                let request_id = sign_request_id(VOTE_EXTENSION_REQUEST_ID_PREFIX, height, round);
152                let sign_bytes_hash = self.calculate_msg_hash(chain_id, height, round)?;
153
154                (request_id, sign_bytes_hash)
155            },
156
157            VoteExtensionType::ThresholdRecoverRaw => {
158                let mut sign_bytes_hash = self.extension.clone();
159                sign_bytes_hash.reverse();
160
161                let request_id = self.sign_request_id.clone().unwrap_or_default();
162                let request_id = if request_id.is_empty() {
163                    sign_request_id(VOTE_EXTENSION_REQUEST_ID_PREFIX, height, round)
164                } else {
165                    // we do double-sha256, and then reverse bytes
166                    let mut request_id = lhash::sha256(&lhash::sha256(&request_id));
167                    request_id.reverse();
168                    request_id.to_vec()
169                };
170
171                (request_id, sign_bytes_hash)
172            },
173
174            VoteExtensionType::Default => unimplemented!(
175                "vote extension of type {:?} cannot be signed",
176                self.r#type()
177            ),
178        };
179        let sign_hash = sign_hash(
180            quorum_type,
181            quorum_hash,
182            request_id[..]
183                .try_into()
184                .expect("invalid request ID length"),
185            &sign_bytes_hash,
186        );
187
188        // TODO: Remove once withdrawals are stable
189        tracing::trace!(
190            digest=hex::encode(&sign_hash),
191            ?quorum_type,
192            quorum_hash=hex::encode(quorum_hash),
193            request_id=hex::encode(request_id),
194            vote_extension=?self, "vote extension sign hash");
195
196        Ok(sign_hash)
197    }
198}
199
200fn sign_request_id(prefix: &str, height: i64, round: i32) -> Vec<u8> {
201    let mut buf: Vec<u8> = Vec::from(prefix.as_bytes());
202    buf.put_i64_le(height);
203    buf.put_i32_le(round);
204
205    lhash::sha256(&buf).to_vec()
206}
207
208fn sign_hash(
209    quorum_type: u8,
210    quorum_hash: &[u8; 32],
211    request_id: &[u8; 32],
212    sign_bytes_hash: &[u8],
213) -> Vec<u8> {
214    let mut quorum_hash = quorum_hash.to_vec();
215    quorum_hash.reverse();
216
217    let mut request_id = request_id.to_vec();
218    request_id.reverse();
219
220    let mut sign_bytes_hash = sign_bytes_hash.to_vec();
221    sign_bytes_hash.reverse();
222
223    let mut buf = Vec::<u8>::new();
224
225    buf.put_u8(quorum_type);
226    buf.append(&mut quorum_hash);
227    buf.append(&mut request_id);
228    buf.append(&mut sign_bytes_hash);
229
230    let hash = lhash::sha256(&buf);
231    // Note: In bls-signatures for go, we do double-hashing, so we need to also do
232    // it here. See: https://github.com/dashpay/bls-signatures/blob/9329803969fd325dc0d5c9029ab15669d658ed5d/go-bindings/threshold.go#L62
233    lhash::sha256(&hash).to_vec()
234}
235
236/// Calculate hash (sha256) of the data, using algorithms used by
237/// Tenderdash.
238pub trait Hashable {
239    /// Generate hash of data to sign
240    fn calculate_msg_hash(&self, chain_id: &str, height: i64, round: i32)
241    -> Result<Vec<u8>, Error>;
242}
243
244impl<T: SignBytes> Hashable for T {
245    /// Generate hash of data, to be used in signature process.
246    ///
247    /// Generates hash of the m
248    fn calculate_msg_hash(
249        &self,
250        chain_id: &str,
251        height: i64,
252        round: i32,
253    ) -> Result<Vec<u8>, Error> {
254        let sb = self.sign_bytes(chain_id, height, round)?;
255        let result = lhash::sha256(&sb);
256        Ok(Vec::from(result))
257    }
258}
259
260/// Marshals data into bytes to be used in signature process.
261///
262/// After marhaling, the bytes are hashed and then
263trait SignBytes {
264    /// Marshal into byte buffer, representing bytes to be used in signature
265    /// process.
266    ///
267    /// See also: [SignDigest].
268    fn sign_bytes(&self, chain_id: &str, height: i64, round: i32) -> Result<Vec<u8>, Error>;
269}
270
271impl SignBytes for StateId {
272    fn sign_bytes(&self, _chain_id: &str, _height: i64, _round: i32) -> Result<Vec<u8>, Error> {
273        let mut buf = Vec::new();
274        self.encode_length_delimited(&mut buf)
275            .map_err(Error::Encode)?;
276
277        Ok(buf.to_vec())
278    }
279}
280
281impl SignBytes for BlockId {
282    fn sign_bytes(&self, _chain_id: &str, _height: i64, _round: i32) -> Result<Vec<u8>, Error> {
283        // determine if block id is zero
284        if self.hash.is_empty()
285            && (self.part_set_header.is_none()
286                || self.part_set_header.as_ref().unwrap().hash.is_empty())
287            && self.state_id.is_empty()
288        {
289            return Ok(Vec::<u8>::new());
290        }
291
292        let part_set_header = self.part_set_header.clone().unwrap_or_default();
293
294        let block_id = CanonicalBlockId {
295            hash: self.hash.clone(),
296            part_set_header: Some(crate::proto::types::CanonicalPartSetHeader {
297                total: part_set_header.total,
298                hash: part_set_header.hash,
299            }),
300        };
301        let mut buf = Vec::new();
302        block_id
303            .encode_length_delimited(&mut buf)
304            .map_err(Error::Encode)?;
305
306        Ok(buf)
307    }
308}
309
310impl SignBytes for Vote {
311    fn sign_bytes(&self, chain_id: &str, height: i64, round: i32) -> Result<Vec<u8>, Error> {
312        if height != self.height || round != self.round {
313            return Err(Error::Canonical(String::from("vote height/round mismatch")));
314        }
315
316        let block_id = self
317            .block_id
318            .clone()
319            .ok_or(Error::Canonical(String::from("missing vote.block id")))?;
320
321        let block_id_hash = block_id.calculate_msg_hash(chain_id, height, round)?;
322        let state_id_hash = block_id.state_id;
323
324        let canonical = CanonicalVote {
325            block_id: block_id_hash,
326            state_id: state_id_hash,
327            chain_id: chain_id.to_string(),
328            height,
329            round: round as i64,
330            r#type: self.r#type,
331        };
332
333        canonical.sign_bytes(chain_id, height, round)
334    }
335}
336
337impl SignBytes for Commit {
338    fn sign_bytes(&self, chain_id: &str, height: i64, round: i32) -> Result<Vec<u8>, Error> {
339        if height != self.height || round != self.round {
340            return Err(Error::Canonical(String::from(
341                "commit height/round mismatch",
342            )));
343        }
344
345        let block_id = self
346            .block_id
347            .clone()
348            .ok_or(Error::Canonical(String::from("missing vote.block id")))?;
349
350        let state_id_hash = block_id.state_id.clone();
351        let block_id_hash = block_id.calculate_msg_hash(chain_id, height, round)?;
352
353        let canonical = CanonicalVote {
354            block_id: block_id_hash,
355            state_id: state_id_hash,
356            chain_id: chain_id.to_string(),
357            height,
358            round: round as i64,
359            r#type: SignedMsgType::Precommit.into(),
360        };
361
362        canonical.sign_bytes(chain_id, height, round)
363    }
364}
365
366impl SignBytes for CanonicalVote {
367    fn sign_bytes(&self, chain_id: &str, height: i64, round: i32) -> Result<Vec<u8>, Error> {
368        if height != self.height || (round as i64) != self.round {
369            return Err(Error::Canonical(String::from(
370                "commit height/round mismatch",
371            )));
372        }
373
374        // we just use some rough guesstimate of intial capacity for performance
375        let mut buf = Vec::with_capacity(100);
376
377        // Based on Tenderdash implementation in
378        // https://github.com/dashpay/tenderdash/blob/bcb623bcf002ac54b26ed1324b98116872dd0da7/proto/tendermint/types/types.go#L56
379
380        buf.put_i32_le(self.r#type().into()); // 4 bytes
381        buf.put_i64_le(height); // 8 bytes
382        buf.put_i64_le(round as i64); // 8 bytes
383
384        buf.extend(&self.block_id); // 32 bytes
385        buf.extend(&self.state_id); // 32 bytes
386        if buf.len() != 4 + 8 + 8 + 32 + 32 {
387            return Err(Error::Canonical(
388                "cannot encode sign bytes: length of input data is invalid".to_string(),
389            ));
390        }
391        buf.put(chain_id.as_bytes());
392
393        // TODO: Remove once withdrawals are stable
394        tracing::trace!(
395            sign_bytes=hex::encode(&buf),
396           height,round,
397            vote=?self, "vote/commit sign bytes calculated");
398
399        Ok(buf.to_vec())
400    }
401}
402
403impl SignBytes for VoteExtension {
404    fn sign_bytes(&self, chain_id: &str, height: i64, round: i32) -> Result<Vec<u8>, Error> {
405        match self.r#type() {
406            VoteExtensionType::ThresholdRecover => {
407                let ve = CanonicalVoteExtension {
408                    chain_id: chain_id.to_string(),
409                    extension: self.extension.clone(),
410                    height,
411                    round: round as i64,
412                    r#type: self.r#type,
413                };
414
415                Ok(ve.encode_length_delimited_to_vec())
416            },
417            VoteExtensionType::ThresholdRecoverRaw => Ok(self.extension.to_vec()),
418            _ => Err(Error::Canonical(format!(
419                "unimplemented: vote extension of type {:?} cannot be signed",
420                self.r#type()
421            ))),
422        }
423    }
424}
425
426impl Hashable for ValidatorSet {
427    /// Calculate hash of validator set.
428    ///
429    /// Calculates hash of validator set that is used in block header
430    /// [Header.validators_hash](crate::proto::types::Header::validators_hash)
431    /// and
432    /// [Header.next_validators_hash](crate::proto::types::Header::next_validators_hash) fields.
433    ///
434    /// ## Tenderdash implementation
435    ///
436    /// Original Tenderdash implementation:
437    /// ```go
438    /// bzs := make([][]byte, 2)
439    /// bzs[0] = vals.ThresholdPublicKey.Bytes()
440    /// bzs[1] = vals.QuorumHash
441    /// return merkle.HashFromByteSlices(bzs)
442    /// ```
443    ///
444    /// ## Arguments
445    ///
446    /// All arguments are ignored.
447    ///
448    /// ## Returns
449    ///
450    /// Returns hash of validator set.
451    fn calculate_msg_hash(
452        &self,
453        _chain_id: &str,
454        _height: i64,
455        _round: i32,
456    ) -> Result<Vec<u8>, Error> {
457        use tenderdash_proto::crypto::public_key::Sum::*;
458        let threshold_public_key_enum = self
459            .threshold_public_key
460            .as_ref()
461            .and_then(|key| key.sum.as_ref())
462            .ok_or(Error::Canonical("missing threshold public key".to_string()))?;
463
464        let threshold_public_key = match &threshold_public_key_enum {
465            Bls12381(pk) => pk,
466            Ed25519(pk) => pk,
467            Secp256k1(pk) => pk,
468        };
469
470        let result = merkle_hash(&[threshold_public_key, &self.quorum_hash]);
471        Ok(result.to_vec())
472    }
473}
474
475#[cfg(test)]
476pub mod tests {
477    use std::{string::ToString, vec::Vec};
478
479    use super::SignBytes;
480    use crate::{
481        proto::types::{
482            Commit, PartSetHeader, SignedMsgType, Vote, VoteExtension, VoteExtensionType,
483        },
484        signatures::{Hashable, Signable},
485    };
486
487    #[test]
488    /// Compare sign bytes for Vote with sign bytes generated by Tenderdash and
489    /// put into `expect_sign_bytes`.
490    fn vote_sign_bytes() {
491        let h = [1u8, 2, 3, 4].repeat(8);
492
493        let state_id_hash =
494            hex::decode("d7509905b5407ee72dadd93b4ae70a24ad8a7755fc677acd2b215710a05cfc47")
495                .unwrap();
496        let expect_sign_bytes = hex::decode("0100000001000000000000000200000000000000fb\
497                7c89bf010a91d50f890455582b7fed0c346e53ab33df7da0bcd85c10fa92ead7509905b5407ee72dadd93b\
498                4ae70a24ad8a7755fc677acd2b215710a05cfc47736f6d652d636861696e")
499        .unwrap();
500
501        let vote = Vote {
502            r#type: SignedMsgType::Prevote as i32,
503            height: 1,
504            round: 2,
505            block_id: Some(crate::proto::types::BlockId {
506                hash: h.clone(),
507                part_set_header: Some(PartSetHeader {
508                    total: 1,
509                    hash: h.clone(),
510                }),
511                state_id: state_id_hash,
512            }),
513            ..Default::default()
514        };
515        let chain_id = "some-chain".to_string();
516        let height = vote.height;
517        let round = vote.round;
518
519        let actual = vote.sign_bytes(&chain_id, height, round).unwrap();
520
521        assert_eq!(expect_sign_bytes, actual);
522    }
523
524    #[test]
525    fn commit_sign_bytes() {
526        let h = [1u8, 2, 3, 4].repeat(8);
527
528        let state_id_hash =
529            hex::decode("d7509905b5407ee72dadd93b4ae70a24ad8a7755fc677acd2b215710a05cfc47")
530                .unwrap();
531        let expect_sign_bytes = hex::decode("0200000001000000000000000200000000000000fb7c89bf010a91d5\
532            0f890455582b7fed0c346e53ab33df7da0bcd85c10fa92ead7509905b5407ee72dadd93b4ae70a24ad8a7755fc677acd2b215710\
533            a05cfc47736f6d652d636861696e")
534        .unwrap();
535
536        let commit = Commit {
537            height: 1,
538            round: 2,
539            block_id: Some(crate::proto::types::BlockId {
540                hash: h.clone(),
541                part_set_header: Some(PartSetHeader {
542                    total: 1,
543                    hash: h.clone(),
544                }),
545                state_id: state_id_hash,
546            }),
547            ..Default::default()
548        };
549        let chain_id = "some-chain".to_string();
550        let height = commit.height;
551        let round = commit.round;
552
553        let actual = commit.sign_bytes(&chain_id, height, round).unwrap();
554
555        assert_eq!(expect_sign_bytes, actual);
556    }
557
558    #[test]
559    fn vote_extension_threshold_sign_bytes() {
560        let ve = VoteExtension {
561            extension: Vec::from([1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8]),
562            r#type: VoteExtensionType::ThresholdRecover.into(),
563            signature: Default::default(),
564            sign_request_id: None,
565        };
566
567        let chain_id = "some-chain".to_string();
568        let height = 1;
569        let round = 2;
570
571        let expect_sign_bytes = hex::decode(
572            "2a0a080102030405060708110100000000000000190200000000000000220a736f6d652d636861696e2801",
573        )
574        .unwrap();
575
576        let actual = ve.sign_bytes(&chain_id, height, round).unwrap();
577
578        assert_eq!(expect_sign_bytes, actual);
579    }
580
581    /// test vector for threshold-raw vote extensions
582    ///
583    /// Returns expected sig hash and vote extension
584    fn ve_threshold_raw() -> ([u8; 32], VoteExtension) {
585        let ve = VoteExtension {
586            extension: [1, 2, 3, 4, 5, 6, 7, 8].repeat(4),
587            r#type: VoteExtensionType::ThresholdRecoverRaw.into(),
588            signature: Default::default(),
589            sign_request_id: Some("dpevote-someSignRequestID".as_bytes().to_vec()),
590        };
591        let expected_sign_hash: [u8; 32] = [
592            0xe, 0x88, 0x8d, 0xa8, 0x97, 0xf1, 0xc0, 0xfd, 0x6a, 0xe8, 0x3b, 0x77, 0x9b, 0x5, 0xdd,
593            0x28, 0xc, 0xe2, 0x58, 0xf6, 0x4c, 0x86, 0x1, 0x34, 0xfa, 0x4, 0x27, 0xe1, 0xaa, 0xab,
594            0x1a, 0xde,
595        ];
596
597        (expected_sign_hash, ve)
598    }
599
600    #[test]
601    fn test_ve_threshold_raw_sign_bytes() {
602        let (_, ve) = ve_threshold_raw();
603        let expected_sign_bytes = ve.extension.clone();
604
605        // chain_id, height and round are unused
606        let chain_id = String::new();
607        let height = -1;
608        let round = -1;
609
610        let actual = ve.sign_bytes(&chain_id, height, round).unwrap();
611
612        assert_eq!(expected_sign_bytes, actual);
613    }
614
615    #[test]
616    fn test_sign_digest() {
617        let quorum_hash: [u8; 32] =
618            hex::decode("6A12D9CF7091D69072E254B297AEF15997093E480FDE295E09A7DE73B31CEEDD")
619                .unwrap()
620                .try_into()
621                .unwrap();
622
623        let request_id = super::sign_request_id(super::VOTE_REQUEST_ID_PREFIX, 1001, 0);
624        let request_id = request_id[..].try_into().unwrap();
625
626        let sign_bytes_hash =
627            hex::decode("0CA3D5F42BDFED0C4FDE7E6DE0F046CC76CDA6CEE734D65E8B2EE0E375D4C57D")
628                .unwrap();
629
630        let expect_sign_hash =
631            hex::decode("DA25B746781DDF47B5D736F30B1D9D0CC86981EEC67CBE255265C4361DEF8C2E")
632                .unwrap();
633
634        let sign_hash = super::sign_hash(100, &quorum_hash, request_id, &sign_bytes_hash);
635        assert_eq!(expect_sign_hash, sign_hash); // 194,4
636    }
637
638    #[test]
639    fn test_ve_threshold_raw_sign_digest() {
640        const QUORUM_TYPE: u8 = 106;
641        let quorum_hash: [u8; 32] = [8u8, 7, 6, 5, 4, 3, 2, 1]
642            .repeat(4)
643            .try_into()
644            .expect("invalid quorum hash length");
645        let (expected_sign_hash, ve) = ve_threshold_raw();
646
647        // height, round, chain id are not used in sign digest for threshold-raw
648        let sign_hash = ve
649            .calculate_sign_hash("", QUORUM_TYPE, &quorum_hash, -1, -1)
650            .expect("sign digest failed");
651
652        assert_eq!(sign_hash, expected_sign_hash);
653    }
654
655    #[test]
656    fn test_validator_set_hash() {
657        use crate::proto::crypto::{PublicKey, public_key::Sum::Bls12381};
658
659        const QUORUM_HASH_HEX: &str =
660            "703ee5bfc78765cc9e151d8dd84e30e196ababa83ac6cbdee31a88a46bba81b9";
661        const THRESHOLD_PUB_KEY_HEX: &str = "830e45e45e6414d9d615473cc2814e6b171c508f9c77e8b16924b74594f61c9956a6fa16335e98467eac8d8bdb76d187";
662        const VALIDATORS_HASH_HEX: &str =
663            "81742F95E99EAE96ABC727FE792CECB4996205DE6BFC88AFEE1F60B96BC648B2";
664
665        let pubkey_vec = hex::decode(THRESHOLD_PUB_KEY_HEX).unwrap();
666        let quorum_hash = hex::decode(QUORUM_HASH_HEX).unwrap();
667        let expected_validators_hash = hex::decode(VALIDATORS_HASH_HEX).unwrap();
668
669        let threshold_public_key = PublicKey {
670            sum: Some(Bls12381(pubkey_vec)),
671        };
672
673        let vs = crate::proto::types::ValidatorSet {
674            threshold_public_key: Some(threshold_public_key),
675            quorum_hash,
676            ..Default::default()
677        };
678
679        // chain_id, height and round are unused
680        let actual = vs.calculate_msg_hash("", 0, 0).unwrap();
681
682        assert_eq!(expected_validators_hash, actual,);
683    }
684}