Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 31 additions & 25 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ alloy-signer-local = "1.1"
auto_impl = "1.3"
axum = "0.8"
axum-extra = "0.12"
bump-scope = { version = "1.5", features = ["nightly"] }
bytes = "1.11"
cfg-if = "1.0"
clap = { version = "4.5", features = ["derive"] }
Expand All @@ -44,12 +45,12 @@ const_format = "0.2"
criterion = { version = "0.8", features = ["html_reports"] }
eyre = "0.6"
hex = "0.4"
itoa = "1.0"
once_cell = { version = "1.21", default-features = false }
paste = "1.0"
regex = "1.12"
serde = "1.0"
serde_with = "3.16"
smallvec = { version = "1.15", features = ["union", "const_generics", "const_new"] }
strum = "0.27"
thiserror = "2"
tokio = { version = "1", features = ["rt"] }
Expand Down
8 changes: 5 additions & 3 deletions crates/calendar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ alloy-primitives = { workspace = true }
alloy-signer = { workspace = true }
alloy-signer-local = { workspace = true }
axum = { workspace = true, features = ["macros"] }
axum-extra = { workspace = true }
bump-scope.workspace = true
bytes = { workspace = true }
eyre = { workspace = true }
itoa = { workspace = true }
sha3 = { workspace = true }
smallvec = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tower-http = { workspace = true, features = ["limit"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
uts-core = { workspace = true, features = ["bytes"] }

[lints]
workspace = true

[features]
performance = ["tracing/release_max_level_info"]
21 changes: 20 additions & 1 deletion crates/calendar/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
#![feature(allocator_api)]
//! Calendar server

#[macro_use]
extern crate tracing;

use alloy_primitives::b256;
use alloy_signer::k256::ecdsa::SigningKey;
use alloy_signer_local::LocalSigner;
use axum::{
Router,
extract::DefaultBodyLimit,
routing::{get, post},
};
use std::sync::Arc;

mod routes;
pub mod time;

/// Application state shared across handlers.
#[derive(Debug)]
pub struct AppState {
signer: LocalSigner<SigningKey>,
}

#[tokio::main]
async fn main() -> eyre::Result<()> {
tracing_subscriber::fmt::init();

tokio::spawn(time::updater());

let signer = LocalSigner::from_bytes(&b256!(
"9ba9926331eb5f4995f1e358f57ba1faab8b005b51928d2fdaea16e69a6ad225"
))?;

let app = Router::new()
.route(
"/digest",
Expand All @@ -24,7 +42,8 @@ async fn main() -> eyre::Result<()> {
.route(
"/timestamp/{hex_commitment}",
get(routes::ots::get_timestamp),
);
)
.with_state(Arc::new(AppState { signer }));

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;

Expand Down
143 changes: 77 additions & 66 deletions crates/calendar/src/routes/ots.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
use alloy_primitives::{Keccak256, b256};
use crate::{AppState, time::current_time_sec};
use alloy_signer::SignerSync;
use alloy_signer_local::LocalSigner;
use axum::body::Bytes;
use axum::{body::Bytes, extract::State};
use bump_scope::Bump;
use bytes::BytesMut;
use smallvec::SmallVec;
use std::time::SystemTime;
use tracing::Level;
use sha3::{Digest, Keccak256};
use std::{cell::RefCell, sync::Arc};
use uts_core::{
codec::{
Encoder,
v1::{Attestation, PendingAttestation, opcode::OpCode},
Encode,
v1::{PendingAttestation, Timestamp},
},
utils::Hexed,
};

pub const MAX_DIGEST_SIZE: usize = 64; // e.g., SHA3-512
const ERC2098_SIGNATURE_SIZE: usize = 64;

// Test this with official ots client:
// ots stamp -c "http://localhost:3000/" -m 1 <input_file>
Expand All @@ -36,74 +34,87 @@ const ERC2098_SIGNATURE_SIZE: usize = 64;
// result c15b4e8b93e9aaee5b8c736f5b73e5f313062e389925a0b1fc6495053f99d352
// result attested by Pending: update URI https://localhost:3000
// ```
#[instrument(level = Level::TRACE, skip_all)]
pub async fn submit_digest(digest: Bytes) -> Bytes {
const MAX_MESSAGE_SIZE: usize = MAX_DIGEST_SIZE + size_of::<u64>() + ERC2098_SIGNATURE_SIZE;
pub async fn submit_digest(State(state): State<Arc<AppState>>, digest: Bytes) -> Bytes {
let (output, _commitment) = submit_digest_inner(digest, &state.signer);
// TODO: submit commitment to journal
output
}

// TODO: We need to benchmark this.
pub fn submit_digest_inner(digest: Bytes, signer: impl SignerSync) -> (Bytes, [u8; 32]) {
const PRE_ALLOCATION_SIZE_HINT: usize = 4096;
thread_local! {
// We don't have `.await` in this function, so it's safe to borrow thread local.
static BUMP: RefCell<Bump> = RefCell::new(Bump::with_size(PRE_ALLOCATION_SIZE_HINT));
static HASHER: RefCell<Keccak256> = RefCell::new(Keccak256::new());
}

let uri = "https://localhost:3000".to_string();
// ots uses 32-bit unix time, but we use u64 here for future proofing, as it's not part of the ots spec.
let recv_timestamp = current_time_sec().to_le_bytes();

let buf_size = 1 // OpCode::PREPEND
+ 1 // length of u64 length in leb128
+ 8 // u64 timestamp
+ 1 // OpCode::APPEND
+ 1 // length of signature length in leb128
+ ERC2098_SIGNATURE_SIZE // signature
+ 1 // FIXME: TBD: OpCode::KECCAK256
+ 1 // OpCode::ATTESTATION
+ 8 // Pending tag
+ 1 // length of packed ATTESTATION data length in leb128
+ (1 + uri.len()); // length of uri in leb128 + uri bytes
let attestation = PendingAttestation { uri: uri.into() };
let undeniable_sig = {
// sign_message_sync invokes heap allocation, so manually hash it.
const EIP191_PREFIX: &str = "\x19Ethereum Signed Message:\n";
let hash = HASHER.with(|hasher| {
let mut hasher = hasher.borrow_mut();
hasher.update(EIP191_PREFIX.as_bytes());
match digest.len() {
// 32 + 8
32 => hasher.update(b"40"),
// 64 + 8
64 => hasher.update(b"72"),
_ => {
let length = digest.len() + size_of::<u64>();
let mut buffer = itoa::Buffer::new();
let printed = buffer.format(length);
hasher.update(printed.as_bytes());
}
}
hasher.update(recv_timestamp);
hasher.update(&digest);
hasher.finalize_reset()
});

let mut timestamp = BytesMut::with_capacity(buf_size);
let undeniable_sig = signer.sign_hash_sync(&hash.0.into()).unwrap();
undeniable_sig.as_erc2098()
};

let mut pending_attestation = SmallVec::<[u8; MAX_MESSAGE_SIZE]>::new();
#[cfg(any(debug_assertions, not(feature = "performance")))]
trace!(
recv_timestamp = ?Hexed(&recv_timestamp),
digest = ?Hexed(&digest),
undeniable_sig = ?Hexed(&undeniable_sig),
);

// ots uses 32-bit unix time, but we use u64 here for future proofing, as it's not part of the ots spec.
let recv_timestamp: u64 = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Clock MUST not go backwards")
.as_secs();
trace!(recv_timestamp);
let recv_timestamp = recv_timestamp.to_le_bytes();
timestamp.encode(OpCode::PREPEND).unwrap();
timestamp.encode_bytes(recv_timestamp).unwrap();
pending_attestation.extend(recv_timestamp);
BUMP.with(|bump| {
let mut bump = bump.borrow_mut();
bump.reset();

trace!(digest = ?Hexed(&digest));
pending_attestation.extend_from_slice(&digest);
let builder = Timestamp::builder_in(&*bump)
.prepend(recv_timestamp.to_vec_in(&bump))
.append(undeniable_sig.to_vec_in(&bump))
.keccak256();

let signer = LocalSigner::from_bytes(&b256!(
"9ba9926331eb5f4995f1e358f57ba1faab8b005b51928d2fdaea16e69a6ad225"
))
.unwrap(); // TODO: load from app state
let undeniable_sig = signer.sign_message_sync(&digest).unwrap();
let undeniable_sig = undeniable_sig.as_erc2098();
trace!(undeniable_sig = ?Hexed(&undeniable_sig));
timestamp.encode(OpCode::APPEND).unwrap();
timestamp.encode_bytes(undeniable_sig).unwrap();
pending_attestation.extend(undeniable_sig);
let mut commitment = [0u8; 32];
commitment.copy_from_slice(&builder.commitment(&digest));

trace!(pending_attestation = ?Hexed(&pending_attestation));
let timestamp = builder
.attest(PendingAttestation {
uri: "https://localhost:3000".into(),
})
.unwrap();

// FIXME:
// discussion: return the hash or the raw timestamp message?
// if using hash, client will request upgrade timestamp by hash (256 bits, 64 hex chars)
//
// if using raw timestamp message, client will request timestamp by whole message (variable size, 208 hex chars if request is 32 bytes),
// but we will have info about the receiving time of the request,
// which can narrow down the search space
let mut hasher = Keccak256::new();
hasher.update(&pending_attestation);
hasher.finalize_into(&mut pending_attestation[0..32]);
timestamp.encode(OpCode::KECCAK256).unwrap();
// copy data out of bump
// TODO: eliminate this allocation by reusing from a pool
// TODO: wrap the buffer with a drop trait to return to pool
let mut buf = BytesMut::with_capacity(128);
timestamp.encode(&mut buf).unwrap();

timestamp.encode(OpCode::ATTESTATION).unwrap();
timestamp.encode(attestation.to_raw().unwrap()).unwrap();
#[cfg(any(debug_assertions, not(feature = "performance")))]
trace!(timestamp = ?timestamp, encoded_length = buf.len());

// TODO: store the pending_attestation into journal
debug_assert_eq!(timestamp.len(), buf_size, "buffer size mismatch");
timestamp.freeze()
(buf.freeze(), commitment)
})
}

pub async fn get_timestamp() {}
28 changes: 28 additions & 0 deletions crates/calendar/src/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! A module that maintains a globally accessible current time in seconds since the Unix epoch.
//!
//! This is for performance optimization to avoid frequent syscalls for time retrieval.
use std::{
sync::atomic::{AtomicU64, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};

static CURRENT_TIME_SEC: AtomicU64 = AtomicU64::new(0);

/// Returns the current time in seconds since the Unix epoch.
#[inline]
pub fn current_time_sec() -> u64 {
CURRENT_TIME_SEC.load(Ordering::Relaxed)
}

/// An asynchronous task that updates the current time every second.
pub async fn updater() {
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
CURRENT_TIME_SEC.store(now, Ordering::Relaxed);
interval.tick().await;
}
}
Loading