opentimestamps_cli/
lib.rs

1// Copyright (C) 2024 The OpenTimestamps developers
2
3extern crate bitcoincore_rpc;
4extern crate camino;
5extern crate chrono;
6extern crate electrum_client;
7extern crate env_logger;
8extern crate log;
9extern crate ots;
10extern crate rand;
11extern crate reqwest;
12extern crate rs_merkle;
13extern crate thiserror;
14
15pub mod calendar;
16pub mod error;
17pub mod extensions;
18
19use bitcoincore_rpc::RpcApi;
20use calendar::Calendar;
21use error::Error;
22use extensions::{StepExtension, TimestampExtension};
23
24use chrono::DateTime;
25use electrum_client::bitcoin::hashes::Hash;
26use electrum_client::{Client, ElectrumApi};
27use log::{debug, error, info};
28use ots::hex::Hexed;
29use ots::ser::DigestType;
30use ots::{
31    attestation::Attestation,
32    op::Op,
33    timestamp::{Step, StepData},
34    DetachedTimestampFile, Timestamp,
35};
36use rs_merkle::{algorithms::Sha256, MerkleTree};
37use std::convert::TryInto;
38use std::time::Duration;
39
40pub fn info(ots: DetachedTimestampFile) -> Result<String, Error> {
41    Ok(ots.to_string())
42}
43/// Verify attestation against a block header
44fn verify_against_blockheader(
45    digest: [u8; 32],
46    block_header: electrum_client::bitcoin::block::Header,
47) -> Result<u32, Error> {
48    if digest != block_header.merkle_root.to_byte_array() {
49        return Err(Error::Generic("Merkle root mismatch".to_string()));
50    }
51    Ok(block_header.time)
52}
53
54fn timestamp_to_date(timestamp: i64) -> String {
55    let from = DateTime::from_timestamp(timestamp, 0).unwrap();
56    let date = from.naive_local();
57    date.format("%Y-%m-%d").to_string()
58}
59
60pub struct BitcoinAttestationResult {
61    height: usize,
62    time: u32,
63}
64impl std::fmt::Display for BitcoinAttestationResult {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        write!(
67            f,
68            "Bitcoin block {} attests existence as of {}",
69            self.height,
70            timestamp_to_date(self.time as i64)
71        )
72    }
73}
74
75pub fn verify(
76    ots: DetachedTimestampFile,
77    bitcoin_client: Option<bitcoincore_rpc::Client>,
78) -> Result<BitcoinAttestationResult, Error> {
79    let client = Client::new("tcp://electrum.blockstream.info:50001").unwrap();
80
81    for attestation in ots.timestamp.all_attestations() {
82        match attestation.1 {
83            Attestation::Bitcoin { height } => {
84                let block_header = match bitcoin_client {
85                    Some(client) => {
86                        let block_hash = client.get_block_hash(height as u64).unwrap();
87                        debug!("Attestation block hash: {:?}", block_hash);
88                        client.get_block_header(&block_hash).unwrap()
89                    }
90                    None => {
91                        let block_header = client.block_header(height).unwrap();
92                        debug!("Attestation block hash: {:?}", block_header.block_hash());
93                        block_header
94                    }
95                };
96                let time =
97                    verify_against_blockheader(attestation.0.try_into().unwrap(), block_header)?;
98                let result = BitcoinAttestationResult { height, time };
99                info!("Success! {}", result);
100                return Ok(BitcoinAttestationResult { height, time });
101            }
102            Attestation::Pending { uri } => {
103                debug!("Ignoring Pending Attestation at {:?}", uri);
104            }
105            Attestation::Unknown { tag: _, data: _ } => {
106                debug!("Ignoring Unknown Attestation");
107            }
108        };
109    }
110    Err(Error::Generic("No bitcoin attestion found".to_string()))
111}
112
113pub fn upgrade(
114    ots: &mut DetachedTimestampFile,
115    calendar_urls: Option<Vec<String>>,
116) -> Result<(), Error> {
117    for attestation in ots.timestamp.all_attestations() {
118        match attestation.1 {
119            Attestation::Bitcoin { height: _ } => {}
120            Attestation::Unknown { tag: _, data: _ } => {}
121            Attestation::Pending { ref uri } => {
122                if calendar_urls
123                    .as_ref()
124                    .is_some_and(|urls| !urls.contains(uri))
125                {
126                    error!("No valid calendar found");
127                    continue;
128                }
129                info!("Upgrading to remote calendar {}", uri.to_string());
130                let upgraded = upgrade_timestamp(attestation.0, uri.to_string(), None)?;
131                ots.timestamp.merge(upgraded);
132            }
133        };
134    }
135    Ok(())
136}
137
138fn upgrade_timestamp(
139    commitment: Vec<u8>,
140    calendar_url: String,
141    timeout: Option<Duration>,
142) -> Result<Timestamp, Error> {
143    let res = Calendar {
144        url: calendar_url,
145        timeout: timeout,
146    }
147    .get_timestamp(commitment.clone())
148    .map_err(|err| Error::NetworkError(err))?;
149    let mut deser = ots::ser::Deserializer::new(res);
150    Timestamp::deserialize(&mut deser, commitment).map_err(|err| Error::InvalidOts(err))
151}
152
153fn timestamp_from_merkle(
154    merkle_tree: &MerkleTree<Sha256>,
155    leave: [u8; 32],
156) -> Result<Timestamp, Error> {
157    let index = merkle_tree
158        .leaves()
159        .unwrap()
160        .iter()
161        .position(|l| *l == leave)
162        .unwrap();
163    let proofs = merkle_tree.proof(&[index]);
164    //debug!("proofs {:?}", proofs.proof_hashes_hex());
165    //debug!("index {:?}", index);
166
167    let mut step = Step {
168        data: StepData::Op(Op::Hexlify),
169        output: vec![],
170        next: vec![],
171    };
172    let mut digest = leave.to_vec();
173    for proof in proofs.proof_hashes().iter().enumerate() {
174        let level = proof.0 as u32;
175        let odd = (index as i32 / 2_i32.pow(level)) % 2 == 1;
176        let op = if odd {
177            Op::Prepend(proof.1.to_vec())
178        } else {
179            Op::Append(proof.1.to_vec())
180        };
181        let step_pend = Step {
182            data: StepData::Op(op.clone()),
183            output: op.execute(&digest),
184            next: vec![],
185        };
186        let op = Op::Sha256;
187        digest = op.execute(&step_pend.clone().output);
188        let step_sha256 = Step {
189            data: StepData::Op(op.clone()),
190            output: op.execute(&step_pend.clone().output),
191            next: vec![],
192        };
193        if level == 0 {
194            step = step_pend;
195        } else {
196            step.cat(step_pend);
197        }
198        step.cat(step_sha256);
199    }
200    Ok(Timestamp {
201        start_digest: leave.to_vec(),
202        first_step: step,
203    })
204}
205
206pub fn stamps(
207    digests: Vec<Vec<u8>>,
208    digest_type: DigestType,
209    calendar_urls: Option<Vec<String>>,
210    timeout: Option<Duration>,
211) -> Result<Vec<DetachedTimestampFile>, Error> {
212    let mut merkle_roots: Vec<[u8; 32]> = vec![];
213    let mut file_timestamps: Vec<ots::DetachedTimestampFile> = vec![];
214    for digest in digests {
215        let random: Vec<u8> = (0..16).map(|_| rand::random::<u8>()).collect();
216        let nonce_op = ots::op::Op::Append(random);
217        let nonce_output_digest = nonce_op.execute(&digest);
218        let hash_op = ots::op::Op::Sha256;
219        let hash_output_digest = hash_op.execute(&nonce_output_digest);
220        let file_timestamp = ots::DetachedTimestampFile {
221            digest_type: digest_type,
222            timestamp: ots::Timestamp {
223                start_digest: digest,
224                first_step: Step {
225                    data: StepData::Op(nonce_op),
226                    output: nonce_output_digest,
227                    next: vec![Step {
228                        data: StepData::Op(hash_op),
229                        output: hash_output_digest.clone(),
230                        next: vec![],
231                    }],
232                },
233            },
234        };
235        //let timestamp = file_timestamp.timestamp;
236        file_timestamps.push(file_timestamp.clone());
237        merkle_roots.push(hash_output_digest.try_into().unwrap());
238    }
239    debug!("file_timestamps {}", file_timestamps[0]);
240    debug!("merkle_roots {:?}", merkle_roots.len());
241    for root in merkle_roots.iter() {
242        debug!("{:?}", Hexed(root));
243    }
244    let merkle_tree = MerkleTree::<Sha256>::from_leaves(&merkle_roots);
245    let merkle_tip = merkle_tree.root().unwrap();
246
247    if file_timestamps.len() > 1 {
248        for ft in file_timestamps.iter_mut().enumerate() {
249            if let Ok(timestamp) = timestamp_from_merkle(&merkle_tree, merkle_roots[ft.0]) {
250                ft.1.timestamp.merge(timestamp);
251            }
252        }
253    }
254    let calendar_urls = match calendar_urls {
255        Some(urls) => urls,
256        None => vec![
257            calendar::APOOL.to_string(),
258            calendar::BPOOL.to_string(),
259            calendar::FINNEY.to_string(),
260        ],
261    };
262
263    let mut calendar_timestamps = vec![];
264    for calendar in calendar_urls {
265        info!("Submitting to remote calendar {}", calendar);
266        let calendar_timestamp = create_timestamp(merkle_tip.to_vec(), calendar.clone(), timeout);
267        match calendar_timestamp {
268            Ok(timestamp) => calendar_timestamps.push(timestamp),
269            Err(e) => error!("Ignoring remote calendar {}: {}", calendar, e.to_string()),
270        }
271    }
272    if calendar_timestamps.is_empty() {
273        return Err(Error::Generic("No valid calendar found".to_string()));
274    }
275    let timestamp: Timestamp;
276    if calendar_timestamps.len() == 1 {
277        timestamp = calendar_timestamps.first().unwrap().clone();
278    } else {
279        let steps = calendar_timestamps
280            .iter()
281            .map(|x| x.first_step.clone())
282            .collect();
283        let fork = Step {
284            data: StepData::Fork,
285            output: merkle_tip.to_vec(),
286            next: steps,
287        };
288        timestamp = Timestamp {
289            start_digest: merkle_tip.to_vec(),
290            first_step: fork,
291        };
292    }
293    for ft in file_timestamps.iter_mut() {
294        ft.timestamp.merge(timestamp.clone());
295    }
296    Ok(file_timestamps)
297}
298
299fn create_timestamp(
300    stamp: Vec<u8>,
301    calendar_url: String,
302    timeout: Option<Duration>,
303) -> Result<Timestamp, Error> {
304    let res = Calendar {
305        url: calendar_url,
306        timeout: timeout,
307    }
308    .submit_calendar(stamp.clone())
309    .map_err(|err| Error::NetworkError(err))?;
310    let mut deser = ots::ser::Deserializer::new(res);
311    Timestamp::deserialize(&mut deser, stamp.to_vec()).map_err(|err| Error::InvalidOts(err))
312}