Skip to main content

confium_log_server/
ots_anchor.rs

1//! Bitcoin OTS anchor loop.
2//!
3//! Periodically submits the current tree head to multiple
4//! OpenTimestamps calendar servers. The OTS proofs are stored in
5//! the database and served via `GET /v1/head/<N>/ots`.
6//!
7//! ## Frequency
8//!
9//! Every 10 minutes by default. OTS aggregation means the per-batch
10//! Bitcoin cost is amortized across many log submitters; the actual
11//! on-chain footprint is one OP_RETURN per calendar per hour.
12//!
13//! ## Calendar servers
14//!
15//! The default set mirrors what `opentimestamps-client` ships:
16//!
17//! - `https://a.pool.opentimestamps.org`
18//! - `https://b.pool.opentimestamps.org`
19//! - `https://a.eternity.college`
20//! - `https://ots.btc.cat`
21//!
22//! A calendar failure is non-fatal — the anchor proceeds with
23//! whichever calendars respond.
24
25use std::sync::Arc;
26use std::time::Duration;
27
28use crate::api::AppState;
29
30/// Run the OTS anchor loop in the background. Submits the current
31/// tree root to calendar servers at the configured interval.
32pub async fn run_anchor_loop(state: Arc<AppState>, interval: Duration) {
33    loop {
34        tokio::time::sleep(interval).await;
35        if let Err(e) = anchor_once(&state).await {
36            tracing::warn!(?e, "OTS anchor cycle failed");
37        }
38    }
39}
40
41/// One anchor cycle: snapshot the current tree head, submit to OTS
42/// calendars, store the result. Returns Ok if at least one calendar
43/// accepted.
44async fn anchor_once(state: &Arc<AppState>) -> anyhow::Result<()> {
45    let (tree_size, root) = {
46        let merkle = state.merkle.lock();
47        (merkle.len(), merkle.root())
48    };
49
50    tracing::info!(tree_size, root = %hex::encode(root), "anchoring tree head");
51
52    // In a real deployment, this is where we'd POST the root to each
53    // calendar and aggregate the OTS proofs. For the scaffold, we
54    // record a placeholder proof so the API surface is testable
55    // end-to-end without an external dependency.
56    let placeholder_proof = build_placeholder_proof(tree_size, &root);
57    state
58        .db
59        .store_ots_proof(tree_size, &root, &placeholder_proof, None)?;
60
61    Ok(())
62}
63
64/// Build a placeholder OTS proof for testing. The real implementation
65/// would parse the calendar server responses and assemble the proof
66/// per the OTS wire format (RFC opentimestamps).
67fn build_placeholder_proof(tree_size: u64, root: &[u8; 32]) -> Vec<u8> {
68    let mut proof = Vec::new();
69    proof.extend_from_slice(b"OTS-PLACEHOLDER/v1\n");
70    proof.extend_from_slice(&tree_size.to_be_bytes());
71    proof.extend_from_slice(root);
72    proof
73}