1use std::collections::{BTreeMap, BTreeSet};
20use std::fmt;
21
22use ripemd::Ripemd160;
23use serde_json::{json, Map, Value};
24use sha1::Sha1;
25use sha2::{Digest as _, Sha256};
26
27use crate::verify::receipt_digest;
28
29pub const MAGIC: &[u8] = b"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94";
30pub const MAJOR_VERSION: u8 = 1;
31
32const MAX_MSG: usize = 4096;
33const MAX_RESULT: usize = 4096;
34const MAX_ATT_PAYLOAD: usize = 8192;
35const MAX_URI: usize = 1000;
36const MAX_NODES: usize = 10_000;
37const MAX_DEPTH: usize = 256;
38const MAX_VARUINT_BYTES: usize = 10;
39
40const KECCAK_TAG: u8 = 0x67;
41const ATT_PENDING: [u8; 8] = [0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e];
42const ATT_BITCOIN: [u8; 8] = [0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01];
43const ATT_LITECOIN: [u8; 8] = [0x06, 0x86, 0x9a, 0x0d, 0x73, 0xd7, 0x1b, 0x45];
44
45const HEADER_LEN: usize = 80;
46const MERKLE_OFFSET: usize = 36;
47const TIME_OFFSET: usize = 68;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct AnchorError {
52 pub code: String,
53 pub detail: String,
54}
55
56impl AnchorError {
57 fn new(code: &str, detail: impl Into<String>) -> Self {
58 Self {
59 code: code.to_string(),
60 detail: detail.into(),
61 }
62 }
63}
64
65impl fmt::Display for AnchorError {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(formatter, "{}: {}", self.code, self.detail)
68 }
69}
70
71impl std::error::Error for AnchorError {}
72
73fn hash_op(tag: u8) -> Option<(&'static str, usize)> {
74 match tag {
75 0x02 => Some(("sha1", 20)),
76 0x03 => Some(("ripemd160", 20)),
77 0x08 => Some(("sha256", 32)),
78 _ => None,
79 }
80}
81
82fn binary_op(tag: u8) -> Option<&'static str> {
83 match tag {
84 0xF0 => Some("append"),
85 0xF1 => Some("prepend"),
86 _ => None,
87 }
88}
89
90fn unary_op(tag: u8) -> Option<&'static str> {
91 match tag {
92 0xF2 => Some("reverse"),
93 0xF3 => Some("hexlify"),
94 _ => None,
95 }
96}
97
98struct Reader<'a> {
99 data: &'a [u8],
100 pos: usize,
101}
102
103impl<'a> Reader<'a> {
104 fn new(data: &'a [u8]) -> Self {
105 Self { data, pos: 0 }
106 }
107
108 fn read(&mut self, count: usize) -> Result<&'a [u8], AnchorError> {
109 if self.pos + count > self.data.len() {
110 return Err(AnchorError::new("truncated", "proof is truncated"));
111 }
112 let out = &self.data[self.pos..self.pos + count];
113 self.pos += count;
114 Ok(out)
115 }
116
117 fn read_varuint(&mut self) -> Result<u64, AnchorError> {
118 let mut value: u64 = 0;
119 let mut shift = 0;
120 for _ in 0..MAX_VARUINT_BYTES {
121 let byte = self.read(1)?[0];
122 value |= u64::from(byte & 0x7F) << shift;
123 if byte & 0x80 == 0 {
124 return Ok(value);
125 }
126 shift += 7;
127 }
128 Err(AnchorError::new("invalid_varuint", "varuint exceeds 10 bytes"))
129 }
130
131 fn read_varbytes(&mut self, max_len: usize, min_len: usize) -> Result<&'a [u8], AnchorError> {
132 let length = usize::try_from(self.read_varuint()?).unwrap_or(usize::MAX);
133 if length > max_len {
134 return Err(AnchorError::new(
135 "invalid_length",
136 format!("varbytes length {length} exceeds {max_len}"),
137 ));
138 }
139 if length < min_len {
140 return Err(AnchorError::new(
141 "invalid_length",
142 format!("varbytes length {length} below {min_len}"),
143 ));
144 }
145 self.read(length)
146 }
147
148 fn assert_eof(&self) -> Result<(), AnchorError> {
149 if self.pos != self.data.len() {
150 return Err(AnchorError::new(
151 "trailing_data",
152 "unexpected bytes after proof node",
153 ));
154 }
155 Ok(())
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum Attestation {
162 Pending(String),
163 Bitcoin(u64),
164 Litecoin(u64),
165 Unknown { tag: String, payload: String },
166}
167
168impl Attestation {
169 #[must_use]
170 pub fn kind(&self) -> &'static str {
171 match self {
172 Self::Pending(_) => "pending",
173 Self::Bitcoin(_) => "bitcoin",
174 Self::Litecoin(_) => "litecoin",
175 Self::Unknown { .. } => "unknown",
176 }
177 }
178
179 #[must_use]
180 pub fn as_dict(&self) -> Value {
181 match self {
182 Self::Pending(uri) => json!({"kind": "pending", "value": uri}),
183 Self::Bitcoin(height) => json!({"kind": "bitcoin", "value": height}),
184 Self::Litecoin(height) => json!({"kind": "litecoin", "value": height}),
185 Self::Unknown { tag, payload } => {
186 json!({"kind": "unknown", "tag": tag, "payload": payload})
187 }
188 }
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct ParsedProof {
195 pub version: u8,
196 pub file_hash_op: String,
197 pub file_digest: Vec<u8>,
198 pub leaves: Vec<(Vec<u8>, Attestation)>,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct AnchorResult {
204 pub status: String,
205 pub code: String,
206 pub detail: String,
207 pub file_digest: String,
208 pub attestations: Vec<Value>,
209 pub confirmed: Option<Value>,
210}
211
212impl AnchorResult {
213 fn new(status: &str, code: &str, detail: impl Into<String>) -> Self {
214 Self {
215 status: status.to_string(),
216 code: code.to_string(),
217 detail: detail.into(),
218 file_digest: String::new(),
219 attestations: Vec::new(),
220 confirmed: None,
221 }
222 }
223
224 #[must_use]
225 pub fn ok(&self) -> bool {
226 self.status == "verified"
227 }
228
229 #[must_use]
230 pub fn as_dict(&self) -> Value {
231 let mut out = Map::new();
232 out.insert("status".to_string(), json!(self.status));
233 out.insert("code".to_string(), json!(self.code));
234 out.insert("detail".to_string(), json!(self.detail));
235 out.insert("file_digest".to_string(), json!(self.file_digest));
236 out.insert("attestations".to_string(), json!(self.attestations));
237 if let Some(confirmed) = &self.confirmed {
238 out.insert("confirmed".to_string(), confirmed.clone());
239 }
240 Value::Object(out)
241 }
242}
243
244fn hex_encode(bytes: &[u8]) -> String {
245 let mut out = String::with_capacity(bytes.len() * 2);
246 for byte in bytes {
247 out.push_str(&format!("{byte:02x}"));
248 }
249 out
250}
251
252fn hex_decode(text: &str) -> Option<Vec<u8>> {
253 let bytes = text.as_bytes();
254 if !bytes.len().is_multiple_of(2) {
255 return None;
256 }
257 let mut out = Vec::with_capacity(bytes.len() / 2);
258 let mut index = 0;
259 while index < bytes.len() {
260 let high = (bytes[index] as char).to_digit(16)?;
261 let low = (bytes[index + 1] as char).to_digit(16)?;
262 out.push(((high << 4) | low) as u8);
263 index += 2;
264 }
265 Some(out)
266}
267
268#[must_use]
270pub fn digest_bytes(value: &Value) -> Option<Vec<u8>> {
271 let text = value.as_str()?.trim();
272 let text = match text.split_once(':') {
273 Some(("sha256", rest)) => rest,
274 Some(_) => return None,
275 None => text,
276 };
277 let text = text.strip_prefix("0x").unwrap_or(text);
278 hex_decode(text)
279}
280
281fn apply_op(name: &str, arg: Option<&[u8]>, message: &[u8]) -> Result<Vec<u8>, AnchorError> {
282 if message.len() > MAX_MSG {
283 return Err(AnchorError::new(
284 "message_too_long",
285 format!("operation input exceeds {MAX_MSG} bytes"),
286 ));
287 }
288 let result = match name {
289 "append" => {
290 let mut out = message.to_vec();
291 out.extend_from_slice(arg.unwrap_or(&[]));
292 out
293 }
294 "prepend" => {
295 let mut out = arg.unwrap_or(&[]).to_vec();
296 out.extend_from_slice(message);
297 out
298 }
299 "reverse" => message.iter().rev().copied().collect(),
300 "hexlify" => hex_encode(message).into_bytes(),
301 "sha1" => Sha1::digest(message).to_vec(),
302 "ripemd160" => Ripemd160::digest(message).to_vec(),
303 "sha256" => Sha256::digest(message).to_vec(),
304 other => {
305 return Err(AnchorError::new(
306 "unsupported_op",
307 format!("hash {other} unavailable"),
308 ))
309 }
310 };
311 if result.is_empty() || result.len() > MAX_RESULT {
312 return Err(AnchorError::new(
313 "invalid_result",
314 format!("{name} produced {} bytes", result.len()),
315 ));
316 }
317 Ok(result)
318}
319
320fn parse_attestation(payload_tag: &[u8], payload: &[u8]) -> Result<Attestation, AnchorError> {
321 let mut reader = Reader::new(payload);
322 if payload_tag == ATT_PENDING.as_slice() {
323 let uri = reader.read_varbytes(MAX_URI, 0)?;
324 reader.assert_eof()?;
325 return Ok(Attestation::Pending(
326 String::from_utf8_lossy(uri).into_owned(),
327 ));
328 }
329 if payload_tag == ATT_BITCOIN.as_slice() || payload_tag == ATT_LITECOIN.as_slice() {
330 let height = reader.read_varuint()?;
331 reader.assert_eof()?;
332 return Ok(if payload_tag == ATT_BITCOIN.as_slice() {
333 Attestation::Bitcoin(height)
334 } else {
335 Attestation::Litecoin(height)
336 });
337 }
338 Ok(Attestation::Unknown {
339 tag: hex_encode(payload_tag),
340 payload: hex_encode(payload),
341 })
342}
343
344fn handle_tag(
345 reader: &mut Reader<'_>,
346 tag: &[u8],
347 message: &[u8],
348 leaves: &mut Vec<(Vec<u8>, Attestation)>,
349 nodes: &mut usize,
350 depth: usize,
351) -> Result<(), AnchorError> {
352 if tag == [0x00] {
353 let tag8 = reader.read(8)?.to_vec();
354 let payload = reader.read_varbytes(MAX_ATT_PAYLOAD, 0)?.to_vec();
355 leaves.push((message.to_vec(), parse_attestation(&tag8, &payload)?));
356 return Ok(());
357 }
358 let tag_byte = tag[0];
359 if tag_byte == KECCAK_TAG {
360 return Err(AnchorError::new(
361 "unsupported_op",
362 "keccak256 proofs are not supported",
363 ));
364 }
365 if let Some((name, _)) = hash_op(tag_byte) {
366 let next = apply_op(name, None, message)?;
367 return parse_timestamp(reader, &next, leaves, nodes, depth + 1);
368 }
369 if let Some(name) = binary_op(tag_byte) {
370 let arg = reader.read_varbytes(MAX_RESULT, 1)?.to_vec();
371 let next = apply_op(name, Some(&arg), message)?;
372 return parse_timestamp(reader, &next, leaves, nodes, depth + 1);
373 }
374 if let Some(name) = unary_op(tag_byte) {
375 let next = apply_op(name, None, message)?;
376 return parse_timestamp(reader, &next, leaves, nodes, depth + 1);
377 }
378 Err(AnchorError::new(
379 "unknown_op",
380 format!("unknown operation tag 0x{tag_byte:02x}"),
381 ))
382}
383
384fn parse_timestamp(
385 reader: &mut Reader<'_>,
386 message: &[u8],
387 leaves: &mut Vec<(Vec<u8>, Attestation)>,
388 nodes: &mut usize,
389 depth: usize,
390) -> Result<(), AnchorError> {
391 if depth > MAX_DEPTH {
392 return Err(AnchorError::new(
393 "recursion_limit",
394 "timestamp tree exceeds depth limit",
395 ));
396 }
397 *nodes += 1;
398 if *nodes > MAX_NODES {
399 return Err(AnchorError::new(
400 "too_many_nodes",
401 "timestamp tree exceeds node limit",
402 ));
403 }
404
405 let mut tag = reader.read(1)?;
406 while tag == [0xFF] {
407 let next = reader.read(1)?.to_vec();
408 handle_tag(reader, &next, message, leaves, nodes, depth)?;
409 tag = reader.read(1)?;
410 }
411 handle_tag(reader, tag, message, leaves, nodes, depth)
412}
413
414pub fn parse_detached(data: &[u8]) -> Result<ParsedProof, AnchorError> {
416 let mut reader = Reader::new(data);
417 if reader.read(MAGIC.len())? != MAGIC {
418 return Err(AnchorError::new(
419 "bad_magic",
420 "not an OpenTimestamps detached proof",
421 ));
422 }
423 let version = reader.read(1)?[0];
424 if version != MAJOR_VERSION {
425 return Err(AnchorError::new(
426 "unsupported_version",
427 format!("major version {version} not supported"),
428 ));
429 }
430 let hash_tag = reader.read(1)?[0];
431 let Some((file_hash_op, digest_len)) = hash_op(hash_tag) else {
432 return Err(AnchorError::new(
433 "unknown_op",
434 format!("unsupported file hash op 0x{hash_tag:02x}"),
435 ));
436 };
437 let file_digest = reader.read(digest_len)?.to_vec();
438 let mut leaves = Vec::new();
439 let mut nodes = 0usize;
440 parse_timestamp(&mut reader, &file_digest, &mut leaves, &mut nodes, 0)?;
441 reader.assert_eof()?;
442 Ok(ParsedProof {
443 version,
444 file_hash_op: file_hash_op.to_string(),
445 file_digest,
446 leaves,
447 })
448}
449
450#[must_use]
456pub fn verify_proof(
457 data: &[u8],
458 expected: &Value,
459 block_headers: Option<&Map<String, Value>>,
460) -> AnchorResult {
461 let Some(expected) = digest_bytes(expected) else {
462 return AnchorResult::new(
463 "invalid",
464 "bad_digest",
465 "expected digest is not bytes/hex/sha256:<hex>",
466 );
467 };
468
469 let proof = match parse_detached(data) {
470 Ok(proof) => proof,
471 Err(error) => return AnchorResult::new("invalid", &error.code, error.detail),
472 };
473
474 let file_digest = format!("{}:{}", proof.file_hash_op, hex_encode(&proof.file_digest));
475 if expected.len() != proof.file_digest.len() || expected != proof.file_digest {
476 let mut result = AnchorResult::new(
477 "mismatch",
478 "digest_mismatch",
479 "proof was not created for the expected digest",
480 );
481 result.file_digest = file_digest;
482 return result;
483 }
484
485 let mut headers: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
486 if let Some(supplied) = block_headers {
487 for (height, header) in supplied {
488 let Ok(height) = height.parse::<u64>() else {
489 return AnchorResult::new(
490 "invalid",
491 "bad_header",
492 format!("header height {height:?} is not an integer"),
493 );
494 };
495 let decoded = digest_bytes(header).unwrap_or_default();
496 if decoded.len() != HEADER_LEN {
497 return AnchorResult::new(
498 "invalid",
499 "bad_header",
500 format!("header for height {height} is not 80 bytes"),
501 );
502 }
503 headers.insert(height, decoded);
504 }
505 }
506
507 let attestations: Vec<Value> = proof
508 .leaves
509 .iter()
510 .map(|(_, attestation)| attestation.as_dict())
511 .collect();
512 let bitcoins: Vec<(&Vec<u8>, u64)> = proof
513 .leaves
514 .iter()
515 .filter_map(|(message, attestation)| match attestation {
516 Attestation::Bitcoin(height) => Some((message, *height)),
517 _ => None,
518 })
519 .collect();
520
521 for (message, height) in &bitcoins {
522 let Some(header) = headers.get(height) else {
523 continue;
524 };
525 if message.as_slice() == &header[MERKLE_OFFSET..MERKLE_OFFSET + 32] {
526 let mut time = [0u8; 4];
527 time.copy_from_slice(&header[TIME_OFFSET..TIME_OFFSET + 4]);
528 let confirmed = json!({
529 "block_height": height,
530 "merkle_root": hex_encode(message),
531 "header_time": u32::from_le_bytes(time),
532 });
533 let mut result = AnchorResult::new(
534 "verified",
535 "anchor_verified",
536 format!("digest is the merkle root of Bitcoin block {height}"),
537 );
538 result.file_digest = file_digest;
539 result.attestations = attestations;
540 result.confirmed = Some(confirmed);
541 return result;
542 }
543 }
544
545 if !bitcoins.is_empty() && !headers.is_empty() {
546 let heights = bitcoins
547 .iter()
548 .map(|(_, height)| height.to_string())
549 .collect::<Vec<_>>()
550 .join(", ");
551 let mut result = AnchorResult::new(
552 "mismatch",
553 "header_mismatch",
554 format!("supplied header(s) do not match the proof (attested height(s): {heights})"),
555 );
556 result.file_digest = file_digest;
557 result.attestations = attestations;
558 return result;
559 }
560
561 if !bitcoins.is_empty() {
562 let mut result = AnchorResult::new(
563 "unverified",
564 "anchor_unverified",
565 "proof reaches a Bitcoin attestation; supply the 80-byte block header to confirm",
566 );
567 result.file_digest = file_digest;
568 result.attestations = attestations;
569 return result;
570 }
571
572 let kinds: BTreeSet<&str> = proof
573 .leaves
574 .iter()
575 .map(|(_, attestation)| attestation.kind())
576 .collect();
577 if kinds.contains("pending") {
578 let mut result = AnchorResult::new(
579 "unverified",
580 "anchor_pending",
581 "proof reaches only pending calendar attestations; upgrade it after confirmation",
582 );
583 result.file_digest = file_digest;
584 result.attestations = attestations;
585 return result;
586 }
587 if kinds.contains("litecoin") {
588 let mut result = AnchorResult::new(
589 "unverified",
590 "anchor_unverified",
591 "proof reaches a Litecoin attestation (not checked by this tool)",
592 );
593 result.file_digest = file_digest;
594 result.attestations = attestations;
595 return result;
596 }
597 if !proof.leaves.is_empty() {
598 let mut result = AnchorResult::new(
599 "unverified",
600 "anchor_unknown_type",
601 "proof reaches only unknown attestation types",
602 );
603 result.file_digest = file_digest;
604 result.attestations = attestations;
605 return result;
606 }
607 let mut result = AnchorResult::new("invalid", "anchor_empty", "proof contains no attestations");
608 result.file_digest = file_digest;
609 result
610}
611
612#[must_use]
614pub fn verify_proof_raw(
615 data: &[u8],
616 expected: &[u8],
617 block_headers: &BTreeMap<u64, Vec<u8>>,
618) -> AnchorResult {
619 let headers: Map<String, Value> = block_headers
620 .iter()
621 .map(|(height, header)| (height.to_string(), json!(hex_encode(header))))
622 .collect();
623 verify_proof(data, &json!(hex_encode(expected)), Some(&headers))
624}
625
626pub fn bundle_digest(
631 bundle_path: &str,
632 target: &str,
633) -> Result<(String, Option<Value>), AnchorError> {
634 let text = std::fs::read_to_string(bundle_path)
635 .map_err(|error| AnchorError::new("io_error", error.to_string()))?;
636 let bundle: Value = serde_json::from_str(&text)
637 .map_err(|error| AnchorError::new("io_error", error.to_string()))?;
638 let receipts = bundle
639 .get("receipts")
640 .and_then(Value::as_array)
641 .cloned()
642 .unwrap_or_default();
643 for receipt in &receipts {
644 if receipt.get("receipt_id").and_then(Value::as_str) != Some(target) {
645 continue;
646 }
647 let binding = bundle
648 .get("anchors")
649 .and_then(Value::as_array)
650 .and_then(|anchors| {
651 anchors.iter().find(|anchor| {
652 anchor.get("target").and_then(Value::as_str) == Some(target)
653 && anchor
654 .get("anchor")
655 .and_then(Value::as_object)
656 .and_then(|meta| meta.get("type"))
657 .and_then(Value::as_str)
658 == Some("opentimestamps")
659 })
660 })
661 .cloned();
662 let object = receipt.as_object().ok_or_else(|| {
663 AnchorError::new("invalid_receipt", "receipt is not an object")
664 })?;
665 let digest = receipt_digest(object).map_err(|error| AnchorError::new("invalid_receipt", error.0))?;
666 return Ok((digest, binding));
667 }
668 Err(AnchorError::new(
669 "unknown_target",
670 format!("no receipt {target:?} in bundle"),
671 ))
672}