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 {
74 return Err(ChainAnchorError::TooManyTrackedBlocks {
75 count: chain.num_tracked_blocks(),
76 max: MAX_INPUT_NOTES_PER_TX,
77 });
78 }
79
80 Ok(Self { header, chain })
81 }
82
83 pub fn block_num(&self) -> BlockNumber {
85 self.header.block_num()
86 }
87
88 pub fn block_commitment(&self) -> Word {
94 self.header.commitment()
95 }
96
97 pub fn header(&self) -> &BlockHeader {
99 &self.header
100 }
101
102 pub fn partial_blockchain(&self) -> &PartialBlockchain {
104 &self.chain
105 }
106
107 pub fn into_parts(self) -> (BlockHeader, PartialBlockchain) {
109 (self.header, self.chain)
110 }
111}
112
113impl Serializable for ChainAnchor {
114 fn write_into<W: ByteWriter>(&self, target: &mut W) {
115 self.header.write_into(target);
116 self.chain.write_into(target);
117 }
118}
119
120impl Deserializable for ChainAnchor {
121 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
122 let header = BlockHeader::read_from(source)?;
123
124 let mmr = PartialMmr::read_from(source)?;
129 let blocks = BTreeMap::<BlockNumber, BlockHeader>::read_from(source)?;
130
131 if blocks.len() > MAX_INPUT_NOTES_PER_TX {
134 return Err(DeserializationError::InvalidValue(
135 ChainAnchorError::TooManyTrackedBlocks {
136 count: blocks.len(),
137 max: MAX_INPUT_NOTES_PER_TX,
138 }
139 .to_string(),
140 ));
141 }
142
143 for (block_num, header) in &blocks {
144 if block_num != &header.block_num() {
148 return Err(DeserializationError::InvalidValue(format!(
149 "block map key {block_num} does not match the block number {} of the header it maps to",
150 header.block_num()
151 )));
152 }
153
154 mmr.open(header.block_num().as_usize())
155 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
156 }
157 let chain = PartialBlockchain::new(mmr, blocks.into_values())
158 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
159
160 Self::new(header, chain).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
161 }
162}
163
164#[derive(Debug, Error)]
168pub enum ChainAnchorError {
169 #[error(
170 "partial blockchain length {chain_length} does not match the anchor block number {block_num}"
171 )]
172 ChainLengthMismatch {
173 chain_length: BlockNumber,
174 block_num: BlockNumber,
175 },
176 #[error(
177 "partial blockchain peaks do not hash to the chain commitment of anchor block {block_num}"
178 )]
179 ChainCommitmentMismatch { block_num: BlockNumber },
180 #[error(
181 "block {block_num} is not tracked by the anchor's partial blockchain; capture the anchor with the blocks of all authenticated input notes"
182 )]
183 BlockNotTracked { block_num: BlockNumber },
184 #[error("the anchor tracks {count} blocks, more than the {max} a transaction can reference")]
185 TooManyTrackedBlocks { count: usize, max: usize },
186 #[error("transaction reference block {requested} does not match the anchor block {anchor}")]
187 ReferenceBlockMismatch {
188 requested: BlockNumber,
189 anchor: BlockNumber,
190 },
191 #[error(
192 "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"
193 )]
194 AnchoredTransactionExpired {
195 expiration: BlockNumber,
196 sync_height: BlockNumber,
197 },
198}
199
200#[cfg(test)]
204mod tests {
205 use alloc::vec::Vec;
206
207 use miden_protocol::block::BlockHeader;
208 use miden_protocol::crypto::merkle::mmr::{Mmr, PartialMmr};
209 use miden_protocol::transaction::PartialBlockchain;
210 use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
211
212 use super::{ChainAnchor, ChainAnchorError};
213
214 fn anchor_parts(chain_length: usize, tracked: &[usize]) -> (BlockHeader, PartialBlockchain) {
217 let mut mmr = Mmr::default();
218 let mut headers = Vec::with_capacity(chain_length);
219 for block_num in 0..chain_length {
220 let header = BlockHeader::mock(u32::try_from(block_num).unwrap(), None, None, &[]);
221 mmr.add(header.commitment()).unwrap();
222 headers.push(header);
223 }
224
225 let peaks = mmr.peaks();
226 let mut partial_mmr = PartialMmr::from_peaks(peaks.clone());
227 let mut tracked_headers = Vec::new();
228 for &pos in tracked {
229 partial_mmr
230 .track(pos, mmr.get(pos).unwrap(), mmr.open(pos).unwrap().merkle_path())
231 .unwrap();
232 tracked_headers.push(headers[pos].clone());
233 }
234
235 let chain = PartialBlockchain::new(partial_mmr, tracked_headers).unwrap();
236 let header = BlockHeader::mock(
237 u32::try_from(chain_length).unwrap(),
238 Some(peaks.hash_peaks()),
239 None,
240 &[],
241 );
242
243 (header, chain)
244 }
245
246 #[test]
247 fn new_accepts_a_consistent_header_and_chain() {
248 let (header, chain) = anchor_parts(8, &[3]);
249 let block_num = header.block_num();
250
251 let anchor = ChainAnchor::new(header, chain).unwrap();
252
253 assert_eq!(anchor.block_num(), block_num);
254 }
255
256 #[test]
257 fn new_rejects_a_chain_length_that_does_not_match_the_header() {
258 let (_, chain) = anchor_parts(8, &[3]);
259 let header = BlockHeader::mock(9, Some(chain.peaks().hash_peaks()), None, &[]);
261
262 let err = ChainAnchor::new(header, chain).unwrap_err();
263
264 assert!(matches!(err, ChainAnchorError::ChainLengthMismatch { .. }), "got {err:?}");
265 }
266
267 #[test]
268 fn new_rejects_peaks_that_do_not_hash_to_the_chain_commitment() {
269 let (_, chain) = anchor_parts(8, &[3]);
270 let header = BlockHeader::mock(8, None, None, &[]);
272
273 let err = ChainAnchor::new(header, chain).unwrap_err();
274
275 assert!(matches!(err, ChainAnchorError::ChainCommitmentMismatch { .. }), "got {err:?}");
276 }
277
278 #[test]
279 fn serialization_round_trips() {
280 let (header, chain) = anchor_parts(8, &[3]);
281 let anchor = ChainAnchor::new(header, chain).unwrap();
282
283 let deserialized = ChainAnchor::read_from_bytes(&anchor.to_bytes()).unwrap();
284 assert_eq!(anchor, deserialized);
285 }
286
287 #[test]
288 fn deserialization_rejects_truncated_and_garbage_input() {
289 let (header, chain) = anchor_parts(8, &[3]);
290 let bytes = ChainAnchor::new(header, chain).unwrap().to_bytes();
291
292 assert!(ChainAnchor::read_from_bytes(&bytes[..bytes.len() - 1]).is_err());
293 assert!(ChainAnchor::read_from_bytes(&[0xaa; 64]).is_err());
294 }
295
296 #[test]
299 fn deserialization_rejects_a_tracked_leaf_with_a_missing_sibling() {
300 use alloc::collections::{BTreeMap, BTreeSet};
301
302 use miden_protocol::crypto::merkle::mmr::InOrderIndex;
303
304 let mut mmr = Mmr::default();
305 let mut headers = Vec::new();
306 for block_num in 0..4u32 {
307 let header = BlockHeader::mock(block_num, None, None, &[]);
308 mmr.add(header.commitment()).unwrap();
309 headers.push(header);
310 }
311 let peaks = mmr.peaks();
312
313 let mut nodes = BTreeMap::new();
315 nodes.insert(InOrderIndex::from_leaf_pos(3), headers[3].commitment());
316
317 let partial_mmr =
320 PartialMmr::from_parts_unchecked(peaks.clone(), nodes, BTreeSet::from([3]));
321
322 let bytes = {
323 let mut buf = Vec::new();
324 let header = BlockHeader::mock(4, Some(peaks.hash_peaks()), None, &[]);
325 header.write_into(&mut buf);
326 PartialBlockchain::new_unchecked(partial_mmr, [headers[3].clone()])
327 .unwrap()
328 .write_into(&mut buf);
329 buf
330 };
331
332 assert!(ChainAnchor::read_from_bytes(&bytes).is_err());
333 }
334
335 #[test]
338 fn deserialization_rejects_a_block_key_that_disagrees_with_its_header() {
339 use alloc::collections::BTreeMap;
340
341 use miden_protocol::block::BlockNumber;
342
343 let (header, chain) = anchor_parts(8, &[3]);
345 let tracked = chain.get_block(BlockNumber::from(3u32)).unwrap().clone();
346
347 let mut blocks = BTreeMap::new();
348 blocks.insert(BlockNumber::from(0u32), tracked);
349
350 let bytes = {
351 let mut buf = Vec::new();
352 header.write_into(&mut buf);
353 chain.mmr().write_into(&mut buf);
354 blocks.write_into(&mut buf);
355 buf
356 };
357
358 let err = ChainAnchor::read_from_bytes(&bytes).unwrap_err();
359 assert!(
360 matches!(&err, DeserializationError::InvalidValue(msg) if msg.contains("does not match the block number")),
361 "got {err:?}"
362 );
363 }
364}