From 4fb2c80bf3d4278889c3b433ae7321bffc401efa Mon Sep 17 00:00:00 2001 From: lightsing Date: Sat, 21 Feb 2026 21:47:45 +0800 Subject: [PATCH 1/4] move to cli crate --- Cargo.lock | 106 +++++++++++++++++++- Cargo.toml | 1 + crates/cli/Cargo.toml | 31 ++++++ crates/cli/src/commands.rs | 21 ++++ crates/cli/src/commands/inspect.rs | 34 +++++++ crates/cli/src/commands/verify.rs | 146 ++++++++++++++++++++++++++++ crates/cli/src/main.rs | 18 ++++ crates/core/Cargo.toml | 24 ----- crates/core/src/bin/uts_info.rs | 61 ------------ crates/core/src/bin/uts_verifier.rs | 141 --------------------------- 10 files changed, 353 insertions(+), 230 deletions(-) create mode 100644 crates/cli/Cargo.toml create mode 100644 crates/cli/src/commands.rs create mode 100644 crates/cli/src/commands/inspect.rs create mode 100644 crates/cli/src/commands/verify.rs create mode 100644 crates/cli/src/main.rs delete mode 100644 crates/core/src/bin/uts_info.rs delete mode 100644 crates/core/src/bin/uts_verifier.rs diff --git a/Cargo.lock b/Cargo.lock index 5d489f7..f6e46a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,12 +882,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -1684,6 +1728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -1692,8 +1737,22 @@ version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -1780,6 +1839,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "commonware-codec" version = "0.0.63" @@ -3308,6 +3373,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -3778,6 +3849,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -5921,6 +5998,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uts-bmt" version = "0.1.0" @@ -5964,6 +6047,25 @@ dependencies = [ "uts-stamper", ] +[[package]] +name = "uts-cli" +version = "0.1.0" +dependencies = [ + "alloy-provider", + "clap", + "color-eyre", + "digest 0.11.0-rc.4", + "eyre", + "jiff", + "ripemd 0.2.0-rc.3", + "sha1 0.11.0-rc.3", + "sha2 0.11.0-rc.3", + "sha3 0.11.0-rc.3", + "tokio", + "tracing", + "uts-core", +] + [[package]] name = "uts-contracts" version = "0.1.0" @@ -5988,12 +6090,9 @@ dependencies = [ "alloy-sol-types", "auto_impl", "bytes", - "color-eyre", "criterion 0.5.1", "digest 0.11.0-rc.4", - "eyre", "hex", - "jiff", "once_cell", "opentimestamps", "paste", @@ -6005,7 +6104,6 @@ dependencies = [ "sha2 0.11.0-rc.3", "sha3 0.11.0-rc.3", "thiserror 2.0.17", - "tokio", "tracing", "uts-contracts", ] diff --git a/Cargo.toml b/Cargo.toml index 640844d..f923f05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +default-members = ["crates/cli"] members = ["crates/*"] resolver = "3" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000..502c7d8 --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,31 @@ +[package] +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "uts-cli" +repository.workspace = true +version.workspace = true + +[[bin]] +name = "uts" +path = "src/main.rs" + +[dependencies] +alloy-provider = { workspace = true } +clap = { workspace = true, features = ["derive"] } +color-eyre = { workspace = true } +digest = { workspace = true } +eyre = { workspace = true } +jiff = { workspace = true } +ripemd = { workspace = true } +sha1 = { workspace = true } +sha2 = { workspace = true } +sha3 = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +uts-core = { workspace = true, features = ["std", "ethereum-uts-verifier"] } + +[lints] +workspace = true diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs new file mode 100644 index 0000000..fe6a209 --- /dev/null +++ b/crates/cli/src/commands.rs @@ -0,0 +1,21 @@ +use clap::Subcommand; + +mod inspect; +mod verify; + +#[derive(Debug, Subcommand)] +pub enum Commands { + /// Inspect an ots file + Inspect(inspect::Inspect), + /// Verify an ots file against a file + Verify(verify::Verify), +} + +impl Commands { + pub async fn run(self) -> eyre::Result<()> { + match self { + Commands::Inspect(cmd) => cmd.run(), + Commands::Verify(cmd) => cmd.run().await, + } + } +} diff --git a/crates/cli/src/commands/inspect.rs b/crates/cli/src/commands/inspect.rs new file mode 100644 index 0000000..ff47f6a --- /dev/null +++ b/crates/cli/src/commands/inspect.rs @@ -0,0 +1,34 @@ +use clap::Args; +use std::{fs, io, io::Seek, path::PathBuf}; +use uts_core::codec::{ + Decode, Reader, VersionedProof, + v1::{DetachedTimestamp, Timestamp}, +}; + +#[derive(Debug, Args)] +pub struct Inspect { + /// Path to the OTS file to inspect + ots_file: PathBuf, +} + +impl Inspect { + pub fn run(self) -> eyre::Result<()> { + let mut fh = fs::File::open(&self.ots_file)?; + + match VersionedProof::::decode(&mut Reader(&mut fh)) { + Ok(ots) => { + eprintln!("OTS Detached Timestamp found:\n{ots}"); + return Ok(()); + } + Err(e) => { + eprintln!("Not a valid Detached Timestamp OTS file (trying raw timestamp): {e}\n"); + } + }; + + fh.seek(io::SeekFrom::Start(0))?; + + let raw = Timestamp::decode(&mut Reader(&mut fh))?; + eprintln!("Raw Timestamp found:\n{raw}"); + Ok(()) + } +} diff --git a/crates/cli/src/commands/verify.rs b/crates/cli/src/commands/verify.rs new file mode 100644 index 0000000..3f817a3 --- /dev/null +++ b/crates/cli/src/commands/verify.rs @@ -0,0 +1,146 @@ +use alloy_provider::ProviderBuilder; +use clap::Args; +use digest::{Digest, DynDigest}; +use eyre::bail; +use jiff::{Timestamp, tz::TimeZone}; +use std::{ + fs::File, + io::{BufReader, Read}, + path::PathBuf, + process, +}; +use uts_core::{ + codec::{ + Decode, Reader, VersionedProof, + v1::{ + Attestation, DetachedTimestamp, EthereumUTSAttestation, PendingAttestation, opcode::*, + }, + }, + utils::Hexed, + verifier::{AttestationVerifier, EthereumUTSVerifier}, +}; + +#[derive(Debug, Args)] +pub struct Verify { + file: PathBuf, + stamp_file: Option, + /// Optional Ethereum provider URL for verifying Ethereum UTS attestations. + /// If not provided, a default provider will be used based on the chain ID. + #[arg(long)] + eth_provider: Option, +} + +impl Verify { + pub async fn run(self) -> eyre::Result<()> { + let stamp_file = self.stamp_file.unwrap_or_else(|| { + let mut default = self.file.clone(); + default.add_extension("ots"); + default + }); + let timestamp = + VersionedProof::::decode(&mut Reader(File::open(stamp_file)?))? + .proof; + + let digest_header = timestamp.header(); + let mut hasher = match digest_header.kind().tag() { + SHA1 => Box::new(sha1::Sha1::new()) as Box, + RIPEMD160 => Box::new(ripemd::Ripemd160::new()) as Box, + SHA256 => Box::new(sha2::Sha256::new()) as Box, + KECCAK256 => Box::new(sha3::Keccak256::new()) as Box, + _ => bail!("Unsupported digest type: {}", digest_header.kind()), + }; + + let mut file = BufReader::new(File::open(self.file)?); + let mut buffer = [0u8; 64 * 1024]; // 64KB buffer + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + let expected = hasher.finalize(); + + if *expected != *digest_header.digest() { + eprintln!( + "Digest mismatch! Expected: {}, Found: {}", + Hexed(&expected), + Hexed(digest_header.digest()) + ); + process::exit(1); + } + eprintln!("Digest matches: {}", Hexed(&expected)); + + timestamp.try_finalize()?; + + for attestation in timestamp.attestations() { + if attestation.tag == PendingAttestation::TAG { + continue; // skip pending attestations + } + + if attestation.tag == EthereumUTSAttestation::TAG { + let eth_attestation = EthereumUTSAttestation::from_raw(&attestation)?; + eprintln!("Attested by {eth_attestation}"); + let provider_url = if let Some(url) = self.eth_provider.as_deref() { + url + } else { + match eth_attestation.chain.id() { + 1 => "https://0xrpc.io/eth", + 11155111 => "https://0xrpc.io/sep", + 534352 => "https://rpc.scroll.io", + 534351 => "https://sepolia-rpc.scroll.io", + _ => bail!("Unsupported chain: {}", eth_attestation.chain), + } + }; + let provider = ProviderBuilder::new().connect(provider_url).await?; + let verifier = EthereumUTSVerifier::new(provider).await?; + let result = verifier + .verify(ð_attestation, attestation.value().unwrap()) + .await?; + if let Some(block_number) = result.block_number { + if let Some(block_hash) = result.block_hash { + eprintln!("\tblock: #{block_number} {block_hash}"); + } else { + eprintln!("\tblock: {block_number}"); + } + } + if let Some(log_index) = result.log_index { + eprintln!("\tlog index: {log_index}"); + } + if let Some(transaction_hash) = result.transaction_hash { + if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { + eprintln!("\ttransaction: {etherscan_url}/tx/{transaction_hash}"); + } else { + eprintln!("\ttransaction hash: {transaction_hash}"); + } + } + + if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { + eprintln!( + "\tuts contract: {etherscan_url}/address/{}", + result.inner.address + ); + } else { + eprintln!("\tuts contract: {}", result.inner.address); + } + if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { + eprintln!( + "\ttx sender: {etherscan_url}/address/{}", + result.inner.sender + ); + } else { + eprintln!("\ttx sender: {}", result.inner.sender); + } + let ts = Timestamp::from_second(result.inner.timestamp.to())?; + let zdt = ts.to_zoned(TimeZone::system()); + eprintln!("\ttime attested: {zdt}"); + eprintln!("\tmerkle root: {}", result.inner.root); + continue; + } + + eprintln!("Unverifiable attestation: {attestation}"); + } + + Ok(()) + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000..fa0de7b --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,18 @@ +//! UTS Cli +use crate::commands::Commands; +use clap::Parser; + +mod commands; + +#[derive(Debug, Parser)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + color_eyre::install()?; + + Cli::parse().command.run().await +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 87103c6..cd7dbd7 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -9,16 +9,6 @@ name = "uts-core" repository.workspace = true version.workspace = true -[[bin]] -name = "uts-info" -path = "src/bin/uts_info.rs" -required-features = ["std"] - -[[bin]] -name = "uts-verifier" -path = "src/bin/uts_verifier.rs" -required-features = ["verifier-bin-deps"] - [dependencies] alloy-chains = { workspace = true } alloy-primitives = { workspace = true } @@ -27,11 +17,8 @@ alloy-rpc-types-eth = { workspace = true, optional = true } alloy-sol-types = { workspace = true, optional = true } auto_impl.workspace = true bytes = { workspace = true, optional = true } -color-eyre = { workspace = true, optional = true } digest.workspace = true -eyre = { workspace = true, optional = true } hex.workspace = true -jiff = { workspace = true, optional = true } once_cell = { workspace = true, features = ["alloc"] } paste.workspace = true ripemd.workspace = true @@ -41,7 +28,6 @@ sha1.workspace = true sha2.workspace = true sha3.workspace = true thiserror.workspace = true -tokio = { workspace = true, optional = true } tracing = { workspace = true, optional = true } uts-contracts = { workspace = true, optional = true } @@ -54,16 +40,6 @@ std = [] tracing = ["dep:tracing"] verifier = [] -verifier-bin-deps = [ - "dep:eyre", - "dep:tokio", - "dep:color-eyre", - "dep:jiff", - "std", - "ethereum-uts-verifier", - "tokio/full", -] - [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } opentimestamps = { git = "https://github.com/opentimestamps/rust-opentimestamps" } diff --git a/crates/core/src/bin/uts_info.rs b/crates/core/src/bin/uts_info.rs deleted file mode 100644 index c7d11ea..0000000 --- a/crates/core/src/bin/uts_info.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (C) The OpenTimestamps developers -// Copyright (C) The ots-rs developers -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! # OpenTimestamps Viewer -//! -//! Simple application to open an OTS info file and dump its contents -//! to stdout in a human-readable format - -use std::{ - env, fs, io, - io::{BufReader, Seek}, - process, -}; -use uts_core::codec::{ - Decode, Reader, VersionedProof, - v1::{DetachedTimestamp, Timestamp}, -}; - -fn main() { - let args: Vec = env::args().collect(); - if args.len() != 2 { - println!("Usage: {} ", args[0]); - process::exit(1); - } - - let mut fh = match fs::File::open(&args[1]) { - Ok(fh) => BufReader::new(fh), - Err(e) => { - println!("Failed to open {}: {}", args[1], e); - process::exit(1); - } - }; - - match VersionedProof::::decode(&mut Reader(&mut fh)) { - Ok(ots) => { - println!("OTS Detached Timestamp found:"); - println!("{ots}"); - return; - } - Err(e) => { - println!( - "Not a valid Detached Timestamp OTS file (trying raw timestamp): {}\n", - e - ); - } - }; - - fh.seek(io::SeekFrom::Start(0)).unwrap(); - - match Timestamp::decode(&mut Reader(&mut fh)) { - Ok(ots) => { - println!("Raw Timestamp found:"); - println!("{ots}"); - } - Err(e) => { - println!("Failed to parse {}: {}", args[1], e); - process::exit(1); - } - } -} diff --git a/crates/core/src/bin/uts_verifier.rs b/crates/core/src/bin/uts_verifier.rs deleted file mode 100644 index 99049b5..0000000 --- a/crates/core/src/bin/uts_verifier.rs +++ /dev/null @@ -1,141 +0,0 @@ -use alloy_provider::ProviderBuilder; -use digest::{Digest, DynDigest}; -use jiff::{Timestamp, tz::TimeZone}; -use std::{ - env, fs, - io::{BufReader, Read}, - process, -}; -use uts_core::{ - codec::{ - Decode, Reader, VersionedProof, - v1::{ - Attestation, DetachedTimestamp, EthereumUTSAttestation, PendingAttestation, opcode::*, - }, - }, - utils::Hexed, - verifier::{AttestationVerifier, EthereumUTSVerifier}, -}; - -#[tokio::main] -async fn main() -> eyre::Result<()> { - color_eyre::install()?; - - let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("Usage: {} [eth provider url]", args[0]); - process::exit(1); - } - - let mut fh = BufReader::new(if args.len() >= 3 { - fs::File::open(&args[2])? - } else { - fs::File::open(format!("{}.ots", &args[1]))? - }); - let timestamp = VersionedProof::::decode(&mut Reader(&mut fh))?.proof; - - let digest_header = timestamp.header(); - let mut hasher = match digest_header.kind().tag() { - SHA1 => Box::new(sha1::Sha1::new()) as Box, - RIPEMD160 => Box::new(ripemd::Ripemd160::new()) as Box, - SHA256 => Box::new(sha2::Sha256::new()) as Box, - KECCAK256 => Box::new(sha3::Keccak256::new()) as Box, - _ => { - eprintln!("Unsupported digest type: {}", digest_header.kind()); - process::exit(1); - } - }; - - let mut file = BufReader::new(fs::File::open(&args[1])?); - let mut buffer = [0u8; 64 * 1024]; // 64KB buffer - loop { - let bytes_read = file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - let expected = hasher.finalize(); - - if *expected != *digest_header.digest() { - eprintln!( - "Digest mismatch! Expected: {}, Found: {}", - Hexed(&expected), - Hexed(digest_header.digest()) - ); - process::exit(1); - } - eprintln!("Digest matches: {}", Hexed(&expected)); - - timestamp.try_finalize()?; - - for attestation in timestamp.attestations() { - if attestation.tag == PendingAttestation::TAG { - continue; // skip pending attestations - } - - if attestation.tag == EthereumUTSAttestation::TAG { - let eth_attestation = EthereumUTSAttestation::from_raw(&attestation)?; - eprintln!("Attested by {eth_attestation}"); - let provider_url = if args.len() >= 4 { - &*args[3] - } else { - match eth_attestation.chain.id() { - 1 => "https://0xrpc.io/eth", - 11155111 => "https://0xrpc.io/sep", - 534352 => "https://rpc.scroll.io", - 534351 => "https://sepolia-rpc.scroll.io", - _ => panic!("Unsupported chain: {}", eth_attestation.chain), - } - }; - let provider = ProviderBuilder::new().connect(provider_url).await?; - let verifier = EthereumUTSVerifier::new(provider).await?; - let result = verifier - .verify(ð_attestation, attestation.value().unwrap()) - .await?; - if let Some(block_number) = result.block_number { - if let Some(block_hash) = result.block_hash { - eprintln!("\tblock: #{block_number} {block_hash}"); - } else { - eprintln!("\tblock: {block_number}"); - } - } - if let Some(log_index) = result.log_index { - eprintln!("\tlog index: {log_index}"); - } - if let Some(transaction_hash) = result.transaction_hash { - if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { - eprintln!("\ttransaction: {etherscan_url}/tx/{transaction_hash}"); - } else { - eprintln!("\ttransaction hash: {transaction_hash}"); - } - } - - if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { - eprintln!( - "\tuts contract: {etherscan_url}/address/{}", - result.inner.address - ); - } else { - eprintln!("\tuts contract: {}", result.inner.address); - } - if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { - eprintln!( - "\ttx sender: {etherscan_url}/address/{}", - result.inner.sender - ); - } else { - eprintln!("\ttx sender: {}", result.inner.sender); - } - let ts = Timestamp::from_second(result.inner.timestamp.to())?; - let zdt = ts.to_zoned(TimeZone::system()); - eprintln!("\ttime attested: {zdt}"); - eprintln!("\tmerkle root: {}", result.inner.root); - continue; - } - - eprintln!("Unverifiable attestation: {attestation}"); - } - - Ok(()) -} From 3a85a58ce1db1fa6608122af27d9b3d69fc0dde8 Mon Sep 17 00:00:00 2001 From: lightsing Date: Sun, 22 Feb 2026 14:05:38 +0800 Subject: [PATCH 2/4] add stamp command --- Cargo.lock | 528 +++++++++++++++++- Cargo.toml | 3 + crates/bmt/benches/tree_construction.rs | 4 +- crates/bmt/src/lib.rs | 12 +- crates/calendar/src/routes/ots.rs | 23 +- crates/cli/Cargo.toml | 21 +- crates/cli/src/client.rs | 9 + crates/cli/src/commands.rs | 4 + crates/cli/src/commands/stamp.rs | 209 +++++++ crates/cli/src/main.rs | 1 + crates/core/Cargo.toml | 3 + crates/core/src/codec/proof.rs | 27 + crates/core/src/codec/v1.rs | 2 +- crates/core/src/codec/v1/digest.rs | 20 +- crates/core/src/codec/v1/opcode.rs | 28 +- crates/core/src/codec/v1/timestamp/builder.rs | 44 +- crates/core/src/utils.rs | 3 + crates/core/src/utils/hash.rs | 48 ++ crates/stamper/src/lib.rs | 10 +- 19 files changed, 931 insertions(+), 68 deletions(-) create mode 100644 crates/cli/src/client.rs create mode 100644 crates/cli/src/commands/stamp.rs create mode 100644 crates/core/src/utils/hash.rs diff --git a/Cargo.lock b/Cargo.lock index f6e46a2..097f267 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,7 +450,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.17", @@ -519,7 +519,7 @@ dependencies = [ "alloy-transport-ws", "futures", "pin-project", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -795,7 +795,7 @@ checksum = "64b722073c76f2de7e118d546ee1921c50710f97feb32aed50db94cfa5b663e1" dependencies = [ "alloy-json-rpc", "alloy-transport", - "reqwest", + "reqwest 0.12.28", "serde_json", "tower", "tracing", @@ -918,7 +918,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -929,7 +929,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1221,6 +1221,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.8" @@ -1615,6 +1637,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1644,7 +1672,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", ] [[package]] @@ -1654,7 +1693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1761,6 +1800,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "coins-bip32" version = "0.12.0" @@ -1845,6 +1893,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "commonware-codec" version = "0.0.63" @@ -1987,7 +2045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -2049,6 +2107,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2064,6 +2132,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -2230,7 +2307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version 0.4.1", @@ -2706,6 +2783,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2851,6 +2934,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.32.3" @@ -3246,6 +3343,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -3453,6 +3556,28 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -3494,7 +3619,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -3503,7 +3628,7 @@ version = "0.2.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d546793a04a1d3049bd192856f804cfe96356e2cf36b54b4e575155babe9f41" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -3522,6 +3647,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.178" @@ -3695,10 +3826,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3899,6 +4030,21 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.111" @@ -3907,6 +4053,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -3935,7 +4082,7 @@ dependencies = [ "bytes", "http", "opentelemetry", - "reqwest", + "reqwest 0.12.28", "tracing", ] @@ -3953,7 +4100,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest", + "reqwest 0.12.28", "thiserror 2.0.17", "tracing", ] @@ -4210,7 +4357,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4254,6 +4401,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -4426,6 +4583,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -4499,6 +4657,17 @@ dependencies = [ "serde", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.1", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4538,6 +4707,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -4692,6 +4867,45 @@ dependencies = [ "webpki-roots 1.0.4", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -4843,6 +5057,7 @@ version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -4851,6 +5066,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + [[package]] name = "rustls-pki-types" version = "1.13.2" @@ -4861,12 +5088,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4987,7 +5242,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -5158,7 +5426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5169,7 +5437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa1ae819b9870cadc959a052363de870944a1646932d274a4e270f64bf79e5ef" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.11.0-rc.4", ] @@ -5181,7 +5449,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -5193,7 +5461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5204,7 +5472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d43dc0354d88b791216bb5c1bfbb60c0814460cc653ae0ebd71f286d0bd927" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.11.0-rc.4", ] @@ -6052,17 +6320,23 @@ name = "uts-cli" version = "0.1.0" dependencies = [ "alloy-provider", + "bytemuck", "clap", "color-eyre", "digest 0.11.0-rc.4", "eyre", + "futures", "jiff", + "rand 0.10.0", + "reqwest 0.13.2", "ripemd 0.2.0-rc.3", "sha1 0.11.0-rc.3", "sha2 0.11.0-rc.3", "sha3 0.11.0-rc.3", "tokio", "tracing", + "url", + "uts-bmt", "uts-core", ] @@ -6104,7 +6378,9 @@ dependencies = [ "sha2 0.11.0-rc.3", "sha3 0.11.0-rc.3", "thiserror 2.0.17", + "tokio", "tracing", + "uts-bmt", "uts-contracts", ] @@ -6207,7 +6483,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -6268,6 +6553,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "semver 1.0.27", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -6302,6 +6621,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -6469,6 +6797,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6496,6 +6833,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6529,6 +6881,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6541,6 +6899,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6553,6 +6917,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6577,6 +6947,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6589,6 +6965,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6601,6 +6983,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6613,6 +7001,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6640,6 +7034,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.12.1", + "prettyplease", + "syn 2.0.111", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.111", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.12.1", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.1", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index f923f05..3bc242a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,9 @@ itoa = "1.0" jiff = "0.2.20" once_cell = { version = "1.21", default-features = false } paste = "1.0" +rand = "0.10" regex = "1.12" +reqwest = { version = "0.13", default-features = false } rocksdb = "0.24" serde = "1.0" serde-wasm-bindgen = "0.6" @@ -73,6 +75,7 @@ toml = "0.9" tower-http = "0.6" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2.5" wasm-bindgen = "0.2" crypto-common = "0.2.0-rc.5" diff --git a/crates/bmt/benches/tree_construction.rs b/crates/bmt/benches/tree_construction.rs index caf9a11..3f3a33a 100644 --- a/crates/bmt/benches/tree_construction.rs +++ b/crates/bmt/benches/tree_construction.rs @@ -7,7 +7,7 @@ use digest::{Digest, FixedOutputReset, Output}; use sha2::Sha256; use sha3::Keccak256; use std::hint::black_box; -use uts_bmt::FlatMerkleTree; +use uts_bmt::UnorderdMerkleTree; const INPUT_SIZES: &[usize] = &[8, 1024, 65536, 1_048_576]; @@ -33,7 +33,7 @@ where group.bench_function(BenchmarkId::new(id, size), move |b| { // Tree construction is the operation under test. b.iter(|| { - let tree = FlatMerkleTree::::new(black_box(leaves.as_slice())); + let tree = UnorderdMerkleTree::::new(black_box(leaves.as_slice())); black_box(tree); }); }); diff --git a/crates/bmt/src/lib.rs b/crates/bmt/src/lib.rs index d4e4fec..c8d5b27 100644 --- a/crates/bmt/src/lib.rs +++ b/crates/bmt/src/lib.rs @@ -15,7 +15,7 @@ pub const INNER_NODE_PREFIX: u8 = 0x01; /// /// Leaves are **sorted** starting at index `len`. #[derive(Debug, Clone, Default)] -pub struct FlatMerkleTree { +pub struct UnorderdMerkleTree { /// Index 0 is not used, leaves start at index `len`. nodes: Box<[Output]>, len: usize, @@ -28,7 +28,7 @@ pub struct UnhashedFlatMerkleTree { len: usize, } -impl FlatMerkleTree +impl UnorderdMerkleTree where Output: Pod + Copy, { @@ -128,7 +128,7 @@ where Output: Pod + Copy, { /// Finalizes the Merkle tree by hashing internal nodes - pub fn finalize(self) -> FlatMerkleTree { + pub fn finalize(self) -> UnorderdMerkleTree { let mut nodes = self.buffer; let len = self.len; unsafe { @@ -152,7 +152,7 @@ where // SAFETY: initialized all elements. nodes.set_len(2 * len); } - FlatMerkleTree { + UnorderdMerkleTree { nodes: nodes.into_boxed_slice(), len, } @@ -234,7 +234,7 @@ mod tests { ]; leaves.sort_unstable(); - let tree = FlatMerkleTree::::new(&leaves); + let tree = UnorderdMerkleTree::::new(&leaves); // Manually compute the expected root let mut hasher = D::new(); @@ -265,7 +265,7 @@ mod tests { ]; leaves.sort_unstable(); - let tree = FlatMerkleTree::::new(&leaves); + let tree = UnorderdMerkleTree::::new(&leaves); for leaf in &leaves { let mut iter = tree diff --git a/crates/calendar/src/routes/ots.rs b/crates/calendar/src/routes/ots.rs index c25ba27..0587e7d 100644 --- a/crates/calendar/src/routes/ots.rs +++ b/crates/calendar/src/routes/ots.rs @@ -1,5 +1,5 @@ use crate::{AppState, time::current_time_sec}; -use alloy_primitives::{B256, hex}; +use alloy_primitives::B256; use alloy_signer::SignerSync; use axum::{ body::Bytes, @@ -12,7 +12,7 @@ use bytes::BytesMut; use digest::Digest; use sha3::Keccak256; use std::{cell::RefCell, sync::Arc}; -use uts_bmt::{FlatMerkleTree, NodePosition}; +use uts_bmt::UnorderdMerkleTree; use uts_core::{ codec::{ Encode, @@ -107,7 +107,8 @@ pub fn submit_digest_inner(digest: Bytes, signer: impl SignerSync) -> (Bytes, [u let mut bump = bump.borrow_mut(); bump.reset(); - let builder = Timestamp::builder_in(&*bump) + let mut builder = Timestamp::builder_in(&*bump); + builder .prepend(recv_timestamp.to_vec_in(&bump)) .append(undeniable_sig.to_vec_in(&bump)) .keccak256(); @@ -153,9 +154,9 @@ pub async fn get_timestamp( .load_entry(root) .expect("DB error") .expect("bug: entry not found"); - let trie: FlatMerkleTree = entry.trie(); + let trie: UnorderdMerkleTree = entry.trie(); - let mut proof_iter = trie + let proof = trie .get_proof_iter(bytemuck::cast_ref(&*commitment)) .expect("bug: proof not found"); let output = BUMP.with(|bump| { @@ -163,18 +164,8 @@ pub async fn get_timestamp( bump.reset(); let mut builder = Timestamp::builder_in(&*bump); + builder.merkle_proof(proof); - while let Some((side, sibling_hash)) = proof_iter.next() { - builder = match side { - NodePosition::Left => builder - .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(&bump)) - .append(sibling_hash.to_vec_in(&bump)), - NodePosition::Right => builder - .prepend(sibling_hash.to_vec_in(&bump)) - .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(&bump)), - } - .keccak256(); - } let timestamp = builder .attest(EthereumUTSAttestation::new( entry.chain_id, diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 502c7d8..4710edf 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -8,24 +8,39 @@ name = "uts-cli" repository.workspace = true version.workspace = true +[lints] +workspace = true + [[bin]] name = "uts" path = "src/main.rs" [dependencies] alloy-provider = { workspace = true } +bytemuck = { workspace = true } clap = { workspace = true, features = ["derive"] } color-eyre = { workspace = true } digest = { workspace = true } eyre = { workspace = true } +futures = { workspace = true } jiff = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true, default-features = false, features = ["http2"] } ripemd = { workspace = true } sha1 = { workspace = true } sha2 = { workspace = true } sha3 = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } -uts-core = { workspace = true, features = ["std", "ethereum-uts-verifier"] } +url = { workspace = true } +uts-bmt = { workspace = true } +uts-core = { workspace = true, features = ["std", "ethereum-uts-verifier", "io-utils"] } -[lints] -workspace = true +[features] +default = ["reqwest-default-tls"] +reqwest-default-tls = ["reqwest/default-tls"] +reqwest-native-tls = ["reqwest/native-tls"] +reqwest-native-tls-no-alpn = ["reqwest/native-tls-no-alpn"] +reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"] +reqwest-native-tls-vendored-no-alpn = ["reqwest/native-tls-vendored-no-alpn"] +reqwest-rustls = ["reqwest/rustls"] diff --git a/crates/cli/src/client.rs b/crates/cli/src/client.rs new file mode 100644 index 0000000..f0d0e53 --- /dev/null +++ b/crates/cli/src/client.rs @@ -0,0 +1,9 @@ +use reqwest::Client; +use std::sync::LazyLock; + +pub static CLIENT: LazyLock = LazyLock::new(|| { + Client::builder() + .user_agent(concat!("uts/", env!("CARGO_PKG_VERSION"))) + .build() + .expect("Failed to build HTTP client") +}); diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index fe6a209..dbe6afd 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -1,6 +1,7 @@ use clap::Subcommand; mod inspect; +mod stamp; mod verify; #[derive(Debug, Subcommand)] @@ -9,6 +10,8 @@ pub enum Commands { Inspect(inspect::Inspect), /// Verify an ots file against a file Verify(verify::Verify), + /// Create timestamp + Stamp(stamp::Stamp), } impl Commands { @@ -16,6 +19,7 @@ impl Commands { match self { Commands::Inspect(cmd) => cmd.run(), Commands::Verify(cmd) => cmd.run().await, + Commands::Stamp(cmd) => cmd.run().await, } } } diff --git a/crates/cli/src/commands/stamp.rs b/crates/cli/src/commands/stamp.rs new file mode 100644 index 0000000..7c34bc7 --- /dev/null +++ b/crates/cli/src/commands/stamp.rs @@ -0,0 +1,209 @@ +use crate::client::CLIENT; +use bytemuck::Pod; +use clap::{Args, ValueEnum}; +use digest::{Digest, FixedOutputReset, Output}; +use futures::TryFutureExt; +use std::{collections::HashMap, future::ready, io, path::PathBuf, sync::LazyLock, time::Duration}; +use tokio::{fs, io::AsyncWriteExt}; +use url::Url; +use uts_bmt::UnorderdMerkleTree; +use uts_core::{ + codec::{ + Decode, Encode, VersionedProof, + v1::{DetachedTimestamp, DigestHeader, Timestamp, TimestampBuilder, opcode::DigestOpExt}, + }, + utils::{HashAsyncFsExt, Hexed}, +}; + +static DEFAULT_CALENDARS: LazyLock> = LazyLock::new(|| { + vec![ + Url::parse("https://a.pool.opentimestamps.org/").unwrap(), + Url::parse("https://b.pool.opentimestamps.org/").unwrap(), + Url::parse("https://a.pool.eternitywall.com/").unwrap(), + Url::parse("https://ots.btc.catallaxy.com/").unwrap(), + ] +}); + +#[derive(Debug, Args)] +pub struct Stamp { + /// Files to timestamp. May be specified multiple times. + #[arg(value_name = "FILE", num_args = 1..)] + files: Vec, + /// Create timestamp with the aid of a remote calendar. May be specified multiple times. + #[arg(short = 'c', long = "calendar", value_name = "URL", num_args = 0..)] + calendars: Vec, + /// Consider the timestamp complete if at least M calendars reply prior to the timeout + #[arg(short = 'm', default_value = "1")] + quorum: usize, + /// Hasher to use when digesting files. Default is Keccak256. + #[arg(short = 'H', long = "hasher", default_value = "keccak256")] + hasher: Hasher, + /// Timeout in seconds to wait for calendar responses. Default is 60 seconds. + #[arg(long = "timeout", default_value = "5")] + timeout: u64, +} + +#[derive(Default, Debug, Copy, Clone, ValueEnum)] +pub enum Hasher { + Sha1, + Ripemd160, + Sha256, + #[default] + Keccak256, +} + +impl Stamp { + pub async fn run(self) -> eyre::Result<()> { + match self.hasher { + Hasher::Sha1 => self.run_inner::().await, + Hasher::Ripemd160 => self.run_inner::().await, + Hasher::Sha256 => self.run_inner::().await, + Hasher::Keccak256 => self.run_inner::().await, + } + } + + async fn run_inner(self) -> eyre::Result<()> + where + D: Digest + FixedOutputReset + DigestOpExt + Send, + Output: Pod + Copy, + { + let digests = + futures::future::join_all(self.files.iter().map(|f| hash_file::(f.clone()))) + .await + .into_iter() + .collect::, _>>()?; + + for (header, path) in digests.iter().zip(self.files.iter()) { + eprintln!("File: {} {}", header, path.display()); + } + + let mut builders: HashMap = HashMap::from_iter( + self.files + .iter() + .map(|path| (path.clone(), Timestamp::builder())), + ); + + let nonced_digest = builders + .iter_mut() + .zip(digests.iter()) + .map(|((_, builder), digest)| { + let mut hasher = D::new(); + Digest::update(&mut hasher, digest.digest()); + let nonce: [u8; 32] = rand::random(); + Digest::update(&mut hasher, &nonce); + builder.append(nonce.to_vec()).digest::(); + hasher.finalize() + }) + .collect::>(); + + let internal_tire = UnorderdMerkleTree::::new(&nonced_digest); + let root = internal_tire.root(); + eprintln!("Internal Merkle root: {}", Hexed(root)); + + for ((_, builder), leaf) in builders.iter_mut().zip(nonced_digest) { + let proof = internal_tire.get_proof_iter(&leaf).expect("infallible"); + builder.merkle_proof(proof); + } + + let calendars = if self.calendars.is_empty() { + &*DEFAULT_CALENDARS + } else { + &*self.calendars + }; + + if self.quorum > calendars.len() { + eyre::bail!( + "Quorum of {} cannot be achieved with only {} calendars", + self.quorum, + self.calendars.len() + ); + } + + let stamps = futures::future::join_all( + calendars + .into_iter() + .map(|calendar| request_calendar(calendar.clone(), self.timeout, root)), + ) + .await + .into_iter() + .filter_map(|res| res.ok()) + .collect::>(); + if stamps.len() < self.quorum { + eyre::bail!( + "Only received {} valid responses from calendars, which does not meet the quorum of {}", + stamps.len(), + self.quorum + ); + } + let merged = Timestamp::merge(stamps); + + let writes = + futures::future::join_all(builders.into_iter().zip(digests).map( + |((path, builder), header)| write_stamp(path, builder, merged.clone(), header), + )) + .await; + for (res, path) in writes.into_iter().zip(self.files.iter()) { + match res { + Ok(_) => eprintln!("Successfully wrote timestamp for {}", path.display()), + Err(e) => eprintln!("Failed to write timestamp for {}: {}", path.display(), e), + } + } + + Ok(()) + } +} + +async fn hash_file(path: PathBuf) -> io::Result { + let mut hasher = D::new(); + let file = fs::File::open(path).await?; + HashAsyncFsExt::update(&mut hasher, file).await?; + Ok(DigestHeader::new::(hasher.finalize())) +} + +async fn request_calendar(calendar: Url, timeout: u64, root: &[u8]) -> eyre::Result { + eprintln!("Submitting to remote calendar: {calendar}"); + let url = calendar.join(&"digest")?; + let response = CLIENT + .post(url) + .header("Accept", "application/vnd.opentimestamps.v1") + .body(root.to_vec()) + .timeout(Duration::from_secs(timeout)) + .send() + .and_then(|r| ready(r.error_for_status())) + .and_then(|r| r.bytes()) + .await + .inspect_err(|e| { + if e.is_status() { + eprintln!("Calendar {} responded with error: {}", calendar, e); + } else if e.is_timeout() { + eprintln!("Calendar {} timed out after {} seconds", calendar, timeout); + } else { + eprintln!("Failed to submit to calendar {}: {}", calendar, e); + } + })?; + + let ts = Timestamp::decode(&mut &*response).inspect_err(|e| { + eprintln!( + "Failed to decode response from calendar {}: {}", + calendar, e + ); + })?; + Ok(ts) +} + +async fn write_stamp( + mut path: PathBuf, + builder: TimestampBuilder, + merged: Timestamp, + header: DigestHeader, +) -> eyre::Result<()> { + let timestamp = builder.concat(merged.clone()); + let timestamp = DetachedTimestamp::from_parts(header, timestamp); + let timestamp = VersionedProof::::new(timestamp); + let mut buf = Vec::new(); + timestamp.encode(&mut buf)?; + path.add_extension("ots"); + let mut file = fs::File::create_new(path).await?; + file.write_all(&buf).await?; + Ok(()) +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index fa0de7b..810c9a2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,6 +2,7 @@ use crate::commands::Commands; use clap::Parser; +mod client; mod commands; #[derive(Debug, Parser)] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index cd7dbd7..1d3bb43 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -28,13 +28,16 @@ sha1.workspace = true sha2.workspace = true sha3.workspace = true thiserror.workspace = true +tokio = { workspace = true, optional = true } tracing = { workspace = true, optional = true } +uts-bmt = { workspace = true } uts-contracts = { workspace = true, optional = true } [features] bytes = ["dep:bytes"] default = ["std"] ethereum-uts-verifier = ["verifier", "dep:alloy-provider", "dep:alloy-rpc-types-eth", "dep:alloy-sol-types", "dep:uts-contracts"] +io-utils = ["dep:tokio", "tokio/fs"] serde = ["dep:serde", "dep:serde_with", "serde/derive", "serde_with/hex"] std = [] tracing = ["dep:tracing"] diff --git a/crates/core/src/codec/proof.rs b/crates/core/src/codec/proof.rs index 7716712..ac0ec6a 100644 --- a/crates/core/src/codec/proof.rs +++ b/crates/core/src/codec/proof.rs @@ -21,6 +21,33 @@ pub struct VersionedProof, A: Allocator = Global> { allocator: A, } +impl> VersionedProof { + /// Creates a new versioned proof with the global allocator. + pub fn new(proof: T) -> Self { + VersionedProof { + proof, + allocator: Global, + } + } +} + +impl, A: Allocator> VersionedProof { + /// Creates a new versioned proof with the specified allocator. + pub fn new_with_allocator(proof: T, allocator: A) -> Self { + VersionedProof { proof, allocator } + } + + /// Returns a reference to the proof payload. + pub fn proof(&self) -> &T { + &self.proof + } + + /// Returns a reference to the allocator used by this proof. + pub fn allocator(&self) -> &A { + &self.allocator + } +} + impl, A: Allocator + Clone> DecodeIn for VersionedProof { fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { decoder.assert_magic()?; diff --git a/crates/core/src/codec/v1.rs b/crates/core/src/codec/v1.rs index 13dae63..9091bee 100644 --- a/crates/core/src/codec/v1.rs +++ b/crates/core/src/codec/v1.rs @@ -12,7 +12,7 @@ pub use attestation::{ }; pub use detached_timestamp::DetachedTimestamp; pub use digest::DigestHeader; -pub use timestamp::{Step, Timestamp}; +pub use timestamp::{Step, Timestamp, builder::TimestampBuilder}; /// Error indicating that finalization of a timestamp failed due to conflicting inputs. #[derive(Debug)] diff --git a/crates/core/src/codec/v1/digest.rs b/crates/core/src/codec/v1/digest.rs index 05792fa..93a6118 100644 --- a/crates/core/src/codec/v1/digest.rs +++ b/crates/core/src/codec/v1/digest.rs @@ -1,10 +1,14 @@ use crate::{ - codec::{DecodeIn, Decoder, Encode, Encoder, v1::opcode::DigestOp}, + codec::{ + DecodeIn, Decoder, Encode, Encoder, + v1::opcode::{DigestOp, DigestOpExt}, + }, error::{DecodeError, EncodeError}, utils::Hexed, }; use alloc::alloc::Allocator; use core::fmt; +use digest::{Output, typenum::Unsigned}; /// Header describing the digest that anchors a timestamp. #[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -14,9 +18,9 @@ use core::fmt; derive(serde::Serialize, serde::Deserialize) )] pub struct DigestHeader { - kind: DigestOp, + pub(crate) kind: DigestOp, #[cfg_attr(feature = "serde", serde_as(as = "serde_with::hex::Hex"))] - digest: [u8; 32], + pub(crate) digest: [u8; 32], } impl fmt::Debug for DigestHeader { @@ -35,6 +39,16 @@ impl fmt::Display for DigestHeader { } impl DigestHeader { + /// Creates a new digest header from the given digest output. + pub fn new(digest: Output) -> Self { + let mut digest_bytes = [0u8; 32]; + digest_bytes[..D::OutputSize::USIZE].copy_from_slice(&digest); + DigestHeader { + kind: D::OPCODE, + digest: digest_bytes, + } + } + /// Returns the digest opcode recorded in the header. pub fn kind(&self) -> DigestOp { self.kind diff --git a/crates/core/src/codec/v1/opcode.rs b/crates/core/src/codec/v1/opcode.rs index b7b0a34..237bd34 100644 --- a/crates/core/src/codec/v1/opcode.rs +++ b/crates/core/src/codec/v1/opcode.rs @@ -8,7 +8,7 @@ use crate::{ }; use alloc::{alloc::Allocator, vec::Vec}; use core::{fmt, hint::unreachable_unchecked}; -use digest::{Digest, OutputSizeUser, typenum::Unsigned}; +use digest::Digest; use ripemd::Ripemd160; use sha1::Sha1; use sha2::Sha256; @@ -234,6 +234,13 @@ impl DigestOp { } } +/// Extension trait for `Digest` implementors to get the corresponding `DigestOp`. +pub trait DigestOpExt: Digest { + const OPCODE: DigestOp; + + fn opcode() -> DigestOp; +} + macro_rules! define_opcodes { ($($val:literal => $variant:ident),* $(,)?) => { $( @@ -307,9 +314,10 @@ macro_rules! define_digest_opcodes { /// Returns the output length of the digest in bytes. #[inline] pub const fn output_size(&self) -> usize { + use digest::typenum::Unsigned; paste::paste! { match *self { - $( Self::$variant => <[<$variant:camel>] as OutputSizeUser>::OutputSize::USIZE, )* + $( Self::$variant => <[<$variant:camel>] as ::digest::OutputSizeUser>::OutputSize::USIZE, )* // SAFETY: unreachable as all variants are covered. _ => unsafe { unreachable_unchecked() } } @@ -336,6 +344,18 @@ macro_rules! define_digest_opcodes { } } } + paste::paste! { + $( + impl DigestOpExt for [<$variant:camel>] { + const OPCODE: DigestOp = DigestOp::$variant; + + #[inline] + fn opcode() -> DigestOp { + DigestOp::$variant + } + } + )* + } }; } @@ -343,7 +363,7 @@ macro_rules! impl_simple_step { ($variant:ident) => {paste::paste! { impl $crate::codec::v1::timestamp::builder::TimestampBuilder { #[doc = concat!("Push the `", stringify!($variant), "` opcode.")] - pub fn [< $variant:lower >](self) -> Self { + pub fn [< $variant:lower >](&mut self) -> &mut Self { self.push_step(OpCode::[<$variant>]) } } @@ -359,7 +379,7 @@ macro_rules! impl_step_with_data { ($variant:ident) => {paste::paste! { impl $crate::codec::v1::timestamp::builder::TimestampBuilder { #[doc = concat!("Push the `", stringify!($variant), "` opcode.")] - pub fn [< $variant:lower >](self, data: ::alloc::vec::Vec) -> Self { + pub fn [< $variant:lower >](&mut self, data: ::alloc::vec::Vec) -> &mut Self { self.push_immediate_step(OpCode::[<$variant>], data) } } diff --git a/crates/core/src/codec/v1/timestamp/builder.rs b/crates/core/src/codec/v1/timestamp/builder.rs index 9275f37..9c36254 100644 --- a/crates/core/src/codec/v1/timestamp/builder.rs +++ b/crates/core/src/codec/v1/timestamp/builder.rs @@ -1,11 +1,16 @@ //! Timestamp Builder use crate::{ - codec::v1::{Attestation, Timestamp, opcode::OpCode, timestamp::Step}, + codec::v1::{ + Attestation, Timestamp, + opcode::{DigestOpExt, OpCode}, + timestamp::Step, + }, error::EncodeError, utils::OnceLock, }; use alloc::alloc::{Allocator, Global}; +use uts_bmt::{NodePosition, SiblingIter}; #[derive(Debug, Clone)] pub struct TimestampBuilder { @@ -31,7 +36,7 @@ impl TimestampBuilder { /// # Panics /// /// Panics if the opcode is not an opcode with immediate data. - pub(crate) fn push_immediate_step(mut self, op: OpCode, data: Vec) -> Self { + pub(crate) fn push_immediate_step(&mut self, op: OpCode, data: Vec) -> &mut Self { assert!(op.has_immediate()); self.steps.push(LinearStep { op, data }); self @@ -44,7 +49,7 @@ impl TimestampBuilder { /// Panics if: /// - the opcode is control opcode /// - the opcode is an opcode with immediate data - pub(crate) fn push_step(mut self, op: OpCode) -> Self { + pub fn push_step(&mut self, op: OpCode) -> &mut Self { self.steps.push(LinearStep { op, data: Vec::new_in(self.allocator().clone()), @@ -52,6 +57,29 @@ impl TimestampBuilder { self } + /// Pushes a new digest step to the timestamp. + pub fn digest(&mut self) -> &mut Self { + self.push_step(D::OPCODE.to_opcode()); + self + } + + /// Pushes the steps corresponding to the given Merkle proof to the timestamp. + pub fn merkle_proof(&mut self, mut proof: SiblingIter<'_, D>) -> &mut Self { + let alloc = self.allocator().clone(); + while let Some((side, sibling_hash)) = proof.next() { + match side { + NodePosition::Left => self + .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(alloc.clone())) + .append(sibling_hash.to_vec_in(alloc.clone())), + NodePosition::Right => self + .prepend(sibling_hash.to_vec_in(alloc.clone())) + .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(alloc.clone())), + } + .digest::(); + } + self + } + /// Computes the commitment of the timestamp for the given input. /// /// In this context, the **commitment** is the deterministic result of @@ -85,9 +113,15 @@ impl TimestampBuilder { self, attestation: T, ) -> Result, EncodeError> { + let current = Timestamp::Attestation(attestation.to_raw_in(self.allocator().clone())?); + Ok(self.concat(current)) + } + + /// Append the given timestamp after the steps in the builder. + pub fn concat(self, timestamp: Timestamp) -> Timestamp { let alloc = self.allocator().clone(); - let mut current = Timestamp::Attestation(attestation.to_raw_in(alloc.clone())?); + let mut current = timestamp; for step in self.steps.into_iter().rev() { let step_node = Step { @@ -103,7 +137,7 @@ impl TimestampBuilder { current = Timestamp::Step(step_node); } - Ok(current) + current } #[inline] diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 6863042..c11db03 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -3,3 +3,6 @@ pub use hex::Hexed; mod sync; pub use sync::OnceLock; + +mod hash; +pub use hash::{HashAsyncFsExt, HashFsExt}; diff --git a/crates/core/src/utils/hash.rs b/crates/core/src/utils/hash.rs new file mode 100644 index 0000000..5fc5d7d --- /dev/null +++ b/crates/core/src/utils/hash.rs @@ -0,0 +1,48 @@ +use digest::Digest; +use std::io::{self, Read}; + +pub trait HashFsExt { + fn update(&mut self, reader: R) -> io::Result<()>; +} + +impl HashFsExt for D { + fn update(&mut self, mut reader: R) -> io::Result<()> { + let mut buffer = [0u8; 64 * 1024]; // 64KB buffer + loop { + let bytes_read = reader.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + self.update(&buffer[..bytes_read]); + } + Ok(()) + } +} + +#[cfg(feature = "io-utils")] +pub trait HashAsyncFsExt { + fn update( + &mut self, + reader: R, + ) -> impl Future> + Send; +} + +#[cfg(feature = "io-utils")] +impl HashAsyncFsExt for D { + async fn update( + &mut self, + mut reader: R, + ) -> io::Result<()> { + use tokio::io::AsyncReadExt; + + let mut buffer = [0u8; 64 * 1024]; // 64KB buffer + loop { + let bytes_read = reader.read(&mut buffer).await?; + if bytes_read == 0 { + break; + } + self.update(&buffer[..bytes_read]); + } + Ok(()) + } +} diff --git a/crates/stamper/src/lib.rs b/crates/stamper/src/lib.rs index d050bc6..0304fb5 100644 --- a/crates/stamper/src/lib.rs +++ b/crates/stamper/src/lib.rs @@ -20,7 +20,7 @@ use std::{ time::Duration, }; use tokio::time::{Interval, MissedTickBehavior}; -use uts_bmt::FlatMerkleTree; +use uts_bmt::UnorderdMerkleTree; use uts_contracts::uts::UniversalTimestamps; use uts_core::utils::Hexed; use uts_journal::reader::JournalReader; @@ -42,7 +42,7 @@ pub struct Stamper { /// Storage for merkle trees and leaf->root mappings storage: Arc, /// FIFO cache of recent merkle trees - cache: VecDeque>, + cache: VecDeque>, /// FIFO cache index of recent merkle trees cache_index: HashMap, /// The contract @@ -81,13 +81,13 @@ pub struct MerkleEntry<'a> { impl MerkleEntry<'_> { /// Get the Merkle tree from the entry - pub fn trie(&self) -> FlatMerkleTree + pub fn trie(&self) -> UnorderdMerkleTree where D: Digest + FixedOutputReset, Output: Pod + Copy, { // SAFETY: We trust that the data in the database is valid, and that the trie was serialized correctly. - unsafe { FlatMerkleTree::from_raw_bytes(&self.trie) } + unsafe { UnorderdMerkleTree::from_raw_bytes(&self.trie) } } } @@ -237,7 +237,7 @@ where } debug_assert_eq!(buffer.len(), target_size); - let merkle_tree = FlatMerkleTree::::new_unhashed(bytemuck::cast_slice(&buffer)); + let merkle_tree = UnorderdMerkleTree::::new_unhashed(bytemuck::cast_slice(&buffer)); let storage = self.storage.clone(); let merkle_tree = tokio::task::spawn_blocking(move || { From 3aca86268c4be4f6324670d9a9ce94364b664d10 Mon Sep 17 00:00:00 2001 From: lightsing Date: Sun, 22 Feb 2026 14:26:42 +0800 Subject: [PATCH 3/4] fix --- crates/bmt/benches/tree_construction.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bmt/benches/tree_construction.rs b/crates/bmt/benches/tree_construction.rs index 3f3a33a..7d06e67 100644 --- a/crates/bmt/benches/tree_construction.rs +++ b/crates/bmt/benches/tree_construction.rs @@ -1,4 +1,5 @@ //! Benchmark for Merkle tree construction. +use bytemuck::Pod; use criterion::{ BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, measurement::WallTime, @@ -25,7 +26,7 @@ fn benchmark(c: &mut Criterion) { fn bench_digest(group: &mut BenchmarkGroup<'_, WallTime>, id: &str) where D: Digest + FixedOutputReset, - Output: Copy, + Output: Pod + Copy, { for &size in INPUT_SIZES { let leaves = generate_leaves::(size); From 7a02024f2727a1817a5b317fb7a9eb4171a9ec66 Mon Sep 17 00:00:00 2001 From: lightsing Date: Sun, 22 Feb 2026 19:02:08 +0800 Subject: [PATCH 4/4] add upgrade --- crates/cli/src/commands.rs | 4 + crates/cli/src/commands/stamp.rs | 15 ++- crates/cli/src/commands/upgrade.rs | 99 +++++++++++++++++++ .../core/src/codec/v1/detached_timestamp.rs | 12 ++- crates/core/src/codec/v1/timestamp.rs | 46 ++++++++- crates/core/src/utils.rs | 4 +- crates/stamper/src/lib.rs | 4 +- 7 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 crates/cli/src/commands/upgrade.rs diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index dbe6afd..a4f8391 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -2,6 +2,7 @@ use clap::Subcommand; mod inspect; mod stamp; +mod upgrade; mod verify; #[derive(Debug, Subcommand)] @@ -12,6 +13,8 @@ pub enum Commands { Verify(verify::Verify), /// Create timestamp Stamp(stamp::Stamp), + /// Upgrade timestamp + Upgrade(upgrade::Upgrade), } impl Commands { @@ -20,6 +23,7 @@ impl Commands { Commands::Inspect(cmd) => cmd.run(), Commands::Verify(cmd) => cmd.run().await, Commands::Stamp(cmd) => cmd.run().await, + Commands::Upgrade(cmd) => cmd.run().await, } } } diff --git a/crates/cli/src/commands/stamp.rs b/crates/cli/src/commands/stamp.rs index 7c34bc7..cdb2980 100644 --- a/crates/cli/src/commands/stamp.rs +++ b/crates/cli/src/commands/stamp.rs @@ -17,10 +17,11 @@ use uts_core::{ static DEFAULT_CALENDARS: LazyLock> = LazyLock::new(|| { vec![ - Url::parse("https://a.pool.opentimestamps.org/").unwrap(), - Url::parse("https://b.pool.opentimestamps.org/").unwrap(), - Url::parse("https://a.pool.eternitywall.com/").unwrap(), - Url::parse("https://ots.btc.catallaxy.com/").unwrap(), + // Url::parse("https://a.pool.opentimestamps.org/").unwrap(), + // Url::parse("https://b.pool.opentimestamps.org/").unwrap(), + // Url::parse("https://a.pool.eternitywall.com/").unwrap(), + // Url::parse("https://ots.btc.catallaxy.com/").unwrap(), + Url::parse("http://127.0.0.1:3000/").unwrap(), ] }); @@ -135,7 +136,11 @@ impl Stamp { self.quorum ); } - let merged = Timestamp::merge(stamps); + let merged = if stamps.len() == 1 { + stamps.into_iter().next().unwrap() + } else { + Timestamp::merge(stamps) + }; let writes = futures::future::join_all(builders.into_iter().zip(digests).map( diff --git a/crates/cli/src/commands/upgrade.rs b/crates/cli/src/commands/upgrade.rs new file mode 100644 index 0000000..cc939a4 --- /dev/null +++ b/crates/cli/src/commands/upgrade.rs @@ -0,0 +1,99 @@ +use crate::client::CLIENT; +use clap::Args; +use eyre::bail; +use futures::TryFutureExt; +use reqwest::StatusCode; +use std::{fs, future::ready, path::PathBuf, time::Duration}; +use url::Url; +use uts_core::{ + codec::{ + Decode, Encode, VersionedProof, + v1::{Attestation, DetachedTimestamp, PendingAttestation, Timestamp}, + }, + utils::Hexed, +}; + +#[derive(Debug, Args)] +pub struct Upgrade { + /// Files to timestamp. May be specified multiple times. + #[arg(value_name = "FILE", num_args = 1..)] + files: Vec, + /// Timeout in seconds to wait for calendar responses. Default is 5 seconds. + #[arg(long = "timeout", default_value = "5")] + timeout: u64, +} + +impl Upgrade { + pub async fn run(self) -> eyre::Result<()> { + // timestamp files are small, so we can read them all synchronously before upgrading. + let files = self + .files + .iter() + .map(|path| fs::read(path)) + .collect::, _>>()?; + + let results = futures::future::join_all( + self.files + .iter() + .cloned() + .zip(files) + .into_iter() + .map(|(path, file)| upgrade_one(path, file, self.timeout)), + ) + .await + .into_iter() + .collect::>(); + for (path, result) in self.files.iter().zip(results) { + match result { + Ok(_) => eprintln!("Upgraded: {}", path.display()), + Err(e) => eprintln!("Failed to upgrade {}: {e}", path.display()), + } + } + Ok(()) + } +} + +async fn upgrade_one(path: PathBuf, file: Vec, timeout: u64) -> eyre::Result<()> { + let mut proof = VersionedProof::::decode(&mut &*file)?; + + for step in proof.proof.pending_attestations_mut() { + let pending_uri = { + let Timestamp::Attestation(attestation) = step else { + unreachable!("bug: PendingAttestationIterMut should only yield Attestations"); + }; + let commitment = attestation.value().expect("finalized when decode"); + let pending_uri = PendingAttestation::from_raw(&*attestation)?.uri; + Url::parse(&pending_uri)?.join(&format!("timestamp/{}", Hexed(commitment)))? + }; + + let result = CLIENT + .get(pending_uri) + .header("Accept", "application/vnd.opentimestamps.v1") + .timeout(Duration::from_secs(timeout)) + .send() + .and_then(|r| ready(r.error_for_status())) + .and_then(|r| r.bytes()) + .await; + + match result { + Ok(response) => { + let attestation = Timestamp::decode(&mut &*response)?; + *step = Timestamp::merge(vec![attestation, step.clone()]) + } + Err(e) => { + if let Some(status) = e.status() + && status == StatusCode::NOT_FOUND + { + bail!("calendar not ready yet."); + } + return Err(e.into()); + } + } + } + + let mut buf = Vec::new(); + proof.encode(&mut buf)?; + tokio::fs::write(path, buf).await?; + + Ok(()) +} diff --git a/crates/core/src/codec/v1/detached_timestamp.rs b/crates/core/src/codec/v1/detached_timestamp.rs index 528c499..353ac91 100644 --- a/crates/core/src/codec/v1/detached_timestamp.rs +++ b/crates/core/src/codec/v1/detached_timestamp.rs @@ -4,7 +4,7 @@ use crate::codec::{ }; use alloc::alloc::{Allocator, Global}; use core::{fmt, fmt::Formatter}; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; /// A file containing a timestamp for another file /// Contains a timestamp, along with a header and the digest of the file. @@ -29,7 +29,9 @@ impl DecodeIn for DetachedTimestamp { ) -> Result { let header = DigestHeader::decode(decoder)?; let timestamp = Timestamp::decode_in(decoder, alloc)?; - Ok(DetachedTimestamp { header, timestamp }) + let detached = DetachedTimestamp { header, timestamp }; + detached.finalize(); + Ok(detached) } } @@ -120,6 +122,12 @@ impl Deref for DetachedTimestamp { } } +impl DerefMut for DetachedTimestamp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.timestamp + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index 849b821..0ca4e2a 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -1,7 +1,10 @@ //! ** The implementation here is subject to change as this is a read-only version. ** use crate::{ - codec::v1::{FinalizationError, MayHaveInput, attestation::RawAttestation, opcode::OpCode}, + codec::v1::{ + Attestation, FinalizationError, MayHaveInput, PendingAttestation, + attestation::RawAttestation, opcode::OpCode, + }, utils::{Hexed, OnceLock}, }; use alloc::{alloc::Global, vec::Vec}; @@ -150,6 +153,16 @@ impl Timestamp { pub fn attestations(&self) -> AttestationIter<'_, A> { AttestationIter { stack: vec![self] } } + + /// Iterates over all pending attestation steps in this timestamp. + /// + /// # Note + /// + /// This iterator will yield `Timestamp` instead of `RawAttestation`. + #[inline] + pub fn pending_attestations_mut(&mut self) -> PendingAttestationIterMut<'_, A> { + PendingAttestationIterMut { stack: vec![self] } + } } impl Timestamp { @@ -276,6 +289,11 @@ impl Step { self.next.as_slice() } + /// Returns the next timestamps of this step. + pub fn next_mut(&mut self) -> &mut [Timestamp] { + self.next.as_mut_slice() + } + /// Returns the allocator used by this step. pub fn allocator(&self) -> &A { self.data.allocator() @@ -311,3 +329,29 @@ impl<'a, A: Allocator> Iterator for AttestationIter<'a, A> { None } } + +pub struct PendingAttestationIterMut<'a, A: Allocator> { + stack: Vec<&'a mut Timestamp>, +} + +impl<'a, A: Allocator> Iterator for PendingAttestationIterMut<'a, A> { + type Item = &'a mut Timestamp; + + fn next(&mut self) -> Option { + while let Some(ts) = self.stack.pop() { + match ts { + Timestamp::Step(step) => { + for next in step.next_mut().iter_mut().rev() { + self.stack.push(next); + } + } + Timestamp::Attestation(attestation) => { + if attestation.tag == PendingAttestation::TAG { + return Some(ts); + } + } + } + } + None + } +} diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index c11db03..1b50d53 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -5,4 +5,6 @@ mod sync; pub use sync::OnceLock; mod hash; -pub use hash::{HashAsyncFsExt, HashFsExt}; +#[cfg(feature = "io-utils")] +pub use hash::HashAsyncFsExt; +pub use hash::HashFsExt; diff --git a/crates/stamper/src/lib.rs b/crates/stamper/src/lib.rs index 0304fb5..6211801 100644 --- a/crates/stamper/src/lib.rs +++ b/crates/stamper/src/lib.rs @@ -94,12 +94,12 @@ impl MerkleEntry<'_> { /// Errors that can occur during storage operations #[derive(Debug, thiserror::Error)] pub enum StorageError { + /// Errors from RocksDB #[error(transparent)] Rocks(#[from] rocksdb::Error), + /// Errors from bitcode serialization/deserialization #[error(transparent)] Bitcode(#[from] bitcode::Error), - #[error("invalid data")] - InvalidData, } /// Extension trait for DB to load Merkle entries and leaf->root mappings