Skip to main content

confium_transparency/ots/
client.rs

1//! OTS client — submit hash to calendar servers, verify proofs.
2
3use crate::ots::proof::{OtsError, OtsProof, OtsVerification};
4use sha2::{Digest, Sha256};
5
6/// Default public calendar servers (free, community-operated).
7pub const DEFAULT_CALENDAR_SERVERS: &[&str] = &[
8    "https://a.pool.opentimestamps.org",
9    "https://b.pool.opentimestamps.org",
10    "https://a.pool.eternitywall.com",
11    "https://ots.btc.catallaxy.com",
12];
13
14/// OTS client.
15pub struct OtsClient {
16    calendar_servers: Vec<String>,
17}
18
19impl OtsClient {
20    /// Construct a new client with default calendar servers.
21    pub fn new() -> Self {
22        Self {
23            calendar_servers: DEFAULT_CALENDAR_SERVERS
24                .iter()
25                .map(|s| s.to_string())
26                .collect(),
27        }
28    }
29
30    /// Construct with custom calendar servers.
31    pub fn with_servers(servers: Vec<String>) -> Self {
32        Self {
33            calendar_servers: servers,
34        }
35    }
36
37    /// Available calendar servers.
38    pub fn calendar_servers(&self) -> &[String] {
39        &self.calendar_servers
40    }
41
42    /// Submit a hash for timestamping. Returns a mock proof immediately;
43    /// real implementation would poll the calendar server until Bitcoin
44    /// confirmation arrives (typically 1-12 hours).
45    pub async fn stamp(&self, hash: [u8; 32]) -> Result<OtsProof, OtsError> {
46        // Mock: return a proof anchored at a fixed block height.
47        // Real impl: POST to calendar server, poll for proof, parse result.
48        let _ = &self.calendar_servers;
49        Ok(OtsProof::new(hash, 800_000))
50    }
51
52    /// Verify a proof against Bitcoin block headers.
53    ///
54    /// Caller provides a `bitcoin_block_header_hash` callback that returns
55    /// the block hash at the given height (real impl: query Bitcoin Core
56    /// RPC, or use a public blockchain API).
57    pub async fn verify<F>(
58        &self,
59        proof: &OtsProof,
60        bitcoin_block_at_height: F,
61    ) -> Result<OtsVerification, OtsError>
62    where
63        F: Fn(u32) -> Result<[u8; 32], String>,
64    {
65        // Verify the Merkle root matches what's in the block.
66        let _block_hash =
67            bitcoin_block_at_height(proof.bitcoin_height).map_err(OtsError::BitcoinBackend)?;
68
69        // Mock: in real impl, parse block header, extract Merkle root,
70        // verify the Merkle branch proves inclusion of `proof.hash` under
71        // that root.
72        let mut current = proof.hash;
73        for sibling in &proof.merkle_branch {
74            let mut h = Sha256::new();
75            h.update(current);
76            h.update(sibling);
77            let mut out = [0u8; 32];
78            out.copy_from_slice(&h.finalize());
79            // Double SHA-256 (Bitcoin convention)
80            let mut h2 = Sha256::new();
81            h2.update(out);
82            current.copy_from_slice(&h2.finalize());
83        }
84
85        let valid = current == proof.merkle_root || proof.merkle_branch.is_empty();
86        Ok(OtsVerification {
87            valid,
88            bitcoin_height: proof.bitcoin_height,
89            block_timestamp: None,
90        })
91    }
92}
93
94impl Default for OtsClient {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn client_has_default_servers() {
106        let client = OtsClient::new();
107        assert!(!client.calendar_servers().is_empty());
108    }
109
110    #[tokio::test]
111    async fn mock_stamp_returns_proof() {
112        let client = OtsClient::new();
113        let hash = [42u8; 32];
114        let proof = client.stamp(hash).await.unwrap();
115        assert_eq!(proof.hash, hash);
116        assert!(proof.bitcoin_height > 0);
117    }
118
119    #[tokio::test]
120    async fn verify_empty_branch_is_valid() {
121        let client = OtsClient::new();
122        let hash = [1u8; 32];
123        let proof = OtsProof::new(hash, 800_000);
124        let result = client.verify(&proof, |_| Ok([0u8; 32])).await.unwrap();
125        assert!(result.valid);
126    }
127}