• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tari-project / tari / 19257987480

11 Nov 2025 07:12AM UTC coverage: 50.496% (-1.1%) from 51.608%
19257987480

push

github

web-flow
fix(sidechain)!: adds shard group accumulated data to checkpoint (#7577)

Description
---
fix(sidechain)!: adds shard group accumulated data to checkpoint

Motivation and Context
---
This PR adds accumulated data typ to the sidechain header used for epoch
checkpoints.

Related https://github.com/tari-project/tari-ootle/pull/1639

How Has This Been Tested?
---
Updated fixtures for tests

What process can a PR reviewer use to test or verify this change?
---

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [ ] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [x] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->

BREAKING CHANGE: Previous commit proofs (epoch checkpoints, etc.) are
not compatible. These are not enabled on mainnet.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added accumulated exhaust burn tracking to sidechain block headers,
enabling storage and retrieval of total exhaust burn metrics within
block validation data.

* **Chores**
* Updated protocol definitions and conversion logic to support the new
accumulated data structure.
* Refreshed test fixtures and block header schemas to accommodate
expanded metadata requirements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

1 of 20 new or added lines in 2 files covered. (5.0%)

1479 existing lines in 36 files now uncovered.

57906 of 114674 relevant lines covered (50.5%)

8050.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.44
/base_layer/sidechain/src/commit_proof.rs
1
// Copyright 2024 The Tari Project
2
// SPDX-License-Identifier: BSD-3-Clause
3

4
use std::fmt::Display;
5

6
use borsh::{BorshDeserialize, BorshSerialize};
7
use serde::{Deserialize, Serialize};
8
use tari_common_types::{
9
    epoch::VnEpoch,
10
    types::{CompressedPublicKey, FixedHash, PrivateKey, UncompressedPublicKey},
11
};
12
use tari_crypto::signatures::CompressedSchnorrSignature;
13
use tari_hashing::{layer2, ValidatorNodeHashDomain};
14
use tari_jellyfish::{LeafKey, SparseMerkleProofExt, TreeHash};
15
use tari_utilities::ByteArray;
16

17
use super::error::SidechainProofValidationError;
18
use crate::{
19
    command::{Command, ToCommand},
20
    serde::hex_or_bytes,
21
    shard_group::ShardGroup,
22
    validations::check_proof_elements,
23
};
24

25
pub type ValidatorBlockSignature =
26
    CompressedSchnorrSignature<UncompressedPublicKey, PrivateKey, ValidatorNodeHashDomain>;
27
pub type CheckVnFunc<'a> = dyn Fn(&CompressedPublicKey) -> Result<bool, SidechainProofValidationError> + 'a;
28

29
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
30
pub enum CommandCommitProof<C> {
31
    V1(CommandCommitProofV1<C>),
32
}
33

34
impl<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(
2✔
68
        &self,
2✔
69
        quorum_threshold: usize,
2✔
70
        check_vn: &CheckVnFunc<'_>,
2✔
71
    ) -> Result<(), SidechainProofValidationError> {
2✔
72
        #[allow(clippy::single_match)]
73
        match self {
2✔
74
            CommandCommitProof::V1(v1) => v1.validate_committed(quorum_threshold, check_vn),
2✔
75
        }
76
    }
2✔
77
}
78

79
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
80
pub struct CommandCommitProofV1<C> {
81
    pub command: C,
82
    pub commit_proof: SidechainBlockCommitProof,
83
    pub inclusion_proof: SparseMerkleProofExt,
84
}
85

86
impl<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> {
2✔
100
        let command_hash = TreeHash::new(command.hash().into_array());
2✔
101
        // Command JMT uses an identity mapping between hashes and keys.
102
        let key = LeafKey::new(command_hash);
2✔
103
        let root_hash = TreeHash::new(self.commit_proof.header.command_merkle_root.into_array());
2✔
104
        self.inclusion_proof.verify_inclusion(&root_hash, &key, &command_hash)?;
2✔
105
        Ok(())
2✔
106
    }
2✔
107

108
    pub fn validate_committed(
2✔
109
        &self,
2✔
110
        quorum_threshold: usize,
2✔
111
        check_vn: &CheckVnFunc<'_>,
2✔
112
    ) -> Result<(), SidechainProofValidationError> {
2✔
113
        let command = self.command.to_command();
2✔
114
        self.validate_inclusion_proof(&command)?;
2✔
115
        self.commit_proof.validate_committed(quorum_threshold, check_vn)
2✔
116
    }
2✔
117
}
118

119
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
120
pub struct SidechainBlockCommitProof {
121
    pub header: SidechainBlockHeader,
122
    pub proof_elements: Vec<CommitProofElement>,
123
}
124

