miden_client/transaction/
chain_anchor.rs1use alloc::collections::BTreeMap;
2use alloc::format;
3use alloc::string::ToString;
4
5use miden_protocol::block::{BlockHeader, BlockNumber};
6use miden_protocol::crypto::merkle::mmr::PartialMmr;
7use miden_protocol::transaction::PartialBlockchain;
8use miden_protocol::{MAX_INPUT_NOTES_PER_TX, Word};
9use miden_tx::utils::serde::{
10 ByteReader,
11 ByteWriter,
12 Deserializable,
13 DeserializationError,
14 Serializable,
15};
16use thiserror::Error;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ChainAnchor {
46 header: BlockHeader,
47 chain: PartialBlockchain,
48}
49
50impl ChainAnchor {
51 pub fn new(header: BlockHeader, chain: PartialBlockchain) -> Result<Self, ChainAnchorError> {
59 if chain.chain_length() != header.block_num() {
60 return Err(ChainAnchorError::ChainLengthMismatch {
61 chain_length: chain.chain_length(),
62 block_num: header.block_num(),
63 });
64 }
65
66 if chain.peaks().hash_peaks() != header.chain_commitment() {
67 return Err(ChainAnchorError::ChainCommitmentMismatch {
68 block_num: header.block_num(),
69 });
70 }
71
72 if chain.num_tracked_blocks() > MAX_INPUT_NOTES_PER_TX {
75 return Err(ChainAnchorError::TooManyTrackedBlocks {
76 count: chain.num_tracked_blocks(),
77 max: MAX_INPUT_NOTES_PER_TX,
78 });
79 }
80
81 Ok(Self { header, chain })
82 }
83
84 pub fn block_num(&self) -> BlockNumber {
86 self.header.block_num()
87 }
88
89 pub fn block_commitment(&self) -> Word {
95 self.header.commitment()
96 }
97
98 pub fn header(&self) -> &BlockHeader {
100 &self.header
101 }
102
103 pub fn partial_blockchain(&self) -> &PartialBlockchain {
105 &self.chain
106 }
107
108 pub fn into_parts(self) -> (BlockHeader, PartialBlockchain) {
110 (self.header, self.chain)
111 }
112}
113
114impl Serializable for ChainAnchor {
115 fn write_into<W: ByteWriter>(&self, target: &mut W) {
116 self.header.write_into(target);
117 self.chain.write_into(target);
118 }
119}
120
121impl Deserializable for ChainAnchor {
122 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
123 let header = BlockHeader::read_from(source)?;
124
125 let mmr = PartialMmr::read_from(source)?;
130 let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
131
132 if blocks.len() > MAX_INPUT_NOTES_PER_TX {
135 return Err(DeserializationError::InvalidValue(
136 ChainAnchorError::TooManyTrackedBlocks {
137 count: blocks.len(),
138 max: MAX_INPUT_NOTES_PER_TX,
139 }
140 .to_string(),
141 ));
142 }
143
144 for (block_num, header) in &blocks {
145 if block_num != &header.block_num() {
149 return Err(DeserializationError::InvalidValue(format!(
150 "block map key {block_num} does not match the block number {} of the header it maps to",
151 header.block_num()
152 )));
153 }
154
155 mmr.open(header.block_num().as_usize())
156 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
157 }
158 let chain = PartialBlockchain::new(mmr, blocks.into_values())
159 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
160
161 Self::new(header, chain).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
162 }
163}
164
165#[derive(Debug, Error)]
169pub enum ChainAnchorError {
170 #[error(
171 "partial blockchain length {chain_length} does not match the anchor block number {block_num}"
172 )]
173 ChainLengthMismatch {
174 chain_length: BlockNumber,
175 block_num: BlockNumber,
176 },
177 #[error(
178 "partial blockchain peaks do not hash to the chain commitment of anchor block {block_num}"
179 )]
180 ChainCommitmentMismatch { block_num: BlockNumber },
181 #[error(
182 "block {block_num} is not tracked by the anchor's partial blockchain; capture the anchor with the blocks of all authenticated input notes"
183 )]
184 BlockNotTracked { block_num: BlockNumber },
185 #[error("the anchor tracks {count} blocks, more than the {max} a transaction can reference")]
186 TooManyTrackedBlocks { count: usize, max: usize },
187 #[error("transaction reference block {requested} does not match the anchor block {anchor}")]
188 ReferenceBlockMismatch {
189 requested: BlockNumber,
190 anchor: BlockNumber,
191 },
192 #[error(
193 "the anchored transaction expires at block {expiration}, which the chain has already reached (sync height {sync_height}); it would be rejected by the network, so re-capture the anchor closer to the tip or raise the request's expiration delta"
194 )]
195 AnchoredTransactionExpired {
196 expiration: BlockNumber,
197 sync_height: BlockNumber,
198 },
199}
200
201#[cfg(test)]
205mod tests {
206 use alloc::vec::Vec;
207
208 use miden_protocol::Word;
209 use miden_protocol::block::BlockHeader;
210 use miden_protocol::crypto::merkle::mmr::{Mmr, PartialMmr};
211 use miden_protocol::transaction::PartialBlockchain;
212 use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
213
214 use super::{ChainAnchor, ChainAnchorError};
215
216 fn anchor_parts(chain_length: usize, tracked: &[usize]) -> (BlockHeader, PartialBlockchain) {
219 let mut mmr = Mmr::default();
220 let mut headers = Vec::with_capacity(chain_length);
221 for block_num in 0..chain_length {
222 let header = BlockHeader::mock(
223 u32::try_from(block_num).unwrap(),
224 None,
225 None,
226 &[],
227 Word::empty(),
228 );
229 mmr.add(header.commitment()).unwrap();
230 headers.push(header);
231 }
232
233 let peaks = mmr.peaks();
234 let mut partial_mmr = PartialMmr::from_peaks(peaks.clone());
235 let mut tracked_headers = Vec::new();
236 for &pos in tracked {
237 partial_mmr
238 .track(pos, mmr.get(pos).unwrap(), mmr.open(pos).unwrap().merkle_path())
239 .unwrap();
240 tracked_headers.push(headers[pos].clone());
241 }
242
243 let chain = PartialBlockchain::new(partial_mmr, tracked_headers).unwrap();
244 let header = BlockHeader::mock(
245 u32::try_from(chain_length).unwrap(),
246 Some(peaks.hash_peaks()),
247 None,
248 &[],
249 Word::empty(),
250 );
251
252 (header, chain)
253 }
254
255 #[test]
256 fn new_accepts_a_consistent_header_and_chain() {
257 let (header, chain) = anchor_parts(8, &[3]);
258 let block_num = header.block_num();
259
260 let anchor = ChainAnchor::new(header, chain).unwrap();
261
262 assert_eq!(anchor.block_num(), block_num);
263 }
264
265 #[test]
266 fn new_rejects_a_chain_length_that_does_not_match_the_header() {
267 let (_, chain) = anchor_parts(8, &[3]);
268 let header =
270 BlockHeader::mock(9, Some(chain.peaks().hash_peaks()), None, &[], Word::empty());
271
272 let err = ChainAnchor::new(header, chain).unwrap_err();
273
274 assert!(matches!(err, ChainAnchorError::ChainLengthMismatch { .. }), "got {err:?}");
275 }
276
277 #[test]
278 fn new_rejects_peaks_that_do_not_hash_to_the_chain_commitment() {
279 let (_, chain) = anchor_parts(8, &[3]);
280 let header = BlockHeader::mock(8, None, None, &[], Word::empty());
282
283 let err = ChainAnchor::new(header, chain).unwrap_err();
284
285 assert!(matches!(err, ChainAnchorError::ChainCommitmentMismatch { .. }), "got {err:?}");
286 }
287
288 #[test]
289 fn serialization_round_trips() {
290 let (header, chain) = anchor_parts(8, &[3]);
291 let anchor = ChainAnchor::new(header, chain).unwrap();
292
293 let deserialized = ChainAnchor::read_from_bytes(&anchor.to_bytes()).unwrap();
294 assert_eq!(anchor, deserialized);
295 }
296
297 #[test]
298 fn deserialization_rejects_truncated_and_garbage_input() {
299 let (header, chain) = anchor_parts(8, &[3]);
300 let bytes = ChainAnchor::new(header, chain).unwrap().to_bytes();
301
302 assert!(ChainAnchor::read_from_bytes(&bytes[..bytes.len() - 1]).is_err());
303 assert!(ChainAnchor::read_from_bytes(&[0xaa; 64]).is_err());
304 }
305
306 #[test]
309 fn deserialization_rejects_a_tracked_leaf_with_a_missing_sibling() {
310 use alloc::collections::{BTreeMap, BTreeSet};
311
312 use miden_protocol::crypto::merkle::mmr::InOrderIndex;
313
314 let mut mmr = Mmr::default();
315 let mut headers = Vec::new();
316 for block_num in 0..4u32 {
317 let header = BlockHeader::mock(block_num, None, None, &[], Word::empty());
318 mmr.add(header.commitment()).unwrap();
319 headers.push(header);
320 }
321 let peaks = mmr.peaks();
322
323 let mut nodes = BTreeMap::new();
325 nodes.insert(InOrderIndex::from_leaf_pos(3), headers[3].commitment());
326
327 let partial_mmr =
328 PartialMmr::from_parts(peaks.clone(), nodes, BTreeSet::from([3])).unwrap();
329
330 let bytes = {
331 let mut buf = Vec::new();
332 let header = BlockHeader::mock(4, Some(peaks.hash_peaks()), None, &[], Word::empty());
333 header.write_into(&mut buf);
334 PartialBlockchain::new_unchecked(partial_mmr, [headers[3].clone()])
335 .unwrap()
336 .write_into(&mut buf);
337 buf
338 };
339
340 assert!(ChainAnchor::read_from_bytes(&bytes).is_err());
341 }
342
343 #[test]
346 fn deserialization_rejects_a_block_key_that_disagrees_with_its_header() {
347 use alloc::collections::BTreeMap;
348
349 use miden_protocol::block::BlockNumber;
350
351 let (header, chain) = anchor_parts(8, &[3]);
353 let tracked = chain.get_block(BlockNumber::from(3u32)).unwrap().clone();
354
355 let mut blocks = BTreeMap::new();
356 blocks.insert(BlockNumber::from(0u32), tracked);
357
358 let bytes = {
359 let mut buf = Vec::new();
360 header.write_into(&mut buf);
361 chain.mmr().write_into(&mut buf);
362 blocks.write_into(&mut buf);
363 buf
364 };
365
366 let err = ChainAnchor::read_from_bytes(&bytes).unwrap_err();
367 assert!(
368 matches!(&err, DeserializationError::InvalidValue(msg) if msg.contains("does not match the block number")),
369 "got {err:?}"
370 );
371 }
372}