Skip to main content

tari_sidechain/
commit_proof.rs

1// Copyright 2024 The Tari Project
2// SPDX-License-Identifier: BSD-3-Clause
3
4use std::fmt::Display;
5
6use borsh::{BorshDeserialize, BorshSerialize};
7use serde::{Deserialize, Serialize};
8use tari_common_types::{
9    epoch::VnEpoch,
10    types::{CompressedPublicKey, FixedHash, PrivateKey, UncompressedPublicKey},
11};
12use tari_crypto::signatures::CompressedSchnorrSignature;
13use tari_hashing::{layer2, ValidatorNodeHashDomain};
14use tari_jellyfish::{LeafKey, SparseMerkleProofExt, TreeHash};
15use tari_utilities::ByteArray;
16
17use super::error::SidechainProofValidationError;
18use crate::{
19    command::{Command, ToCommand},
20    serde::hex_or_bytes,
21    shard_group::ShardGroup,
22    validations::check_proof_elements,
23};
24
25pub type ValidatorBlockSignature =
26    CompressedSchnorrSignature<UncompressedPublicKey, PrivateKey, ValidatorNodeHashDomain>;
27pub type CheckVnFunc<'a> = dyn Fn(&CompressedPublicKey) -> Result<bool, SidechainProofValidationError> + 'a;
28
29#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
30pub enum CommandCommitProof<C> {
31    V1(CommandCommitProofV1<C>),
32}
33
34impl<C: ToCommand> CommandCommitProof<C> {
35    pub fn new(command: C, commit_proof: SidechainBlockCommitProof, inclusion_proof: SparseMerkleProofExt) -> Self {
36        Self::V1(CommandCommitProofV1 {
37            command,
38            commit_proof,
39            inclusion_proof,
40        })
41    }
42
43    pub fn command(&self) -> &C {
44        match self {
45            CommandCommitProof::V1(v1) => &v1.command,
46        }
47    }
48
49    pub fn header(&self) -> &SidechainBlockHeader {
50        match self {
51            CommandCommitProof::V1(v1) => &v1.commit_proof.header,
52        }
53    }
54
55    pub fn epoch(&self) -> VnEpoch {
56        match self {
57            CommandCommitProof::V1(v1) => VnEpoch(v1.commit_proof.header().epoch),
58        }
59    }
60
61    pub fn shard_group(&self) -> ShardGroup {
62        match self {
63            CommandCommitProof::V1(v1) => v1.commit_proof.header().shard_group,
64        }
65    }
66
67    pub fn validate_committed(
68        &self,
69        quorum_threshold: usize,
70        check_vn: &CheckVnFunc<'_>,
71    ) -> Result<(), SidechainProofValidationError> {
72        #[allow(clippy::single_match)]
73        match self {
74            CommandCommitProof::V1(v1) => v1.validate_committed(quorum_threshold, check_vn),
75        }
76    }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
80pub struct CommandCommitProofV1<C> {
81    pub command: C,
82    pub commit_proof: SidechainBlockCommitProof,
83    pub inclusion_proof: SparseMerkleProofExt,
84}
85
86impl<C: ToCommand> CommandCommitProofV1<C> {
87    pub fn command(&self) -> &C {
88        &self.command
89    }
90
91    pub fn commit_proof(&self) -> &SidechainBlockCommitProof {
92        &self.commit_proof
93    }
94
95    pub fn inclusion_proof(&self) -> &SparseMerkleProofExt {
96        &self.inclusion_proof
97    }
98
99    fn validate_inclusion_proof(&self, command: &Command) -> Result<(), SidechainProofValidationError> {
100        let command_hash = TreeHash::new(command.hash().into_array());
101        // Command JMT uses an identity mapping between hashes and keys.
102        let key = LeafKey::new(command_hash);
103        let root_hash = TreeHash::new(self.commit_proof.header.command_merkle_root.into_array());
104        self.inclusion_proof.verify_inclusion(&root_hash, &key, &command_hash)?;
105        Ok(())
106    }
107
108    pub fn validate_committed(
109        &self,
110        quorum_threshold: usize,
111        check_vn: &CheckVnFunc<'_>,
112    ) -> Result<(), SidechainProofValidationError> {
113        let command = self.command.to_command();
114        self.validate_inclusion_proof(&command)?;
115        self.commit_proof.validate_committed(quorum_threshold, check_vn)
116    }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
120pub struct SidechainBlockCommitProof {
121    pub header: SidechainBlockHeader,
122    pub proof_elements: Vec<CommitProofElement>,
123}
124
125impl SidechainBlockCommitProof {
126    pub fn validate_committed(
127        &self,
128        quorum_threshold: usize,
129        check_vn: &CheckVnFunc<'_>,
130    ) -> Result<(), SidechainProofValidationError> {
131        check_proof_elements(
132            &self.header,
133            &self.proof_elements,
134            check_vn,
135            QuorumDecision::Accept,
136            quorum_threshold,
137        )?;
138
139        Ok(())
140    }
141
142    pub fn proof_elements(&self) -> &[CommitProofElement] {
143        &self.proof_elements
144    }
145
146    pub fn header(&self) -> &SidechainBlockHeader {
147        &self.header
148    }
149
150    pub fn last_qc(&self) -> Option<&QuorumCertificate> {
151        self.proof_elements
152            .iter()
153            .filter_map(|elem| match elem {
154                CommitProofElement::QuorumCertificate(qc) => Some(qc),
155                _ => None,
156            })
157            .next_back()
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
162pub enum CommitProofElement {
163    QuorumCertificate(QuorumCertificate),
164    ChainLinks(Vec<ChainLink>),
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
168pub struct ChainLink {
169    #[serde(with = "hex_or_bytes")]
170    pub header_hash: FixedHash,
171    #[serde(with = "hex_or_bytes")]
172    pub parent_id: FixedHash,
173}
174
175impl ChainLink {
176    pub fn calc_block_id(&self) -> FixedHash {
177        layer2::block_hasher()
178            .chain(&self.parent_id)
179            .chain(&self.header_hash)
180            .finalize()
181            .into()
182    }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
186pub struct SidechainBlockHeader {
187    pub network: u8,
188    #[serde(with = "hex_or_bytes")]
189    pub parent_id: FixedHash,
190    #[serde(with = "hex_or_bytes")]
191    pub justify_id: FixedHash,
192    pub height: u64,
193    pub epoch: u64,
194    pub shard_group: ShardGroup,
195    pub proposed_by: CompressedPublicKey,
196    #[serde(with = "hex_or_bytes")]
197    pub state_merkle_root: FixedHash,
198    #[serde(with = "hex_or_bytes")]
199    pub command_merkle_root: FixedHash,
200    /// Signature of block by the proposer.
201    pub signature: ValidatorBlockSignature,
202    pub accumulated_data: ShardGroupAccumulatedData,
203    #[serde(with = "hex_or_bytes")]
204    pub metadata_hash: FixedHash,
205}
206
207impl SidechainBlockHeader {
208    pub fn calculate_hash(&self) -> FixedHash {
209        let fields = BlockHeaderHashFields::V1(BlockHeaderHashFieldsV1 {
210            network: self.network,
211            justify_id: &self.justify_id,
212            height: self.height,
213            epoch: self.epoch,
214            shard_group: self.shard_group,
215            proposed_by: self.proposed_by.as_bytes(),
216            state_merkle_root: &self.state_merkle_root,
217            command_merkle_root: &self.command_merkle_root,
218            accumulated_data: &self.accumulated_data,
219            metadata_hash: &self.metadata_hash,
220        });
221
222        layer2::block_hasher().chain(&fields).finalize().into()
223    }
224
225    pub fn calculate_block_id(&self) -> FixedHash {
226        let header_hash = self.calculate_hash();
227        layer2::block_hasher()
228            .chain(&self.parent_id)
229            .chain(&header_hash)
230            .finalize()
231            .into()
232    }
233
234    pub fn signature(&self) -> &ValidatorBlockSignature {
235        &self.signature
236    }
237
238    pub fn accumulated_data(&self) -> &ShardGroupAccumulatedData {
239        &self.accumulated_data
240    }
241}
242
243#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
244pub struct ShardGroupAccumulatedData {
245    pub total_exhaust_burn: u128,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
249pub struct QuorumCertificate {
250    #[serde(with = "hex_or_bytes")]
251    pub header_hash: FixedHash,
252    #[serde(with = "hex_or_bytes")]
253    pub parent_id: FixedHash,
254    pub signatures: Vec<ValidatorQcSignature>,
255    pub decision: QuorumDecision,
256}
257
258impl QuorumCertificate {
259    pub fn calculate_justified_block(&self) -> FixedHash {
260        layer2::block_hasher()
261            .chain(&self.parent_id)
262            .chain(&self.header_hash)
263            .finalize()
264            .into()
265    }
266}
267
268#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
269pub enum QuorumDecision {
270    Accept,
271    Reject,
272}
273
274impl QuorumDecision {
275    pub fn is_accept(&self) -> bool {
276        matches!(self, QuorumDecision::Accept)
277    }
278
279    pub fn is_reject(&self) -> bool {
280        matches!(self, QuorumDecision::Reject)
281    }
282}
283
284impl QuorumDecision {
285    pub fn as_u8(&self) -> u8 {
286        match self {
287            QuorumDecision::Accept => 0,
288            QuorumDecision::Reject => 1,
289        }
290    }
291
292    pub fn from_u8(v: u8) -> Option<Self> {
293        match v {
294            0 => Some(QuorumDecision::Accept),
295            1 => Some(QuorumDecision::Reject),
296            _ => None,
297        }
298    }
299}
300
301impl TryFrom<u8> for QuorumDecision {
302    type Error = InvalidQuorumDecisionByteError;
303
304    fn try_from(value: u8) -> Result<Self, Self::Error> {
305        Self::from_u8(value).ok_or(InvalidQuorumDecisionByteError(value))
306    }
307}
308
309impl Display for QuorumDecision {
310    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311        match self {
312            QuorumDecision::Accept => write!(f, "Accept"),
313            QuorumDecision::Reject => write!(f, "Reject"),
314        }
315    }
316}
317
318#[derive(Debug, thiserror::Error)]
319#[error("Invalid quorum decision byte: {0}")]
320pub struct InvalidQuorumDecisionByteError(u8);
321
322#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
323pub struct ValidatorQcSignature {
324    pub public_key: CompressedPublicKey,
325    pub signature: ValidatorBlockSignature,
326}
327
328impl ValidatorQcSignature {
329    #[must_use]
330    pub fn verify(&self, block_id: &FixedHash, decision: QuorumDecision) -> bool {
331        let Ok(public_key) = self.public_key.to_public_key() else {
332            return false;
333        };
334
335        let Ok(signature) = self.signature.to_schnorr_signature() else {
336            return false;
337        };
338
339        let fields = ProposalCertificateSignatureFields { block_id, decision };
340
341        let message = layer2::proposal_vote_signature_hasher().chain(&fields).finalize();
342        signature.verify(&public_key, message)
343    }
344
345    pub fn public_key(&self) -> &CompressedPublicKey {
346        &self.public_key
347    }
348
349    pub fn signature(&self) -> &ValidatorBlockSignature {
350        &self.signature
351    }
352}
353
354#[derive(Debug, BorshSerialize)]
355pub struct ProposalCertificateSignatureFields<'a> {
356    pub block_id: &'a FixedHash,
357    pub decision: QuorumDecision,
358}
359
360#[derive(Debug, BorshSerialize)]
361pub enum BlockHeaderHashFields<'a> {
362    V1(BlockHeaderHashFieldsV1<'a>),
363}
364
365#[derive(Debug, BorshSerialize)]
366pub struct BlockHeaderHashFieldsV1<'a> {
367    pub network: u8,
368    pub justify_id: &'a FixedHash,
369    pub height: u64,
370    pub epoch: u64,
371    pub shard_group: ShardGroup,
372    pub accumulated_data: &'a ShardGroupAccumulatedData,
373    // NOTE this is borsh encoded as variable length bytes - technically should always be 32
374    pub proposed_by: &'a [u8],
375    pub state_merkle_root: &'a FixedHash,
376    pub command_merkle_root: &'a FixedHash,
377    pub metadata_hash: &'a FixedHash,
378}