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::block::BlockHeader;
209 use miden_protocol::crypto::merkle::mmr::{Mmr, PartialMmr};
210 use miden_protocol::transaction::PartialBlockchain;
211 use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
212
213 use super::{ChainAnchor, ChainAnchorError};
214
215 fn anchor_parts(chain_length: usize, tracked: &[usize]) -> (BlockHeader, PartialBlockchain) {
218 let mut mmr = Mmr::default();
219 let mut headers = Vec::with_capacity(chain_length);
220 for block_num in 0..chain_length {
221 let header = BlockHeader::mock(u32::try_from(block_num).unwrap(), None, None, &[]);
222 mmr.add(header.commitment()).unwrap();
223 headers.push(header);
224 }
225
226 let peaks = mmr.peaks();
227 let mut partial_mmr = PartialMmr::from_peaks(peaks.clone());
228 let mut tracked_headers = Vec::new();
229 for &pos in tracked {
230 partial_mmr
231 .track(pos, mmr.get(pos).unwrap(), mmr.open(pos).unwrap().merkle_path())
232 .unwrap();
233 tracked_headers.push(headers[pos].clone());
234 }
235
236 let chain = PartialBlockchain::new(partial_mmr, tracked_headers).unwrap();
237 let header = BlockHeader::mock(
238 u32::try_from(chain_length).unwrap(),
239 Some(peaks.hash_peaks()),
240 None,
241 &[],
242 );
243
244 (header, chain)
245 }
246
247 #[test]
248 fn new_accepts_a_consistent_header_and_chain() {
249 let (header, chain) = anchor_parts(8, &[3]);
250 let block_num = header.block_num();
251
252 let anchor = ChainAnchor::new(header, chain).unwrap();
253
254 assert_eq!(anchor.block_num(), block_num);
255 }
256
257 #[test]
258 fn new_rejects_a_chain_length_that_does_not_match_the_header() {
259 let (_, chain) = anchor_parts(8, &[3]);
260 let header = BlockHeader::mock(9, Some(chain.peaks().hash_peaks()), None, &[]);
262
263 let err = ChainAnchor::new(header, chain).unwrap_err();
264
265 assert!(matches!(err, ChainAnchorError::ChainLengthMismatch { .. }), "got {err:?}");
266 }
267
268 #[test]
269 fn new_rejects_peaks_that_do_not_hash_to_the_chain_commitment() {
270 let (_, chain) = anchor_parts(8, &[3]);
271 let header = BlockHeader::mock(8, None, None, &[]);
273
274 let err = ChainAnchor::new(header, chain).unwrap_err();
275
276 assert!(matches!(err, ChainAnchorError::ChainCommitmentMismatch { .. }), "got {err:?}");
277 }
278
279 #[test]
280 fn serialization_round_trips() {
281 let (header, chain) = anchor_parts(8, &[3]);
282 let anchor = ChainAnchor::new(header, chain).unwrap();
283
284 let deserialized = ChainAnchor::read_from_bytes(&anchor.to_bytes()).unwrap();
285 assert_eq!(anchor, deserialized);
286 }
287
288 #[test]
289 fn deserialization_rejects_truncated_and_garbage_input() {
290 let (header, chain) = anchor_parts(8, &[3]);
291 let bytes = ChainAnchor::new(header, chain).unwrap().to_bytes();
292
293 assert!(ChainAnchor::read_from_bytes(&bytes[..bytes.len() - 1]).is_err());
294 assert!(ChainAnchor::read_from_bytes(&[0xaa; 64]).is_err());
295 }
296
297 #[test]
300 fn deserialization_rejects_a_tracked_leaf_with_a_missing_sibling() {
301 use alloc::collections::{BTreeMap, BTreeSet};
302
303 use miden_protocol::crypto::merkle::mmr::InOrderIndex;
304
305 let mut mmr = Mmr::default();
306 let mut headers = Vec::new();
307 for block_num in 0..4u32 {
308 let header = BlockHeader::mock(block_num, None, None, &[]);
309 mmr.add(header.commitment()).unwrap();
310 headers.push(header);
311 }
312 let peaks = mmr.peaks();
313
314 let mut nodes = BTreeMap::new();
316 nodes.insert(InOrderIndex::from_leaf_pos(3), headers[3].commitment());
317
318 let partial_mmr =
321 PartialMmr::from_parts_unchecked(peaks.clone(), nodes, BTreeSet::from([3]));
322
323 let bytes = {
324 let mut buf = Vec::new();
325 let header = BlockHeader::mock(4, Some(peaks.hash_peaks()), None, &[]);
326 header.write_into(&mut buf);
327 PartialBlockchain::new_unchecked(partial_mmr, [headers[3].clone()])
328 .unwrap()
329 .write_into(&mut buf);
330 buf
331 };
332
333 assert!(ChainAnchor::read_from_bytes(&bytes).is_err());
334 }
335
336 #[test]
339 fn deserialization_rejects_a_block_key_that_disagrees_with_its_header() {
340 use alloc::collections::BTreeMap;
341
342 use miden_protocol::block::BlockNumber;
343
344 let (header, chain) = anchor_parts(8, &[3]);
346 let tracked = chain.get_block(BlockNumber::from(3u32)).unwrap().clone();
347
348 let mut blocks = BTreeMap::new();
349 blocks.insert(BlockNumber::from(0u32), tracked);
350
351 let bytes = {
352 let mut buf = Vec::new();
353 header.write_into(&mut buf);
354 chain.mmr().write_into(&mut buf);
355 blocks.write_into(&mut buf);
356 buf
357 };
358
359 let err = ChainAnchor::read_from_bytes(&bytes).unwrap_err();
360 assert!(
361 matches!(&err, DeserializationError::InvalidValue(msg) if msg.contains("does not match the block number")),
362 "got {err:?}"
363 );
364 }
365}