125
impl SidechainBlockCommitProof {
126
    pub fn validate_committed(
3✔
127
        &self,
3✔
128
        quorum_threshold: usize,
3✔
129
        check_vn: &CheckVnFunc<'_>,
3✔
130
    ) -> Result<(), SidechainProofValidationError> {
3✔
131
        check_proof_elements(
3✔
132
            &self.header,
3✔
133
            &self.proof_elements,
3✔
134
            check_vn,
3✔
135
            QuorumDecision::Accept,
3✔
136
            quorum_threshold,
3✔
137
        )?;
1✔
138

139
        Ok(())
2✔
140
    }
3✔
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)]
×
162
pub enum CommitProofElement {
163
    QuorumCertificate(QuorumCertificate),
164
    ChainLinks(Vec<ChainLink>),
165
}
166

167
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
168
pub 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

175
impl ChainLink {
UNCOV
176
    pub fn calc_block_id(&self) -> FixedHash {
×
UNCOV
177
        layer2::block_hasher()
×
UNCOV
178
            .chain(&self.parent_id)
×
UNCOV
179
            .chain(&self.header_hash)
×
UNCOV
180
            .finalize()
×
UNCOV
181
            .into()
×
UNCOV
182
    }
×
183
}
184

185
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
186
pub 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

207
impl SidechainBlockHeader {
208
    pub fn calculate_hash(&self) -> FixedHash {
2✔
209
        let fields = BlockHeaderHashFields::V1(BlockHeaderHashFieldsV1 {
2✔
210
            network: self.network,
2✔
211
            justify_id: &self.justify_id,
2✔
212
            height: self.height,
2✔
213
            epoch: self.epoch,
2✔
214
            shard_group: self.shard_group,
2✔
215
            proposed_by: self.proposed_by.as_bytes(),
2✔
216
            state_merkle_root: &self.state_merkle_root,
2✔
217
            command_merkle_root: &self.command_merkle_root,
2✔
218
            accumulated_data: &self.accumulated_data,
2✔
219
            metadata_hash: &self.metadata_hash,
2✔
220
        });
2✔
221

222
        layer2::block_hasher().chain(&fields).finalize().into()
2✔
223
    }
2✔
224

225
    pub fn calculate_block_id(&self) -> FixedHash {
2✔
226
        let header_hash = self.calculate_hash();
2✔
227
        layer2::block_hasher()
2✔
228
            .chain(&self.parent_id)
2✔
229
            .chain(&header_hash)
2✔
230
            .finalize()
2✔
231
            .into()
2✔
232
    }
2✔
233

234
    pub fn signature(&self) -> &ValidatorBlockSignature {
×
235
        &self.signature
×
236
    }
×
237

NEW
238
    pub fn accumulated_data(&self) -> &ShardGroupAccumulatedData {
×
NEW
239
        &self.accumulated_data
×
NEW
240
    }
×
241
}
242

NEW
243
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
244
pub struct ShardGroupAccumulatedData {
245
    pub total_exhaust_burn: u128,
246
}
247

248
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
249
pub 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

258
impl QuorumCertificate {
259
    pub fn calculate_justified_block(&self) -> FixedHash {
16✔
260
        layer2::block_hasher()
16✔
261
            .chain(&self.parent_id)
16✔
262
            .chain(&self.header_hash)
16✔
263
            .finalize()
16✔
264
            .into()
16✔
265
    }
16✔
266
}
267

268
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
×
269
pub enum QuorumDecision {
270
    Accept,
271
    Reject,
272
}
273

274
impl 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

284
impl 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

301
impl 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

309
impl 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}")]
320
pub struct InvalidQuorumDecisionByteError(u8);
321

322
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
×
323
pub struct ValidatorQcSignature {
324
    pub public_key: CompressedPublicKey,
325
    pub signature: ValidatorBlockSignature,
326
}
327

328
impl ValidatorQcSignature {
329
    #[must_use]
330
    pub fn verify(&self, block_id: &FixedHash, decision: QuorumDecision) -> bool {
24✔
331
        let Ok(public_key) = self.public_key.to_public_key() else {
24✔
332
            return false;
×
333
        };
334

335
        let Ok(signature) = self.signature.to_schnorr_signature() else {
24✔
336
            return false;
×
337
        };
338

339
        let fields = ProposalCertificateSignatureFields { block_id, decision };
24✔
340

341
        let message = layer2::proposal_vote_signature_hasher().chain(&fields).finalize();
24✔
342
        signature.verify(&public_key, message)
24✔
343
    }
24✔
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)]
×
355
pub struct ProposalCertificateSignatureFields<'a> {
356
    pub block_id: &'a FixedHash,
357
    pub decision: QuorumDecision,
358
}
359

360
#[derive(Debug, BorshSerialize)]
×
361
pub enum BlockHeaderHashFields<'a> {
362
    V1(BlockHeaderHashFieldsV1<'a>),
363
}
364

365
#[derive(Debug, BorshSerialize)]
×
366
pub 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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc