From cabfef4afde52062ccc0a09eae8c0d3b72b8ef40 Mon Sep 17 00:00:00 2001 From: lightsing Date: Sat, 13 Dec 2025 10:33:28 +0800 Subject: [PATCH 01/11] add builder --- crates/core/src/codec/v1/timestamp.rs | 1 + crates/core/src/codec/v1/timestamp/builder.rs | 1 + 2 files changed, 2 insertions(+) create mode 100644 crates/core/src/codec/v1/timestamp/builder.rs diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index 6b48b63..c7971fc 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -9,6 +9,7 @@ type StepPtr = Option; mod decode; mod encode; mod fmt; +mod builder; const RECURSION_LIMIT: usize = 256; const MAX_OP_LENGTH: usize = 4096; diff --git a/crates/core/src/codec/v1/timestamp/builder.rs b/crates/core/src/codec/v1/timestamp/builder.rs new file mode 100644 index 0000000..d5ee9ab --- /dev/null +++ b/crates/core/src/codec/v1/timestamp/builder.rs @@ -0,0 +1 @@ +//! Timestamp Builder From 13ed8eaef35e78614d90a5298d475112e497f09d Mon Sep 17 00:00:00 2001 From: lightsing Date: Sat, 13 Dec 2025 10:33:28 +0800 Subject: [PATCH 02/11] add builder --- crates/core/src/codec/v1/timestamp.rs | 1 + crates/core/src/codec/v1/timestamp/builder.rs | 1 + 2 files changed, 2 insertions(+) create mode 100644 crates/core/src/codec/v1/timestamp/builder.rs diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index d15dfcf..28a2633 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -7,6 +7,7 @@ use std::num::NonZeroU32; type StepPtr = Option; +mod builder; mod decode; mod encode; pub(crate) mod fmt; diff --git a/crates/core/src/codec/v1/timestamp/builder.rs b/crates/core/src/codec/v1/timestamp/builder.rs new file mode 100644 index 0000000..d5ee9ab --- /dev/null +++ b/crates/core/src/codec/v1/timestamp/builder.rs @@ -0,0 +1 @@ +//! Timestamp Builder From e45d7481dc5bf8da4047370700bd1b8953b5a130 Mon Sep 17 00:00:00 2001 From: lightsing Date: Mon, 22 Dec 2025 16:12:52 +0800 Subject: [PATCH 03/11] rewrite --- Cargo.lock | 1 + crates/calendar/Cargo.toml | 2 +- crates/calendar/src/routes/ots.rs | 18 +- crates/core/Cargo.toml | 11 +- crates/core/src/bin/uts_info.rs | 6 +- crates/core/src/codec.rs | 66 +++---- crates/core/src/codec/imp.rs | 44 +++++ crates/core/src/codec/imp/bytes.rs | 14 ++ crates/core/src/codec/{ => imp}/primitives.rs | 25 ++- crates/core/src/codec/imp/smallvec.rs | 14 ++ crates/core/src/codec/imp/std_io.rs | 32 ++++ crates/core/src/codec/proof.rs | 22 +-- crates/core/src/codec/v1/attestation.rs | 45 +++-- .../core/src/codec/v1/detached_timestamp.rs | 46 +++-- crates/core/src/codec/v1/digest.rs | 32 ++-- crates/core/src/codec/v1/opcode.rs | 34 ++-- crates/core/src/codec/v1/timestamp.rs | 162 +++++------------ crates/core/src/codec/v1/timestamp/decode.rs | 167 +++++------------- crates/core/src/codec/v1/timestamp/encode.rs | 77 +++----- crates/core/src/codec/v1/timestamp/fmt.rs | 141 +++++++-------- crates/core/src/error.rs | 17 +- crates/core/src/fixtures.rs | 4 +- crates/core/src/lib.rs | 4 + crates/core/src/utils.rs | 2 +- 24 files changed, 461 insertions(+), 525 deletions(-) create mode 100644 crates/core/src/codec/imp.rs create mode 100644 crates/core/src/codec/imp/bytes.rs rename crates/core/src/codec/{ => imp}/primitives.rs (72%) create mode 100644 crates/core/src/codec/imp/smallvec.rs create mode 100644 crates/core/src/codec/imp/std_io.rs diff --git a/Cargo.lock b/Cargo.lock index fffc147..d17520b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4545,6 +4545,7 @@ name = "uts-core" version = "0.1.0" dependencies = [ "auto_impl", + "bytes", "criterion 0.5.1", "digest 0.11.0-rc.4", "hex", diff --git a/crates/calendar/Cargo.toml b/crates/calendar/Cargo.toml index ea29bad..e8ca796 100644 --- a/crates/calendar/Cargo.toml +++ b/crates/calendar/Cargo.toml @@ -22,7 +22,7 @@ tokio = { workspace = true, features = ["full"] } tower-http = { workspace = true, features = ["limit"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } -uts-core = { workspace = true } +uts-core = { workspace = true, features = ["bytes"] } [lints] workspace = true diff --git a/crates/calendar/src/routes/ots.rs b/crates/calendar/src/routes/ots.rs index 5513e7a..97dca0a 100644 --- a/crates/calendar/src/routes/ots.rs +++ b/crates/calendar/src/routes/ots.rs @@ -2,13 +2,13 @@ use alloy_primitives::{Keccak256, b256}; use alloy_signer::SignerSync; use alloy_signer_local::LocalSigner; use axum::body::Bytes; -use bytes::{BufMut, BytesMut}; +use bytes::BytesMut; use smallvec::SmallVec; use std::time::SystemTime; use tracing::Level; use uts_core::{ codec::{ - Encode, Encoder, + Encoder, v1::{Attestation, opcode::OpCode}, }, utils::Hexed, @@ -55,7 +55,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { + (1 + uri.len()); // length of uri in leb128 + uri bytes let attestation = Attestation::Pending { uri }; - let mut timestamp = BytesMut::with_capacity(buf_size).writer(); // TODO: replace this with a builder + let mut timestamp = BytesMut::with_capacity(buf_size); let mut pending_attestation = SmallVec::<[u8; MAX_MESSAGE_SIZE]>::new(); @@ -66,7 +66,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { .as_secs(); trace!(recv_timestamp); let recv_timestamp = recv_timestamp.to_le_bytes(); - OpCode::PREPEND.encode(&mut timestamp).unwrap(); + timestamp.encode(OpCode::PREPEND).unwrap(); timestamp.encode_bytes(&recv_timestamp).unwrap(); pending_attestation.extend(recv_timestamp); @@ -80,7 +80,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { let undeniable_sig = signer.sign_message_sync(&digest).unwrap(); let undeniable_sig = undeniable_sig.as_erc2098(); trace!(undeniable_sig = ?Hexed(&undeniable_sig)); - OpCode::APPEND.encode(&mut timestamp).unwrap(); + timestamp.encode(OpCode::APPEND).unwrap(); timestamp.encode_bytes(&undeniable_sig).unwrap(); pending_attestation.extend(undeniable_sig); @@ -96,14 +96,12 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { let mut hasher = Keccak256::new(); hasher.update(&pending_attestation); hasher.finalize_into(&mut pending_attestation[0..32]); - OpCode::KECCAK256.encode(&mut timestamp).unwrap(); + timestamp.encode(OpCode::KECCAK256).unwrap(); - OpCode::ATTESTATION.encode(&mut timestamp).unwrap(); - attestation.encode(&mut timestamp).unwrap(); + timestamp.encode(OpCode::ATTESTATION).unwrap(); + timestamp.encode(&attestation).unwrap(); // TODO: store the pending_attestation into journal - - let timestamp = timestamp.into_inner(); debug_assert_eq!(timestamp.len(), buf_size, "buffer size mismatch"); timestamp.freeze() } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c6fba23..8b7c422 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -12,6 +12,7 @@ version.workspace = true [[bin]] name = "uts-info" path = "src/bin/uts_info.rs" +required-features = ["std"] [dependencies] auto_impl.workspace = true @@ -22,16 +23,16 @@ ripemd.workspace = true sha1.workspace = true sha2.workspace = true sha3.workspace = true -smallvec = { workspace = true, features = ["write"] } +smallvec = { workspace = true, features = ["specialization", "may_dangle"] } thiserror.workspace = true tracing = { workspace = true, optional = true } +bytes = { workspace = true, optional = true } [features] -nightly = [ - "smallvec/specialization", - "smallvec/may_dangle", -] +default = ["std"] tracing = ["dep:tracing"] +std = [] +bytes = ["dep:bytes"] [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/crates/core/src/bin/uts_info.rs b/crates/core/src/bin/uts_info.rs index 837c1a9..b0f26e9 100644 --- a/crates/core/src/bin/uts_info.rs +++ b/crates/core/src/bin/uts_info.rs @@ -13,7 +13,7 @@ use std::{ process, }; use uts_core::codec::{ - Decode, VersionedProof, + Decode, Reader, VersionedProof, v1::{DetachedTimestamp, Timestamp}, }; @@ -32,7 +32,7 @@ fn main() { } }; - match VersionedProof::::decode(&mut fh) { + match VersionedProof::::decode(&mut Reader(&mut fh)) { Ok(ots) => { println!("OTS Detached Timestamp found:"); println!("{ots}"); @@ -47,7 +47,7 @@ fn main() { fh.seek(io::SeekFrom::Start(0)).unwrap(); - match Timestamp::decode(fh) { + match Timestamp::decode(&mut Reader(&mut fh)) { Ok(ots) => { println!("Raw Timestamp found:"); println!("{ots}"); diff --git a/crates/core/src/codec.rs b/crates/core/src/codec.rs index e51cc05..ebc2ff5 100644 --- a/crates/core/src/codec.rs +++ b/crates/core/src/codec.rs @@ -1,14 +1,14 @@ use crate::error::{DecodeError, EncodeError}; +use alloc::alloc::Global; use auto_impl::auto_impl; -use std::{ - io::{BufRead, Write}, - ops::RangeBounds, -}; +use core::{alloc::Allocator, ops::RangeBounds}; mod proof; pub use proof::{Proof, Version, VersionedProof}; -mod primitives; +mod imp; +#[cfg(feature = "std")] +pub use imp::{Reader, Writer}; /// Types and helpers for the version 1 serialization format. pub mod v1; @@ -17,15 +17,13 @@ pub mod v1; pub const MAGIC: &[u8; 31] = b"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94"; /// Helper trait for writing OpenTimestamps primitives to a byte stream. -pub trait Encoder: Write { +pub trait Encoder: Sized { /// Encodes a single byte to the writer. - fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { - self.write_all(&[byte])?; - Ok(()) - } + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError>; /// Encodes a byte slice prefixed with its length. - fn encode_bytes(&mut self, bytes: &[u8]) -> Result<(), EncodeError> { + fn encode_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<(), EncodeError> { + let bytes = bytes.as_ref(); self.encode(bytes.len())?; self.write_all(bytes)?; Ok(()) @@ -33,27 +31,22 @@ pub trait Encoder: Write { /// Writes the OpenTimestamps magic sequence to the stream. fn encode_magic(&mut self) -> Result<(), EncodeError> { - self.write_all(MAGIC)?; - Ok(()) + self.write_all(&MAGIC[..]) } /// Encodes a value implementing the [`Encode`] trait. - #[inline] fn encode(&mut self, value: impl Encode) -> Result<(), EncodeError> { value.encode(self) } -} -impl Encoder for W {} + // --- no_std feature compatibility --- + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError>; +} /// Helper trait for reading OpenTimestamps primitives from a byte stream. -pub trait Decoder: BufRead { +pub trait Decoder: Sized { /// Decodes a single byte from the reader. - fn decode_byte(&mut self) -> Result { - let mut byte = [0]; - self.read_exact(&mut byte)?; - Ok(byte[0]) - } + fn decode_byte(&mut self) -> Result; /// Decodes a value and ensures it falls within the supplied range. fn decode_ranged( @@ -80,26 +73,37 @@ pub trait Decoder: BufRead { } /// Decodes a value implementing the [`Decode`] trait. - #[inline] fn decode(&mut self) -> Result { T::decode(self) } -} -impl Decoder for R {} - -/// Marker trait for types supporting both [`Encode`] and [`Decode`]. -pub trait Codec: Encode + Decode {} + /// Decodes a value implementing the [`Decode`] trait. + fn decode_in, A: Allocator>(&mut self, alloc: A) -> Result { + T::decode_in(self, alloc) + } -impl Codec for T {} + // --- no_std feature compatibility --- + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), DecodeError>; +} /// Serializes a value into an OpenTimestamps-compatible byte stream. #[auto_impl(&, &mut, Box, Rc, Arc)] pub trait Encode { - fn encode(&self, writer: impl Encoder) -> Result<(), EncodeError>; + fn encode(&self, writer: &mut impl Encoder) -> Result<(), EncodeError>; } /// Deserializes a value from an OpenTimestamps-compatible byte stream. pub trait Decode: Sized { - fn decode(reader: impl Decoder) -> Result; + fn decode(decoder: &mut impl Decoder) -> Result; +} + +/// Deserializes a value from an OpenTimestamps-compatible byte stream. +pub trait DecodeIn: Sized { + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result; +} + +impl> Decode for T { + fn decode(decoder: &mut impl Decoder) -> Result { + T::decode_in(decoder, Global) + } } diff --git a/crates/core/src/codec/imp.rs b/crates/core/src/codec/imp.rs new file mode 100644 index 0000000..3cc91b1 --- /dev/null +++ b/crates/core/src/codec/imp.rs @@ -0,0 +1,44 @@ +use crate::codec::*; +use alloc::vec::Vec; + +#[cfg(feature = "bytes")] +mod bytes; +mod primitives; +mod smallvec; +#[cfg(feature = "std")] +mod std_io; + +#[cfg(feature = "std")] +pub use std_io::{Reader, Writer}; + +impl Encoder for Vec { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.push(byte); + Ok(()) + } + + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.extend_from_slice(buf.as_ref()); + Ok(()) + } +} + +impl Decoder for &[u8] { + fn decode_byte(&mut self) -> Result { + let Some((a, b)) = self.split_at_checked(1) else { + return Err(DecodeError::UnexpectedEof); + }; + *self = b; + Ok(a[0]) + } + + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), DecodeError> { + let len = buf.len(); + let Some((a, b)) = self.split_at_checked(len) else { + return Err(DecodeError::UnexpectedEof); + }; + buf.copy_from_slice(a); + *self = b; + Ok(()) + } +} diff --git a/crates/core/src/codec/imp/bytes.rs b/crates/core/src/codec/imp/bytes.rs new file mode 100644 index 0000000..f4d0158 --- /dev/null +++ b/crates/core/src/codec/imp/bytes.rs @@ -0,0 +1,14 @@ +use crate::codec::*; +use bytes::{BufMut, BytesMut}; + +impl Encoder for BytesMut { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.put_u8(byte); + Ok(()) + } + + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.put_slice(buf.as_ref()); + Ok(()) + } +} diff --git a/crates/core/src/codec/primitives.rs b/crates/core/src/codec/imp/primitives.rs similarity index 72% rename from crates/core/src/codec/primitives.rs rename to crates/core/src/codec/imp/primitives.rs index 8655876..8394579 100644 --- a/crates/core/src/codec/primitives.rs +++ b/crates/core/src/codec/imp/primitives.rs @@ -1,14 +1,11 @@ -use crate::{ - codec::{Decode, Encode}, - error::EncodeError, -}; +use crate::codec::*; macro_rules! leb128 { ($ty:ty) => { paste::paste! { - impl super::Encode for $ty { + impl crate::codec::Encode for $ty { #[inline] - fn encode(&self, mut encoder: impl crate::codec::Encoder) -> Result<(), $crate::error::EncodeError> { + fn encode(&self, encoder: &mut impl crate::codec::Encoder) -> Result<(), $crate::error::EncodeError> { let mut n = *self; let mut buf = [0u8; <$ty>::BITS.div_ceil(7) as usize]; let mut i = 0; @@ -34,9 +31,9 @@ macro_rules! leb128 { } } - impl super::Decode for $ty { + impl crate::codec::Decode for $ty { #[inline] - fn decode(mut decoder: impl crate::codec::Decoder) -> Result { + fn decode(decoder: &mut impl crate::codec::Decoder) -> Result { let mut ret: $ty = 0; let mut shift: u32 = 0; @@ -69,7 +66,7 @@ leb128!(u16, u32, u64, u128); impl Encode for u8 { #[inline] - fn encode(&self, mut encoder: impl crate::codec::Encoder) -> Result<(), EncodeError> { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { encoder.write_all(&[*self])?; Ok(()) } @@ -77,7 +74,7 @@ impl Encode for u8 { impl Decode for u8 { #[inline] - fn decode(mut decoder: impl crate::codec::Decoder) -> Result { + fn decode(decoder: &mut impl Decoder) -> Result { let mut byte = [0u8; 1]; decoder.read_exact(&mut byte)?; Ok(byte[0]) @@ -86,17 +83,17 @@ impl Decode for u8 { impl Encode for usize { #[inline] - fn encode(&self, mut encoder: impl crate::codec::Encoder) -> Result<(), EncodeError> { + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { let val: u32 = (*self).try_into().map_err(|_| EncodeError::UsizeOverflow)?; - val.encode(&mut encoder)?; + val.encode(encoder)?; Ok(()) } } impl Decode for usize { #[inline] - fn decode(mut decoder: impl crate::codec::Decoder) -> Result { - let val = u32::decode(&mut decoder)?; + fn decode(decoder: &mut impl Decoder) -> Result { + let val = u32::decode(decoder)?; Ok(val as usize) } } diff --git a/crates/core/src/codec/imp/smallvec.rs b/crates/core/src/codec/imp/smallvec.rs new file mode 100644 index 0000000..f89bb5e --- /dev/null +++ b/crates/core/src/codec/imp/smallvec.rs @@ -0,0 +1,14 @@ +use crate::codec::*; +use smallvec::SmallVec; + +impl Encoder for SmallVec<[u8; N]> { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.push(byte); + Ok(()) + } + + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.extend_from_slice(buf.as_ref()); + Ok(()) + } +} diff --git a/crates/core/src/codec/imp/std_io.rs b/crates/core/src/codec/imp/std_io.rs new file mode 100644 index 0000000..18a2c49 --- /dev/null +++ b/crates/core/src/codec/imp/std_io.rs @@ -0,0 +1,32 @@ +use crate::codec::*; +use std::io::{Read, Write}; + +pub struct Writer(pub W); +pub struct Reader(pub R); + +impl Encoder for Writer { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.write_all(&[byte])?; + Ok(()) + } + + #[inline] + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.0.write_all(buf.as_ref())?; + Ok(()) + } +} + +impl Decoder for Reader { + fn decode_byte(&mut self) -> Result { + let mut byte = [0]; + self.read_exact(&mut byte)?; + Ok(byte[0]) + } + + #[inline] + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), DecodeError> { + self.0.read_exact(buf)?; + Ok(()) + } +} diff --git a/crates/core/src/codec/proof.rs b/crates/core/src/codec/proof.rs index 179f2a6..525da86 100644 --- a/crates/core/src/codec/proof.rs +++ b/crates/core/src/codec/proof.rs @@ -1,14 +1,14 @@ use crate::{ - codec::{Codec, Decode, Decoder, Encode, Encoder}, + codec::{Decode, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; -use std::fmt; +use core::fmt; /// Version number of the serialization format. pub type Version = u32; /// Trait implemented by proof payloads for a specific serialization version. -pub trait Proof: Codec { +pub trait Proof: Encode + Decode { /// Version identifier that must match the encoded proof. const VERSION: Version; } @@ -19,21 +19,21 @@ pub trait Proof: Codec { pub struct VersionedProof(pub T); impl Decode for VersionedProof { - fn decode(mut reader: impl Decoder) -> Result { - reader.assert_magic()?; - let version: Version = reader.decode()?; + fn decode(decoder: &mut impl Decoder) -> Result { + decoder.assert_magic()?; + let version: Version = decoder.decode()?; if version != T::VERSION { return Err(DecodeError::BadVersion); } - Ok(VersionedProof(T::decode(&mut reader)?)) + Ok(VersionedProof(T::decode(decoder)?)) } } impl Encode for VersionedProof { - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - writer.encode_magic()?; - writer.encode(T::VERSION)?; - self.0.encode(writer) + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.encode_magic()?; + encoder.encode(T::VERSION)?; + self.0.encode(encoder) } } diff --git a/crates/core/src/codec/v1/attestation.rs b/crates/core/src/codec/v1/attestation.rs index 85d313c..132a785 100644 --- a/crates/core/src/codec/v1/attestation.rs +++ b/crates/core/src/codec/v1/attestation.rs @@ -8,15 +8,13 @@ //! comes from some server or from a blockchain. use crate::{ - codec::{Decoder, Encoder}, + codec::{Decode, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, utils::Hexed, }; +use alloc::{string::String, vec::Vec}; +use core::fmt; use smallvec::SmallVec; -use std::{ - fmt, - io::{BufRead, Write}, -}; /// Size in bytes of the tag identifying the attestation type. const TAG_SIZE: usize = 8; @@ -48,23 +46,22 @@ pub enum Attestation { Unknown { tag: AttestationTag, data: Vec }, } -impl Attestation { - /// Decodes an attestation payload from the reader. - pub fn decode(mut reader: R) -> Result { +impl Decode for Attestation { + fn decode(decoder: &mut impl Decoder) -> Result { let mut tag = [0u8; TAG_SIZE]; - reader.read_exact(&mut tag)?; - let len = reader.decode()?; + decoder.read_exact(&mut tag)?; + let len = decoder.decode()?; if tag == *BITCOIN_TAG { - let height = reader.decode()?; + let height = decoder.decode()?; Ok(Attestation::Bitcoin { height }) } else if tag == *PENDING_TAG { // This validation logic copied from python-opentimestamps. Peter comments // that he is deliberately avoiding ?, &, @, etc., to "keep us out of trouble" - let length = reader.decode_ranged(0..=MAX_URI_LEN)?; + let length = decoder.decode_ranged(0..=MAX_URI_LEN)?; let mut uri_bytes = Vec::with_capacity(len); uri_bytes.resize(length, 0); - reader.read_exact(&mut uri_bytes)?; + decoder.read_exact(&mut uri_bytes)?; let uri_string = String::from_utf8(uri_bytes).map_err(|_| DecodeError::InvalidUriChar)?; if !uri_string.chars().all( @@ -77,30 +74,32 @@ impl Attestation { } else { let mut data = Vec::with_capacity(len); data.resize(len, 0); - reader.read_exact(&mut data)?; + decoder.read_exact(&mut data)?; Ok(Attestation::Unknown { tag, data }) } } +} - /// Encodes the attestation to the writer. - pub fn encode(&self, mut writer: W) -> Result<(), EncodeError> { +impl Encode for Attestation { + #[inline] + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { match *self { Attestation::Bitcoin { height } => { - writer.write_all(BITCOIN_TAG)?; + encoder.write_all(BITCOIN_TAG)?; let mut buffer = SmallVec::<[u8; u32::BITS.div_ceil(7) as usize]>::new(); - buffer.encode(height)?; - writer.encode_bytes(&buffer) + height.encode(&mut buffer)?; + encoder.encode_bytes(&buffer) } Attestation::Pending { ref uri } => { - writer.write_all(PENDING_TAG)?; + encoder.write_all(PENDING_TAG)?; let mut buffer = Vec::new(); buffer.encode_bytes(uri.as_bytes())?; - writer.encode_bytes(&buffer) + encoder.encode_bytes(&buffer) } Attestation::Unknown { ref tag, ref data } => { - writer.write_all(tag)?; - writer.encode_bytes(data) + encoder.write_all(tag)?; + encoder.encode_bytes(data) } } } diff --git a/crates/core/src/codec/v1/detached_timestamp.rs b/crates/core/src/codec/v1/detached_timestamp.rs index 627a1f7..7a2f2d3 100644 --- a/crates/core/src/codec/v1/detached_timestamp.rs +++ b/crates/core/src/codec/v1/detached_timestamp.rs @@ -1,9 +1,9 @@ use crate::codec::{ - Decode, Encode, Proof, Version, - v1::{DigestHeader, Timestamp, timestamp}, + Decode, Encode, Encoder, Proof, Version, + v1::{DigestHeader, Timestamp}, }; +use core::{fmt, fmt::Formatter}; use smallvec::ToSmallVec; -use std::{fmt, fmt::Formatter}; /// A file containing a timestamp for another file /// Contains a timestamp, along with a header and the digest of the file. @@ -22,20 +22,17 @@ impl Proof for DetachedTimestamp { } impl Decode for DetachedTimestamp { - fn decode(mut reader: impl crate::codec::Decoder) -> Result { - let header = DigestHeader::decode(&mut reader)?; - let timestamp = Timestamp::decode(&mut reader)?; + fn decode(decoder: &mut impl crate::codec::Decoder) -> Result { + let header = DigestHeader::decode(decoder)?; + let timestamp = Timestamp::decode(decoder)?; Ok(DetachedTimestamp { header, timestamp }) } } impl Encode for DetachedTimestamp { - fn encode( - &self, - mut writer: impl crate::codec::Encoder, - ) -> Result<(), crate::error::EncodeError> { - self.header.encode(&mut writer)?; - self.timestamp.encode(&mut writer)?; + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), crate::error::EncodeError> { + self.header.encode(encoder)?; + self.timestamp.encode(encoder)?; Ok(()) } } @@ -44,11 +41,8 @@ impl fmt::Display for DetachedTimestamp { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { writeln!(f, "digest of {}", self.header)?; - timestamp::fmt::fmt( - &self.timestamp, - Some(&self.header.digest().to_smallvec()), - f, - ) + self.timestamp + .fmt(Some(&self.header.digest().to_smallvec()), f) } } @@ -56,7 +50,7 @@ impl fmt::Display for DetachedTimestamp { mod tests { use super::*; use crate::{ - codec::{Decode, Encode, proof::VersionedProof}, + codec::{Decode, Encoder, proof::VersionedProof}, fixtures, }; @@ -65,14 +59,18 @@ mod tests { let mut encoded_small = vec![]; let mut encoded_large = vec![]; - let ots = VersionedProof::::decode(fixtures::SMALL_DETACHED_TIMESTAMP); - assert!(ots.is_ok()); - assert!(ots.unwrap().encode(&mut encoded_small).is_ok()); + let ots = + VersionedProof::::decode(&mut &*fixtures::SMALL_DETACHED_TIMESTAMP) + .unwrap(); + println!("{:#?}", ots); + println!("{}", ots); + assert!(encoded_small.encode(&ots).is_ok()); assert_eq!(encoded_small, fixtures::SMALL_DETACHED_TIMESTAMP); - let ots = VersionedProof::::decode(fixtures::LARGE_DETACHED_TIMESTAMP); - assert!(ots.is_ok()); - assert!(ots.unwrap().encode(&mut encoded_large).is_ok()); + let ots = + VersionedProof::::decode(&mut &*fixtures::LARGE_DETACHED_TIMESTAMP) + .unwrap(); + assert!(encoded_large.encode(&ots).is_ok()); assert_eq!(encoded_large, fixtures::LARGE_DETACHED_TIMESTAMP); } } diff --git a/crates/core/src/codec/v1/digest.rs b/crates/core/src/codec/v1/digest.rs index 8ba71ff..b528f87 100644 --- a/crates/core/src/codec/v1/digest.rs +++ b/crates/core/src/codec/v1/digest.rs @@ -3,23 +3,27 @@ use crate::{ error::{DecodeError, EncodeError}, utils::Hexed, }; -use std::fmt; +use core::fmt; /// Header describing the digest that anchors a timestamp. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DigestHeader { kind: DigestOp, digest: [u8; 32], } +impl fmt::Debug for DigestHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DigestHeader") + .field("kind", &self.kind) + .field("digest", &Hexed(self.digest())) + .finish() + } +} + impl fmt::Display for DigestHeader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {}", - self.kind, - Hexed(&self.digest[..self.kind.output_size()]) - ) + write!(f, "{} {}", self.kind, Hexed(self.digest())) } } @@ -38,9 +42,9 @@ impl DigestHeader { impl Encode for DigestHeader { #[cfg_attr(feature = "tracing", tracing::instrument(skip(writer), err))] #[inline] - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - writer.encode(&self.kind)?; - writer.write_all(&self.digest[..self.kind.output_size()])?; + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.encode(&self.kind)?; + encoder.write_all(&self.digest[..self.kind.output_size()])?; Ok(()) } } @@ -48,10 +52,10 @@ impl Encode for DigestHeader { impl Decode for DigestHeader { #[cfg_attr(feature = "tracing", tracing::instrument(skip(reader), ret, err))] #[inline] - fn decode(mut reader: impl Decoder) -> Result { - let kind = reader.decode()?; + fn decode(decoder: &mut impl Decoder) -> Result { + let kind = decoder.decode()?; let mut digest = [0u8; 32]; - reader.read_exact(&mut digest)?; + decoder.read_exact(&mut digest)?; Ok(DigestHeader { kind, digest }) } diff --git a/crates/core/src/codec/v1/opcode.rs b/crates/core/src/codec/v1/opcode.rs index ed8d2d5..81dcc1c 100644 --- a/crates/core/src/codec/v1/opcode.rs +++ b/crates/core/src/codec/v1/opcode.rs @@ -6,23 +6,29 @@ use crate::{ codec::{Decode, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; +use core::{fmt, hint::unreachable_unchecked}; use digest::{Digest, OutputSizeUser, typenum::Unsigned}; use ripemd::Ripemd160; use sha1::Sha1; use sha2::Sha256; use sha3::Keccak256; use smallvec::ToSmallVec; -use std::{fmt, hint::unreachable_unchecked}; pub(crate) type OperationBuffer = smallvec::SmallVec<[u8; 64]>; /// An OpenTimestamps opcode. /// /// This is always a valid opcode. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct OpCode(u8); +impl fmt::Debug for OpCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + impl fmt::Display for OpCode { /// Formats the opcode as a string. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -32,15 +38,15 @@ impl fmt::Display for OpCode { impl Encode for OpCode { #[inline] - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - writer.encode_byte(self.tag()) + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.encode_byte(self.tag()) } } impl Decode for OpCode { #[inline] - fn decode(mut reader: impl Decoder) -> Result { - let byte = reader.decode_byte()?; + fn decode(decoder: &mut impl Decoder) -> Result { + let byte = decoder.decode_byte()?; OpCode::new(byte).ok_or(DecodeError::BadOpCode(byte)) } } @@ -153,10 +159,16 @@ impl PartialEq for OpCode { /// An OpenTimestamps digest opcode. /// /// This is always a valid opcode. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct DigestOp(OpCode); +impl fmt::Debug for DigestOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0.name()) + } +} + impl fmt::Display for DigestOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) @@ -177,15 +189,15 @@ impl PartialEq for DigestOp { impl Encode for DigestOp { #[inline] - fn encode(&self, writer: impl Encoder) -> Result<(), EncodeError> { - self.0.encode(writer) + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + self.0.encode(encoder) } } impl Decode for DigestOp { #[inline] - fn decode(mut reader: impl Decoder) -> Result { - let opcode = OpCode::decode(&mut reader)?; + fn decode(decoder: &mut impl Decoder) -> Result { + let opcode = OpCode::decode(decoder)?; opcode .as_digest() .ok_or(DecodeError::ExpectedDigestOp(opcode)) diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index 28a2633..d28b853 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -1,19 +1,16 @@ //! ** The implementation here is subject to change as this is a read-only version. ** -use crate::codec::{ - Proof, Version, - v1::{Attestation, opcode::OpCode}, -}; -use std::num::NonZeroU32; -type StepPtr = Option; +use crate::{ + codec::v1::{Attestation, opcode::OpCode}, + utils::Hexed, +}; +use alloc::{alloc::Global, vec::Vec}; +use core::{alloc::Allocator, fmt::Debug}; mod builder; mod decode; mod encode; -pub(crate) mod fmt; - -const RECURSION_LIMIT: usize = 256; -const MAX_OP_LENGTH: usize = 4096; +mod fmt; /// Proof that that one or more attestations commit to a message. /// @@ -32,125 +29,54 @@ const MAX_OP_LENGTH: usize = 4096; /// execute APPEND 0ef41e45bb5534b3 /// result attested by Pending: update URI https://alice.btc.calendar.opentimestamps.org /// ``` -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Timestamp { - steps: Vec, - data: Vec, - attestations: Vec, +#[derive(Clone, Debug)] +pub enum Timestamp { + Step(Step), + Attestation(Attestation), } -/// An OpenTimestamps step. -#[derive(Clone, PartialEq, Eq, Debug)] -#[repr(C)] -struct Step { - opcode: OpCode, - _padding: u8, - data_len: u16, - data_offset: u32, - // LCRS tree structure - first_child: StepPtr, - next_sibling: StepPtr, +/// An execution Step. +#[derive(Clone)] +pub struct Step { + op: OpCode, + data: Vec, + next: Vec, A>, } -// cache line aligned -const _: () = assert!(size_of::() == 16); -impl Default for Step { - fn default() -> Self { - Step { - opcode: OpCode::ATTESTATION, - _padding: 0, - data_len: 0, - data_offset: 0, - first_child: None, - next_sibling: None, +impl PartialEq for Timestamp { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Timestamp::Step(s1), Timestamp::Step(s2)) => s1 == s2, + (Timestamp::Attestation(a1), Timestamp::Attestation(a2)) => a1 == a2, + _ => false, } } } +impl Eq for Timestamp {} -impl Proof for Timestamp { - const VERSION: Version = 1; +impl PartialEq for Step { + fn eq(&self, other: &Self) -> bool { + self.op == other.op && self.data == other.data && self.next == other.next + } } - -impl Timestamp { - /// Returns the data slice associated with a step. - /// - /// # Safety - /// - /// The caller must ensure that the step was constructed from this timestamp's data buffer. - #[inline] - unsafe fn get_step_data(&self, step: &Step) -> &[u8] { - if step.data_len == 0 { - return &[]; +impl Eq for Step {} + +impl Debug for Step { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut f = f.debug_struct("Step"); + f.field("op", &self.op); + if self.op.has_immediate() { + f.field("data", &Hexed(&self.data)); } - let start = step.data_offset as usize; - debug_assert!(start < self.data.len()); - let end = start + step.data_len as usize; - debug_assert!(end <= self.data.len()); - // SAFETY: bounds checked above - unsafe { self.data.get_unchecked(start..end) } + f.field("next", &self.next).finish() } - - /// Returns the attestation index encoded by an attestation step. - /// - /// # Safety - /// - /// The caller must ensure that the step is an attestation step and that the - /// safety requirements of [`Self::get_step_data`] also hold. - #[inline] - unsafe fn get_attest_idx(&self, step: &Step) -> u32 { - debug_assert!(step.opcode == OpCode::ATTESTATION); - debug_assert_eq!(step.data_len as usize, size_of::()); - let data = unsafe { self.get_step_data(step) }; - u32::from_le_bytes(data.try_into().unwrap()) - } - - #[inline] - fn push_to_heap(heap: &mut Vec, data: &[u8]) -> (u32, u16) { - if data.is_empty() { - return (0, 0); +} +impl Timestamp { + /// Returns the opcode of this timestamp node. + pub fn op(&self) -> OpCode { + match self { + Timestamp::Step(step) => step.op, + Timestamp::Attestation(_) => OpCode::ATTESTATION, } - - let offset = heap.len(); - let len = data.len(); - - assert!(offset <= u32::MAX as usize, "Data heap overflow (max 4GB)"); - assert!(len <= u16::MAX as usize, "Ref data too large (max 65KB)"); - - heap.extend_from_slice(data); - - (offset as u32, len as u16) - } - - /// Returns a mutable buffer from the heap. - /// - /// # Safety - /// - /// The caller must write exactly `len` bytes into the returned buffer. - #[inline] - unsafe fn get_buffer_from_heap(heap: &mut Vec, len: usize) -> (u32, u16, &mut [u8]) { - let offset = heap.len(); - assert!(offset <= u32::MAX as usize, "Data heap overflow (max 4GB)"); - assert!(len <= u16::MAX as usize, "Ref data too large (max 65KB)"); - - heap.reserve(len); - - // SAFETY: we just reserved enough space - let buffer = unsafe { - heap.set_len(offset + len); - heap.get_unchecked_mut(offset..offset + len) - }; - - (offset as u32, len as u16, buffer) } } - -#[inline] -fn make_ptr(idx: usize) -> StepPtr { - assert!(idx < u32::MAX as usize); - NonZeroU32::new((idx + 1) as u32) -} - -#[inline] -fn resolve_ptr(ptr: StepPtr) -> Option { - ptr.map(|nz| (nz.get() - 1) as usize) -} diff --git a/crates/core/src/codec/v1/timestamp/decode.rs b/crates/core/src/codec/v1/timestamp/decode.rs index 277814f..533abc3 100644 --- a/crates/core/src/codec/v1/timestamp/decode.rs +++ b/crates/core/src/codec/v1/timestamp/decode.rs @@ -1,152 +1,75 @@ use super::*; use crate::{ - codec::{Decode, Decoder}, + codec::{Decode, DecodeIn, Decoder}, error::DecodeError, }; -use std::io::BufRead; -impl Decode for Timestamp { - fn decode(mut reader: impl Decoder) -> Result { - let mut steps = Vec::new(); - let mut data = Vec::new(); - let mut attestations = Vec::new(); +const RECURSION_LIMIT: usize = 256; - Self::decode_step_recurse( - &mut reader, - &mut steps, - &mut data, - &mut attestations, - None, - RECURSION_LIMIT, - )?; - - Ok(Timestamp { - steps, - data, - attestations, - }) +impl DecodeIn for Timestamp { + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { + Self::decode_recursive(decoder, RECURSION_LIMIT, alloc) } } -impl Timestamp { - fn decode_step_recurse( - reader: &mut R, - steps: &mut Vec, - data: &mut Vec, - attestations: &mut Vec, - op: Option, +impl Timestamp { + fn decode_recursive( + decoder: &mut impl Decoder, recursion_limit: usize, - ) -> Result { + alloc: A, + ) -> Result, DecodeError> { if recursion_limit == 0 { return Err(DecodeError::RecursionLimit); } + let op = OpCode::decode(&mut *decoder)?; - let op = match op { - Some(op) => op, - None => reader.decode()?, - }; + Self::decode_from_op(op, decoder, recursion_limit, alloc) + } - let step = match op { + fn decode_from_op( + op: OpCode, + decoder: &mut impl Decoder, + limit: usize, + alloc: A, + ) -> Result, DecodeError> { + match op { OpCode::ATTESTATION => { - let attest = Attestation::decode(reader)?; - let attest_idx = attestations.len(); - attestations.push(attest); - let (data_offset, data_len) = - Self::push_to_heap(data, &(attest_idx as u32).to_le_bytes()); - Step { - opcode: op, - data_len, - data_offset, - ..Default::default() - } + let attestation = Attestation::decode(decoder)?; + Ok(Timestamp::Attestation(attestation)) } OpCode::FORK => { - let mut first_child: StepPtr = None; - let mut prev_sibling_idx: Option = None; - + let mut children = Vec::new_in(alloc); let mut next_op = OpCode::FORK; - while next_op == OpCode::FORK { - let child_ptr = Self::decode_step_recurse( - reader, - steps, - data, - attestations, - None, - recursion_limit - 1, - )?; - - // LCRS: - // if prev sibling exist, link its next_sibling to current child - // else it's first_child - if let Some(prev) = prev_sibling_idx { - steps[prev].next_sibling = child_ptr; - } else { - first_child = child_ptr; - } - - // update prev_sibling_idx to current child - prev_sibling_idx = resolve_ptr(child_ptr); - - next_op = reader.decode()?; - } - - let child_ptr = Self::decode_step_recurse( - reader, - steps, - data, - attestations, - Some(next_op), - recursion_limit - 1, - )?; - if let Some(prev) = prev_sibling_idx { - steps[prev].next_sibling = child_ptr; - } else { - first_child = child_ptr; - } - - Step { - opcode: op, - data_len: 0, - data_offset: 0, - first_child, - ..Default::default() + let child = Self::decode_recursive(&mut *decoder, limit - 1, alloc)?; + children.push(child); + next_op = OpCode::decode(&mut *decoder)?; } + children.push(Self::decode_from_op(next_op, decoder, limit - 1, alloc)?); + Ok(Timestamp::Step(Step { + op: OpCode::FORK, + data: Vec::new_in(alloc), + next: children, + })) } _ => { - debug_assert!(!op.is_control()); - let (data_offset, data_len) = if op.has_immediate() { - let length = reader.decode_ranged(1..=MAX_OP_LENGTH)?; - // SAFETY: We will fill the buffer right after getting it. - let (data_offset, data_len, buffer) = - unsafe { Self::get_buffer_from_heap(data, length) }; - reader.read_exact(buffer)?; - (data_offset, data_len) + let data = if op.has_immediate() { + const MAX_OP_LENGTH: usize = 4096; + let length = decoder.decode_ranged(1..MAX_OP_LENGTH)?; + let mut data = Vec::with_capacity_in(length, alloc); + data.resize(length, 0); + decoder.read_exact(&mut data)?; + + data } else { - (0, 0) + Vec::new_in(alloc) }; - let next = Self::decode_step_recurse( - reader, - steps, - data, - attestations, - None, - recursion_limit - 1, - )?; + let mut next = Vec::with_capacity_in(1, alloc); + next.push(Self::decode_recursive(decoder, limit - 1, alloc)?); - Step { - opcode: op, - data_len, - data_offset, - first_child: next, - ..Default::default() - } + Ok(Timestamp::Step(Step { op, data, next })) } - }; - - let step_idx = steps.len(); - steps.push(step); - Ok(make_ptr(step_idx)) + } } } diff --git a/crates/core/src/codec/v1/timestamp/encode.rs b/crates/core/src/codec/v1/timestamp/encode.rs index 67b4f4a..185dc87 100644 --- a/crates/core/src/codec/v1/timestamp/encode.rs +++ b/crates/core/src/codec/v1/timestamp/encode.rs @@ -3,67 +3,34 @@ use crate::{ codec::{Encode, Encoder}, error::EncodeError, }; -use std::io::Write; impl Encode for Timestamp { - fn encode(&self, mut writer: impl Encoder) -> Result<(), EncodeError> { - self.encode_step_recurse(&mut writer, &self.steps.last().unwrap()) - } -} - -impl Timestamp { - fn encode_step_recurse( - &self, - writer: &mut W, - step: &Step, - ) -> Result<(), EncodeError> { - // 1. Write OpCode - // Note: We need a way to serialize the OpCode (e.g., as u8) - writer.encode(step.opcode)?; - - // 2. Write data - match step.opcode { - OpCode::ATTESTATION => { - // SAFETY: caller ensures step is attestation step - let attest_idx = unsafe { self.get_attest_idx(step) }; - let attest = &self.attestations[attest_idx as usize]; - attest.encode(&mut *writer)?; + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + match self { + Self::Attestation(attestation) => { + encoder.encode(self.op())?; + attestation.encode(encoder)?; } - _ if step.data_len != 0 => { - // SAFETY: caller ensures step is valid - let step_data = unsafe { self.get_step_data(step) }; - writer.encode_bytes(step_data)?; + Self::Step(step) if step.op == OpCode::FORK => { + debug_assert!(step.next.len() >= 2, "FORK must have at least two children"); + for child in step.next.iter().take(step.next.len() - 1) { + encoder.encode(self.op())?; + child.encode(encoder)?; + } + // Encode the last child + step.next.last().expect("infallible").encode(encoder)?; } - _ => {} - } - - if let Some(first_child) = resolve_ptr(step.first_child) { - let mut current = &self.steps[first_child]; - if let OpCode::FORK = step.opcode { - // Encode the first child - self.encode_step_recurse(writer, current)?; - - // Logic: Child -> FORK -> Child -> ... -> LastChild - while let Some(next_sibling_idx) = resolve_ptr(current.next_sibling) { - let next = &self.steps[next_sibling_idx]; - // Encode current child - self.encode_step_recurse(writer, next)?; - // Write Separator FORK - let continues = resolve_ptr(next.next_sibling).is_some(); - if continues { - writer.encode(OpCode::FORK)?; - } - - // Move to next - current = next; + Self::Step(step) => { + encoder.encode(self.op())?; + if !step.data.is_empty() { + debug_assert!(step.op.has_immediate()); + encoder.encode_bytes(&step.data)?; } - Ok(()) - } else { - // FIXME: tailcall optimization - self.encode_step_recurse(writer, current) + debug_assert_eq!(step.next.len(), 1); + step.next[0].encode(encoder)?; } - } else { - Ok(()) } + + Ok(()) } } diff --git a/crates/core/src/codec/v1/timestamp/fmt.rs b/crates/core/src/codec/v1/timestamp/fmt.rs index 5ded172..dc02034 100644 --- a/crates/core/src/codec/v1/timestamp/fmt.rs +++ b/crates/core/src/codec/v1/timestamp/fmt.rs @@ -1,96 +1,79 @@ use super::*; -use crate::{ - codec::v1::opcode::{OpCode, OperationBuffer}, - utils::Hexed, -}; -use std::fmt; +use crate::{codec::v1::opcode::OperationBuffer, utils::Hexed}; +use core::fmt; -pub(crate) fn fmt( - timestamp: &Timestamp, - input: Option<&OperationBuffer>, - f: &mut fmt::Formatter, -) -> fmt::Result { - fmt_recurse( - ×tamp, - input, - ×tamp.steps.last().unwrap(), - f, - 0, - true, - ) +impl fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt_recurse(None, f, 0, true) + } } -fn fmt_recurse( - timestamp: &Timestamp, - input: Option<&OperationBuffer>, - step: &Step, - f: &mut fmt::Formatter, - depth: usize, - first_line: bool, -) -> fmt::Result { - fn indent(f: &mut fmt::Formatter, depth: usize, first_line: bool) -> fmt::Result { - if depth == 0 { - return Ok(()); - } - - for _ in 0..depth - 1 { - f.write_str(" ")?; - } - if first_line { - f.write_str("--->")?; - } else { - f.write_str(" ")?; - } - Ok(()) +impl Timestamp { + pub(crate) fn fmt( + &self, + input: Option<&OperationBuffer>, + f: &mut fmt::Formatter, + ) -> fmt::Result { + self.fmt_recurse(input, f, 0, true) } - indent(f, depth, first_line)?; - match step.opcode { - OpCode::FORK => { - writeln!(f, "fork")?; - let mut child_ptr = step.first_child; - while let Some(child_idx) = resolve_ptr(child_ptr) { - let child_step = ×tamp.steps[child_idx]; - fmt_recurse(timestamp, input, child_step, f, depth + 1, true)?; - child_ptr = child_step.next_sibling; + fn fmt_recurse( + &self, + input: Option<&OperationBuffer>, + f: &mut fmt::Formatter, + depth: usize, + first_line: bool, + ) -> fmt::Result { + fn indent(f: &mut fmt::Formatter, depth: usize, first_line: bool) -> fmt::Result { + if depth == 0 { + return Ok(()); + } + + for _ in 0..depth - 1 { + f.write_str(" ")?; + } + if first_line { + f.write_str("--->")?; + } else { + f.write_str(" ")?; } Ok(()) } - OpCode::ATTESTATION => { - // SAFETY: caller ensures step is attestation step - let attest_idx = unsafe { timestamp.get_attest_idx(step) } as usize; - let attest = ×tamp.attestations[attest_idx]; - writeln!(f, "result attested by {attest}") - } - op @ _ => { - let data = unsafe { timestamp.get_step_data(step) }; - if op.has_immediate() { - writeln!(f, "execute {op} {}", Hexed(&data))?; - } else { - writeln!(f, "execute {op}")?; + indent(f, depth, first_line)?; + match self { + Self::Step(step) if step.op == OpCode::FORK => { + writeln!(f, "fork")?; + for child in &step.next { + child.fmt_recurse(input, f, depth + 1, true)?; + } + Ok(()) } + Self::Step(step) => { + let op = step.op; + if op.has_immediate() { + writeln!(f, "execute {op} {}", Hexed(&step.data))?; + } else { + writeln!(f, "execute {op}")?; + } - let result = if let Some(input) = input { - let result = op.execute(&input, &data); - indent(f, depth, false)?; - writeln!(f, " result {}", Hexed(&result))?; - Some(result) - } else { - None - }; + let result = if let Some(input) = input { + let result = op.execute(&input, &step.data); + indent(f, depth, false)?; + writeln!(f, " result {}", Hexed(&result))?; + Some(result) + } else { + None + }; - if let Some(step_idx) = resolve_ptr(step.first_child) { - let step = ×tamp.steps[step_idx]; - fmt_recurse(timestamp, result.as_ref(), step, f, depth, false)?; + for child in &step.next { + child.fmt_recurse(result.as_ref(), f, depth, false)?; + } + Ok(()) + } + Self::Attestation(attestation) => { + writeln!(f, "result attested by {attestation}") } - Ok(()) } } } - -impl fmt::Display for Timestamp { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt_recurse(self, None, &self.steps.last().unwrap(), f, 0, true) - } -} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 44c0362..de5b444 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -27,9 +27,13 @@ pub enum DecodeError { /// Recursed deeper than allowed while decoding the proof. #[error("recursion limit reached")] RecursionLimit, + /// Reached end-of-file unexpectedly. + #[error("unexpected end of file")] + UnexpectedEof, /// General I/O error + #[cfg(feature = "std")] #[error("I/O error: {0}")] - Io(#[from] std::io::Error), + Io(std::io::Error), } /// Errors returned while encoding proofs. @@ -39,6 +43,17 @@ pub enum EncodeError { #[error("tried to encode a usize exceeding u32::MAX")] UsizeOverflow, /// General I/O error + #[cfg(feature = "std")] #[error("I/O error: {0}")] Io(#[from] std::io::Error), } + +#[cfg(feature = "std")] +impl From for DecodeError { + fn from(err: std::io::Error) -> Self { + match err.kind() { + std::io::ErrorKind::UnexpectedEof => Self::UnexpectedEof, + _ => Self::Io(err), + } + } +} diff --git a/crates/core/src/fixtures.rs b/crates/core/src/fixtures.rs index 7b9aa3f..2eb9b7c 100644 --- a/crates/core/src/fixtures.rs +++ b/crates/core/src/fixtures.rs @@ -4,7 +4,7 @@ //! Embedded test fixtures for detached timestamp files used in tests and benchmarks. -pub const SMALL_DETACHED_TIMESTAMP: &[u8] = b"\ +pub static SMALL_DETACHED_TIMESTAMP: &[u8] = b"\ \x00\x4f\x70\x65\x6e\x54\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x00\x00\x50\x72\x6f\x6f\x66\x00\xbf\x89\xe2\xe8\x84\xe8\x92\ \x94\x01\x08\xa7\x0d\xfe\x69\xc5\xa0\xd6\x28\x16\x78\x1a\xbb\x6e\x17\x77\x85\x47\x18\x62\x4a\x0d\x19\x42\x31\xad\xb1\x4c\ \x32\xee\x54\x38\xa4\xf0\x10\x7a\x46\x05\xde\x0a\x5b\x37\xcb\x21\x17\x59\xc6\x81\x2b\xfe\x2e\x08\xff\xf0\x10\x24\x4b\x79\ @@ -15,7 +15,7 @@ pub const SMALL_DETACHED_TIMESTAMP: &[u8] = b"\ \x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e\x2e\x2d\x68\x74\x74\x70\x73\x3a\x2f\x2f\x61\x6c\x69\x63\x65\x2e\x62\x74\x63\x2e\x63\x61\ \x6c\x65\x6e\x64\x61\x72\x2e\x6f\x70\x65\x6e\x74\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x2e\x6f\x72\x67"; -pub const LARGE_DETACHED_TIMESTAMP: &[u8] = b"\ +pub static LARGE_DETACHED_TIMESTAMP: &[u8] = b"\ \x00\x4f\x70\x65\x6e\x54\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x00\x00\x50\x72\x6f\x6f\x66\x00\xbf\x89\xe2\xe8\x84\xe8\x92\ \x94\x01\x08\x6f\xd9\xc1\xc4\xf0\x96\xb7\x7e\x6d\x44\x57\xba\xc1\xc7\xf5\x10\x10\xd3\x18\xdb\x48\x3f\x28\x68\xd3\x79\x58\ \x43\xf0\x98\xd3\x78\xf0\x10\xe2\xe2\x24\x43\x9e\x7f\x0f\xdd\x8c\x1e\xea\xc7\x3e\xa7\x39\xdb\x08\xf1\x20\xa5\x74\x44\x4a\ diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1ac7265..ce502c9 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,6 +1,10 @@ #![feature(exact_bitshifts)] +#![feature(allocator_api)] +#![cfg_attr(not(feature = "std"), no_std)] //! # Universal Timestamps Core Library +extern crate alloc; + mod tracing; #[cfg(test)] diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 56c4372..8a1948b 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,4 +1,4 @@ -use std::fmt; +use core::fmt; /// Zero-allocation wrapper that displays byte slices as lowercase hex. pub struct Hexed<'a, T: ?Sized>(pub &'a T); From 5ccbf19ef30b221e2bd99ee77a62a7130a84caef Mon Sep 17 00:00:00 2001 From: lightsing Date: Mon, 22 Dec 2025 16:50:44 +0800 Subject: [PATCH 04/11] rewrite Attestation --- crates/calendar/src/routes/ots.rs | 6 +- crates/core/src/codec/v1.rs | 4 +- crates/core/src/codec/v1/attestation.rs | 220 ++++++++++++------- crates/core/src/codec/v1/timestamp.rs | 4 +- crates/core/src/codec/v1/timestamp/decode.rs | 2 +- crates/core/src/error.rs | 12 + crates/core/src/lib.rs | 1 + 7 files changed, 163 insertions(+), 86 deletions(-) diff --git a/crates/calendar/src/routes/ots.rs b/crates/calendar/src/routes/ots.rs index 97dca0a..0cac408 100644 --- a/crates/calendar/src/routes/ots.rs +++ b/crates/calendar/src/routes/ots.rs @@ -9,7 +9,7 @@ use tracing::Level; use uts_core::{ codec::{ Encoder, - v1::{Attestation, opcode::OpCode}, + v1::{Attestation, PendingAttestation, opcode::OpCode}, }, utils::Hexed, }; @@ -53,7 +53,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { + 8 // Pending tag + 1 // length of packed ATTESTATION data length in leb128 + (1 + uri.len()); // length of uri in leb128 + uri bytes - let attestation = Attestation::Pending { uri }; + let attestation = PendingAttestation { uri: uri.into() }; let mut timestamp = BytesMut::with_capacity(buf_size); @@ -99,7 +99,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { timestamp.encode(OpCode::KECCAK256).unwrap(); timestamp.encode(OpCode::ATTESTATION).unwrap(); - timestamp.encode(&attestation).unwrap(); + timestamp.encode(&attestation.to_raw().unwrap()).unwrap(); // TODO: store the pending_attestation into journal debug_assert_eq!(timestamp.len(), buf_size, "buffer size mismatch"); diff --git a/crates/core/src/codec/v1.rs b/crates/core/src/codec/v1.rs index aa0b6b6..4815fc6 100644 --- a/crates/core/src/codec/v1.rs +++ b/crates/core/src/codec/v1.rs @@ -6,7 +6,9 @@ mod digest; pub mod opcode; mod timestamp; -pub use attestation::{Attestation, AttestationTag}; +pub use attestation::{ + Attestation, AttestationTag, BitcoinAttestation, PendingAttestation, RawAttestation, +}; pub use detached_timestamp::DetachedTimestamp; pub use digest::DigestHeader; pub use timestamp::Timestamp; diff --git a/crates/core/src/codec/v1/attestation.rs b/crates/core/src/codec/v1/attestation.rs index 132a785..b9bb359 100644 --- a/crates/core/src/codec/v1/attestation.rs +++ b/crates/core/src/codec/v1/attestation.rs @@ -1,25 +1,19 @@ -// Copyright (C) The OpenTimestamps developers -// Copyright (C) The ots-rs developers -// SPDX-License-Identifier: MIT OR Apache-2.0 - //! # Attestations //! //! An attestation is a claim that some data existed at some time. It //! comes from some server or from a blockchain. use crate::{ - codec::{Decode, Decoder, Encode, Encoder}, + codec::{Decode, DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, utils::Hexed, }; -use alloc::{string::String, vec::Vec}; +use alloc::{borrow::Cow, vec::Vec}; use core::fmt; -use smallvec::SmallVec; +use std::alloc::{Allocator, Global}; /// Size in bytes of the tag identifying the attestation type. const TAG_SIZE: usize = 8; -/// Maximum length of a URI in a "pending" attestation. -const MAX_URI_LEN: usize = 1000; /// Tag indicating a Bitcoin attestation. const BITCOIN_TAG: &[u8; 8] = b"\x05\x88\x96\x0d\x73\xd7\x19\x01"; @@ -29,93 +23,161 @@ const PENDING_TAG: &[u8; 8] = b"\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e"; /// Tag identifying the attestation kind. pub type AttestationTag = [u8; TAG_SIZE]; -/// Proof that some data existed at a given time. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum Attestation { - /// Attestation derived from a Bitcoin block header. - /// - /// This consists solely of a block height and asserts that the - /// current hash matches the Merkle root of the block at that height. - Bitcoin { height: u32 }, - /// Attestation delivered by an OpenTimestamps calendar server. - /// - /// Only a restricted URI is stored locally so that the server can be - /// queried later for the full proof material. - Pending { uri: String }, - /// Opaque attestation stored verbatim. - Unknown { tag: AttestationTag, data: Vec }, +/// Raw Proof that some data existed at a given time. +#[derive(Clone, Debug)] +pub struct RawAttestation { + pub tag: AttestationTag, + pub data: Vec, } -impl Decode for Attestation { - fn decode(decoder: &mut impl Decoder) -> Result { +impl DecodeIn for RawAttestation { + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { let mut tag = [0u8; TAG_SIZE]; decoder.read_exact(&mut tag)?; + let len = decoder.decode()?; + let mut data = Vec::with_capacity_in(len, alloc); + data.resize(len, 0); + decoder.read_exact(&mut data)?; - if tag == *BITCOIN_TAG { - let height = decoder.decode()?; - Ok(Attestation::Bitcoin { height }) - } else if tag == *PENDING_TAG { - // This validation logic copied from python-opentimestamps. Peter comments - // that he is deliberately avoiding ?, &, @, etc., to "keep us out of trouble" - let length = decoder.decode_ranged(0..=MAX_URI_LEN)?; - let mut uri_bytes = Vec::with_capacity(len); - uri_bytes.resize(length, 0); - decoder.read_exact(&mut uri_bytes)?; - let uri_string = - String::from_utf8(uri_bytes).map_err(|_| DecodeError::InvalidUriChar)?; - if !uri_string.chars().all( - |ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' | '/' | ':'), - ) { - return Err(DecodeError::InvalidUriChar); - } + Ok(RawAttestation { tag, data }) + } +} + +impl Encode for RawAttestation { + #[inline] + fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { + encoder.write_all(&self.tag)?; + encoder.encode_bytes(&self.data) + } +} + +impl PartialEq for RawAttestation { + fn eq(&self, other: &Self) -> bool { + self.tag == other.tag && self.data.as_slice() == other.data.as_slice() + } +} - Ok(Attestation::Pending { uri: uri_string }) - } else { - let mut data = Vec::with_capacity(len); - data.resize(len, 0); - decoder.read_exact(&mut data)?; +impl Eq for RawAttestation {} - Ok(Attestation::Unknown { tag, data }) +pub trait Attestation<'a>: Sized { + const TAG: AttestationTag; + + fn from_raw(raw: &'a RawAttestation) -> Result { + if raw.tag != Self::TAG { + return Err(DecodeError::BadAttestationTag); } + + Self::from_raw_data(&raw.data) + } + + fn to_raw(&self) -> Result { + self.to_raw_in(Global) + } + + fn to_raw_in(&self, alloc: A) -> Result, EncodeError> { + Ok(RawAttestation { + tag: Self::TAG, + data: self.to_raw_data_in(alloc)?, + }) + } + + fn from_raw_data(data: &'a [u8]) -> Result; + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError>; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BitcoinAttestation { + pub height: u32, +} + +impl fmt::Display for BitcoinAttestation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Bitcoin at height {}", self.height) + } +} + +impl Attestation<'_> for BitcoinAttestation { + const TAG: AttestationTag = *BITCOIN_TAG; + + fn from_raw_data(data: &[u8]) -> Result { + let height = u32::decode(&mut &*data)?; + Ok(BitcoinAttestation { height }) } + + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError> { + let mut buffer = Vec::with_capacity_in(u32::BITS.div_ceil(7) as usize, alloc); + buffer.encode(self.height)?; + Ok(buffer) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PendingAttestation<'a> { + pub uri: Cow<'a, str>, } -impl Encode for Attestation { +impl PendingAttestation<'_> { + /// Maximum length of a URI in a "pending" attestation. + pub const MAX_URI_LEN: usize = 1000; + #[inline] - fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { - match *self { - Attestation::Bitcoin { height } => { - encoder.write_all(BITCOIN_TAG)?; - let mut buffer = SmallVec::<[u8; u32::BITS.div_ceil(7) as usize]>::new(); - height.encode(&mut buffer)?; - encoder.encode_bytes(&buffer) - } - Attestation::Pending { ref uri } => { - encoder.write_all(PENDING_TAG)?; - let mut buffer = Vec::new(); - buffer.encode_bytes(uri.as_bytes())?; - encoder.encode_bytes(&buffer) - } - Attestation::Unknown { ref tag, ref data } => { - encoder.write_all(tag)?; - encoder.encode_bytes(data) - } + pub fn validate_uri(uri: &str) -> bool { + uri.chars() + .all(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' | '/' | ':')) + } +} + +impl fmt::Display for PendingAttestation<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Pending at {}", self.uri) + } +} + +impl<'a> Attestation<'a> for PendingAttestation<'a> { + const TAG: AttestationTag = *PENDING_TAG; + + fn from_raw_data(data: &'a [u8]) -> Result { + let data = &mut &data[..]; + let length = u32::decode(data)?; // length prefix + if length as usize > Self::MAX_URI_LEN { + return Err(DecodeError::UriTooLong); + } + let uri = core::str::from_utf8(&data).map_err(|_| DecodeError::InvalidUriChar)?; + if !Self::validate_uri(uri) { + return Err(DecodeError::InvalidUriChar); } + Ok(PendingAttestation { + uri: Cow::Borrowed(uri), + }) + } + + fn to_raw_data_in(&self, alloc: B) -> Result, EncodeError> { + if self.uri.len() > Self::MAX_URI_LEN { + return Err(EncodeError::UriTooLong); + } + if !Self::validate_uri(&self.uri) { + return Err(EncodeError::InvalidUriChar); + } + let mut buffer = + Vec::with_capacity_in(self.uri.len() + u32::BITS.div_ceil(7) as usize, alloc); + buffer.encode_bytes(self.uri.as_bytes())?; + Ok(buffer) } } -impl fmt::Display for Attestation { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Attestation::Bitcoin { height } => write!(f, "Bitcoin block {}", height), - Attestation::Pending { ref uri } => write!(f, "Pending: update URI {}", uri), - Attestation::Unknown { ref tag, ref data } => write!( - f, - "unknown attestation type {}: {}", - Hexed(tag), - Hexed(data) - ), +impl fmt::Display for RawAttestation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.tag { + tag if *tag == *BITCOIN_TAG => { + let att = BitcoinAttestation::from_raw(self).expect("Valid Bitcoin attestation"); + write!(f, "{}", att) + } + tag if *tag == *PENDING_TAG => { + let att = PendingAttestation::from_raw(self).expect("Valid Pending attestation"); + write!(f, "{}", att) + } + _ => write!(f, "Unknown Attestation with tag {}", Hexed(&self.tag)), } } } diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index d28b853..9f921dc 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -1,7 +1,7 @@ //! ** The implementation here is subject to change as this is a read-only version. ** use crate::{ - codec::v1::{Attestation, opcode::OpCode}, + codec::v1::{attestation::RawAttestation, opcode::OpCode}, utils::Hexed, }; use alloc::{alloc::Global, vec::Vec}; @@ -32,7 +32,7 @@ mod fmt; #[derive(Clone, Debug)] pub enum Timestamp { Step(Step), - Attestation(Attestation), + Attestation(RawAttestation), } /// An execution Step. diff --git a/crates/core/src/codec/v1/timestamp/decode.rs b/crates/core/src/codec/v1/timestamp/decode.rs index 533abc3..d1218db 100644 --- a/crates/core/src/codec/v1/timestamp/decode.rs +++ b/crates/core/src/codec/v1/timestamp/decode.rs @@ -34,7 +34,7 @@ impl Timestamp { ) -> Result, DecodeError> { match op { OpCode::ATTESTATION => { - let attestation = Attestation::decode(decoder)?; + let attestation = RawAttestation::decode_in(decoder, alloc)?; Ok(Timestamp::Attestation(attestation)) } OpCode::FORK => { diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index de5b444..e5a4946 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -9,6 +9,9 @@ pub enum DecodeError { /// File has a version we do not understand. #[error("bad version")] BadVersion, + /// Expected an attestation tag but decoded something else. + #[error("bad attestation tag")] + BadAttestationTag, /// Read an LEB128-encoded integer that overflowed the expected size. #[error("read a LEB128 value overflows {0} bits")] LEB128Overflow(u32), @@ -24,6 +27,9 @@ pub enum DecodeError { /// Encountered an invalid character in a URI. #[error("invalid character in URI")] InvalidUriChar, + /// URI is too long. + #[error("URI too long")] + UriTooLong, /// Recursed deeper than allowed while decoding the proof. #[error("recursion limit reached")] RecursionLimit, @@ -42,6 +48,12 @@ pub enum EncodeError { /// Tried to encode a `usize` exceeding `u32::MAX`. #[error("tried to encode a usize exceeding u32::MAX")] UsizeOverflow, + /// Encountered an invalid character in a URI. + #[error("invalid character in URI")] + InvalidUriChar, + /// URI is too long. + #[error("URI too long")] + UriTooLong, /// General I/O error #[cfg(feature = "std")] #[error("I/O error: {0}")] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index ce502c9..e524fb4 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -4,6 +4,7 @@ //! # Universal Timestamps Core Library extern crate alloc; +extern crate core; mod tracing; From b74c929caea4a4a17006eccc8d044d5187055e15 Mon Sep 17 00:00:00 2001 From: lightsing Date: Mon, 22 Dec 2025 16:51:30 +0800 Subject: [PATCH 05/11] clippy --- crates/calendar/src/routes/ots.rs | 6 +++--- crates/core/src/codec/imp/primitives.rs | 2 +- crates/core/src/codec/imp/std_io.rs | 2 +- crates/core/src/codec/v1/attestation.rs | 4 ++-- crates/core/src/codec/v1/digest.rs | 2 +- crates/core/src/codec/v1/opcode.rs | 10 ++-------- crates/core/src/codec/v1/timestamp/fmt.rs | 2 +- 7 files changed, 11 insertions(+), 17 deletions(-) diff --git a/crates/calendar/src/routes/ots.rs b/crates/calendar/src/routes/ots.rs index 0cac408..4b847b0 100644 --- a/crates/calendar/src/routes/ots.rs +++ b/crates/calendar/src/routes/ots.rs @@ -67,7 +67,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { trace!(recv_timestamp); let recv_timestamp = recv_timestamp.to_le_bytes(); timestamp.encode(OpCode::PREPEND).unwrap(); - timestamp.encode_bytes(&recv_timestamp).unwrap(); + timestamp.encode_bytes(recv_timestamp).unwrap(); pending_attestation.extend(recv_timestamp); trace!(digest = ?Hexed(&digest)); @@ -81,7 +81,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { let undeniable_sig = undeniable_sig.as_erc2098(); trace!(undeniable_sig = ?Hexed(&undeniable_sig)); timestamp.encode(OpCode::APPEND).unwrap(); - timestamp.encode_bytes(&undeniable_sig).unwrap(); + timestamp.encode_bytes(undeniable_sig).unwrap(); pending_attestation.extend(undeniable_sig); trace!(pending_attestation = ?Hexed(&pending_attestation)); @@ -99,7 +99,7 @@ pub async fn submit_digest(digest: Bytes) -> Bytes { timestamp.encode(OpCode::KECCAK256).unwrap(); timestamp.encode(OpCode::ATTESTATION).unwrap(); - timestamp.encode(&attestation.to_raw().unwrap()).unwrap(); + timestamp.encode(attestation.to_raw().unwrap()).unwrap(); // TODO: store the pending_attestation into journal debug_assert_eq!(timestamp.len(), buf_size, "buffer size mismatch"); diff --git a/crates/core/src/codec/imp/primitives.rs b/crates/core/src/codec/imp/primitives.rs index 8394579..b4f834e 100644 --- a/crates/core/src/codec/imp/primitives.rs +++ b/crates/core/src/codec/imp/primitives.rs @@ -67,7 +67,7 @@ leb128!(u16, u32, u64, u128); impl Encode for u8 { #[inline] fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { - encoder.write_all(&[*self])?; + encoder.write_all([*self])?; Ok(()) } } diff --git a/crates/core/src/codec/imp/std_io.rs b/crates/core/src/codec/imp/std_io.rs index 18a2c49..205eb16 100644 --- a/crates/core/src/codec/imp/std_io.rs +++ b/crates/core/src/codec/imp/std_io.rs @@ -6,7 +6,7 @@ pub struct Reader(pub R); impl Encoder for Writer { fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { - self.write_all(&[byte])?; + self.write_all([byte])?; Ok(()) } diff --git a/crates/core/src/codec/v1/attestation.rs b/crates/core/src/codec/v1/attestation.rs index b9bb359..f429c2d 100644 --- a/crates/core/src/codec/v1/attestation.rs +++ b/crates/core/src/codec/v1/attestation.rs @@ -47,7 +47,7 @@ impl DecodeIn for RawAttestation { impl Encode for RawAttestation { #[inline] fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { - encoder.write_all(&self.tag)?; + encoder.write_all(self.tag)?; encoder.encode_bytes(&self.data) } } @@ -143,7 +143,7 @@ impl<'a> Attestation<'a> for PendingAttestation<'a> { if length as usize > Self::MAX_URI_LEN { return Err(DecodeError::UriTooLong); } - let uri = core::str::from_utf8(&data).map_err(|_| DecodeError::InvalidUriChar)?; + let uri = core::str::from_utf8(data).map_err(|_| DecodeError::InvalidUriChar)?; if !Self::validate_uri(uri) { return Err(DecodeError::InvalidUriChar); } diff --git a/crates/core/src/codec/v1/digest.rs b/crates/core/src/codec/v1/digest.rs index b528f87..536b636 100644 --- a/crates/core/src/codec/v1/digest.rs +++ b/crates/core/src/codec/v1/digest.rs @@ -43,7 +43,7 @@ impl Encode for DigestHeader { #[cfg_attr(feature = "tracing", tracing::instrument(skip(writer), err))] #[inline] fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { - encoder.encode(&self.kind)?; + encoder.encode(self.kind)?; encoder.write_all(&self.digest[..self.kind.output_size()])?; Ok(()) } diff --git a/crates/core/src/codec/v1/opcode.rs b/crates/core/src/codec/v1/opcode.rs index 81dcc1c..fede904 100644 --- a/crates/core/src/codec/v1/opcode.rs +++ b/crates/core/src/codec/v1/opcode.rs @@ -61,19 +61,13 @@ impl OpCode { /// Returns `true` when the opcode requires an immediate operand. #[inline] pub const fn has_immediate(&self) -> bool { - match *self { - Self::APPEND | Self::PREPEND => true, - _ => false, - } + matches!(*self, Self::APPEND | Self::PREPEND) } /// Returns `true` for control opcodes. #[inline] pub const fn is_control(&self) -> bool { - match *self { - Self::ATTESTATION | Self::FORK => true, - _ => false, - } + matches!(*self, Self::APPEND | Self::PREPEND) } /// Returns `true` for digest opcodes. diff --git a/crates/core/src/codec/v1/timestamp/fmt.rs b/crates/core/src/codec/v1/timestamp/fmt.rs index dc02034..9bacf49 100644 --- a/crates/core/src/codec/v1/timestamp/fmt.rs +++ b/crates/core/src/codec/v1/timestamp/fmt.rs @@ -58,7 +58,7 @@ impl Timestamp { } let result = if let Some(input) = input { - let result = op.execute(&input, &step.data); + let result = op.execute(input, &step.data); indent(f, depth, false)?; writeln!(f, " result {}", Hexed(&result))?; Some(result) From 2e158eb2946d5bcab8fc0e29e975d89a1d6a1df8 Mon Sep 17 00:00:00 2001 From: lightsing Date: Mon, 22 Dec 2025 16:56:08 +0800 Subject: [PATCH 06/11] return when ok --- crates/core/src/bin/uts_info.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/core/src/bin/uts_info.rs b/crates/core/src/bin/uts_info.rs index b0f26e9..c7d11ea 100644 --- a/crates/core/src/bin/uts_info.rs +++ b/crates/core/src/bin/uts_info.rs @@ -36,6 +36,7 @@ fn main() { Ok(ots) => { println!("OTS Detached Timestamp found:"); println!("{ots}"); + return; } Err(e) => { println!( From 768e368b3f0f4b296b2a7e721029745264a94aca Mon Sep 17 00:00:00 2001 From: lightsing Date: Mon, 22 Dec 2025 17:07:34 +0800 Subject: [PATCH 07/11] fix --- crates/core/src/codec/v1/timestamp.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index 47301eb..9f921dc 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -11,7 +11,6 @@ mod builder; mod decode; mod encode; mod fmt; -mod builder; /// Proof that that one or more attestations commit to a message. /// From 081e7f40bcd2ffbb0fc9b7406ce9176fb85c03f4 Mon Sep 17 00:00:00 2001 From: lightsing Date: Tue, 23 Dec 2025 15:14:14 +0800 Subject: [PATCH 08/11] full integration with allocator API --- Cargo.lock | 2 +- Cargo.toml | 1 + crates/core/Cargo.toml | 2 +- crates/core/src/codec/imp.rs | 1 - crates/core/src/codec/imp/primitives.rs | 12 +-- crates/core/src/codec/imp/smallvec.rs | 14 --- crates/core/src/codec/proof.rs | 26 +++-- crates/core/src/codec/v1/attestation.rs | 28 +++++- .../core/src/codec/v1/detached_timestamp.rs | 44 ++++++--- crates/core/src/codec/v1/digest.rs | 7 +- crates/core/src/codec/v1/opcode.rs | 48 ++++++--- crates/core/src/codec/v1/timestamp.rs | 99 ++++++++++++++++++- crates/core/src/codec/v1/timestamp/decode.rs | 29 ++++-- crates/core/src/codec/v1/timestamp/encode.rs | 2 +- crates/core/src/codec/v1/timestamp/fmt.rs | 29 +++--- crates/core/src/utils.rs | 22 +---- crates/core/src/utils/hex.rs | 19 ++++ crates/core/src/utils/sync.rs | 9 ++ crates/core/src/utils/sync/race.rs | 20 ++++ crates/core/src/utils/sync/std.rs | 20 ++++ 20 files changed, 323 insertions(+), 111 deletions(-) delete mode 100644 crates/core/src/codec/imp/smallvec.rs create mode 100644 crates/core/src/utils/hex.rs create mode 100644 crates/core/src/utils/sync.rs create mode 100644 crates/core/src/utils/sync/race.rs create mode 100644 crates/core/src/utils/sync/std.rs diff --git a/Cargo.lock b/Cargo.lock index d17520b..fad715a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4549,13 +4549,13 @@ dependencies = [ "criterion 0.5.1", "digest 0.11.0-rc.4", "hex", + "once_cell", "opentimestamps", "paste", "ripemd", "sha1", "sha2 0.11.0-rc.3", "sha3 0.11.0-rc.3", - "smallvec", "thiserror 2.0.17", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index b3bb4ed..56eaa18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ toml = "0.9" tower-http = "0.6" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +once_cell = { version = "1.21", default-features = false } digest = "0.11.0-rc.4" ripemd = "0.2.0-rc.3" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8b7c422..b7d1ca0 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -23,7 +23,7 @@ ripemd.workspace = true sha1.workspace = true sha2.workspace = true sha3.workspace = true -smallvec = { workspace = true, features = ["specialization", "may_dangle"] } +once_cell = { workspace = true, features = ["alloc"] } thiserror.workspace = true tracing = { workspace = true, optional = true } bytes = { workspace = true, optional = true } diff --git a/crates/core/src/codec/imp.rs b/crates/core/src/codec/imp.rs index 3cc91b1..397da10 100644 --- a/crates/core/src/codec/imp.rs +++ b/crates/core/src/codec/imp.rs @@ -4,7 +4,6 @@ use alloc::vec::Vec; #[cfg(feature = "bytes")] mod bytes; mod primitives; -mod smallvec; #[cfg(feature = "std")] mod std_io; diff --git a/crates/core/src/codec/imp/primitives.rs b/crates/core/src/codec/imp/primitives.rs index b4f834e..afd129d 100644 --- a/crates/core/src/codec/imp/primitives.rs +++ b/crates/core/src/codec/imp/primitives.rs @@ -31,9 +31,9 @@ macro_rules! leb128 { } } - impl crate::codec::Decode for $ty { + impl crate::codec::DecodeIn for $ty { #[inline] - fn decode(decoder: &mut impl crate::codec::Decoder) -> Result { + fn decode_in(decoder: &mut impl crate::codec::Decoder, _alloc: A) -> Result { let mut ret: $ty = 0; let mut shift: u32 = 0; @@ -72,9 +72,9 @@ impl Encode for u8 { } } -impl Decode for u8 { +impl DecodeIn for u8 { #[inline] - fn decode(decoder: &mut impl Decoder) -> Result { + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { let mut byte = [0u8; 1]; decoder.read_exact(&mut byte)?; Ok(byte[0]) @@ -90,9 +90,9 @@ impl Encode for usize { } } -impl Decode for usize { +impl DecodeIn for usize { #[inline] - fn decode(decoder: &mut impl Decoder) -> Result { + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { let val = u32::decode(decoder)?; Ok(val as usize) } diff --git a/crates/core/src/codec/imp/smallvec.rs b/crates/core/src/codec/imp/smallvec.rs deleted file mode 100644 index f89bb5e..0000000 --- a/crates/core/src/codec/imp/smallvec.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::codec::*; -use smallvec::SmallVec; - -impl Encoder for SmallVec<[u8; N]> { - fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { - self.push(byte); - Ok(()) - } - - fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { - self.extend_from_slice(buf.as_ref()); - Ok(()) - } -} diff --git a/crates/core/src/codec/proof.rs b/crates/core/src/codec/proof.rs index 525da86..c01e82d 100644 --- a/crates/core/src/codec/proof.rs +++ b/crates/core/src/codec/proof.rs @@ -1,44 +1,50 @@ use crate::{ - codec::{Decode, Decoder, Encode, Encoder}, + codec::{DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; +use alloc::alloc::{Allocator, Global}; use core::fmt; /// Version number of the serialization format. pub type Version = u32; /// Trait implemented by proof payloads for a specific serialization version. -pub trait Proof: Encode + Decode { +pub trait Proof: Encode + DecodeIn { /// Version identifier that must match the encoded proof. const VERSION: Version; } /// Wrapper that prefixes a proof with its version and magic bytes. #[derive(Clone, PartialEq, Eq, Debug)] -#[repr(transparent)] -pub struct VersionedProof(pub T); +pub struct VersionedProof, A: Allocator = Global> { + pub proof: T, + allocator: A, +} -impl Decode for VersionedProof { - fn decode(decoder: &mut impl Decoder) -> Result { +impl, A: Allocator + Clone> DecodeIn for VersionedProof { + fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { decoder.assert_magic()?; let version: Version = decoder.decode()?; if version != T::VERSION { return Err(DecodeError::BadVersion); } - Ok(VersionedProof(T::decode(decoder)?)) + Ok(VersionedProof { + proof: T::decode_in(decoder, alloc.clone())?, + allocator: alloc, + }) } } -impl Encode for VersionedProof { +impl, A: Allocator> Encode for VersionedProof { fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { encoder.encode_magic()?; encoder.encode(T::VERSION)?; - self.0.encode(encoder) + self.proof.encode(encoder) } } impl fmt::Display for VersionedProof { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Version {} Proof {}", T::VERSION, self.0) + write!(f, "Version {} Proof {}", T::VERSION, self.proof) } } diff --git a/crates/core/src/codec/v1/attestation.rs b/crates/core/src/codec/v1/attestation.rs index f429c2d..224ab6f 100644 --- a/crates/core/src/codec/v1/attestation.rs +++ b/crates/core/src/codec/v1/attestation.rs @@ -6,11 +6,14 @@ use crate::{ codec::{Decode, DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, - utils::Hexed, + utils::{Hexed, OnceLock}, +}; +use alloc::{ + alloc::{Allocator, Global}, + borrow::Cow, + vec::Vec, }; -use alloc::{borrow::Cow, vec::Vec}; use core::fmt; -use std::alloc::{Allocator, Global}; /// Size in bytes of the tag identifying the attestation type. const TAG_SIZE: usize = 8; @@ -28,6 +31,8 @@ pub type AttestationTag = [u8; TAG_SIZE]; pub struct RawAttestation { pub tag: AttestationTag, pub data: Vec, + /// Cached value for verifying the attestation. + pub(crate) value: OnceLock>, } impl DecodeIn for RawAttestation { @@ -40,11 +45,15 @@ impl DecodeIn for RawAttestation { data.resize(len, 0); decoder.read_exact(&mut data)?; - Ok(RawAttestation { tag, data }) + Ok(RawAttestation { + tag, + data, + value: OnceLock::new(), + }) } } -impl Encode for RawAttestation { +impl Encode for RawAttestation { #[inline] fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { encoder.write_all(self.tag)?; @@ -60,6 +69,14 @@ impl PartialEq for RawAttestation { impl Eq for RawAttestation {} +impl RawAttestation { + /// Returns the allocator used by this raw attestation. + #[inline] + pub fn allocator(&self) -> &A { + self.data.allocator() + } +} + pub trait Attestation<'a>: Sized { const TAG: AttestationTag; @@ -79,6 +96,7 @@ pub trait Attestation<'a>: Sized { Ok(RawAttestation { tag: Self::TAG, data: self.to_raw_data_in(alloc)?, + value: OnceLock::new(), }) } diff --git a/crates/core/src/codec/v1/detached_timestamp.rs b/crates/core/src/codec/v1/detached_timestamp.rs index 7a2f2d3..7626851 100644 --- a/crates/core/src/codec/v1/detached_timestamp.rs +++ b/crates/core/src/codec/v1/detached_timestamp.rs @@ -1,9 +1,9 @@ use crate::codec::{ - Decode, Encode, Encoder, Proof, Version, + Decode, DecodeIn, Encode, Encoder, Proof, Version, v1::{DigestHeader, Timestamp}, }; +use alloc::alloc::{Allocator, Global}; use core::{fmt, fmt::Formatter}; -use smallvec::ToSmallVec; /// A file containing a timestamp for another file /// Contains a timestamp, along with a header and the digest of the file. @@ -12,24 +12,27 @@ use smallvec::ToSmallVec; /// which don't encode/decode the magic and version. /// The Python version is equivalent to `VersionedProof`. #[derive(Clone, PartialEq, Eq, Debug)] -pub struct DetachedTimestamp { +pub struct DetachedTimestamp { header: DigestHeader, - timestamp: Timestamp, + timestamp: Timestamp, } -impl Proof for DetachedTimestamp { +impl Proof for DetachedTimestamp { const VERSION: Version = 1; } -impl Decode for DetachedTimestamp { - fn decode(decoder: &mut impl crate::codec::Decoder) -> Result { +impl DecodeIn for DetachedTimestamp { + fn decode_in( + decoder: &mut impl crate::codec::Decoder, + alloc: A, + ) -> Result { let header = DigestHeader::decode(decoder)?; - let timestamp = Timestamp::decode(decoder)?; + let timestamp = Timestamp::decode_in(decoder, alloc)?; Ok(DetachedTimestamp { header, timestamp }) } } -impl Encode for DetachedTimestamp { +impl Encode for DetachedTimestamp { fn encode(&self, encoder: &mut impl Encoder) -> Result<(), crate::error::EncodeError> { self.header.encode(encoder)?; self.timestamp.encode(encoder)?; @@ -37,12 +40,29 @@ impl Encode for DetachedTimestamp { } } -impl fmt::Display for DetachedTimestamp { +impl fmt::Display for DetachedTimestamp { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { writeln!(f, "digest of {}", self.header)?; - self.timestamp - .fmt(Some(&self.header.digest().to_smallvec()), f) + self.timestamp.fmt(Some(self.header.digest()), f) + } +} + +impl DetachedTimestamp { + /// Returns the digest header. + pub fn header(&self) -> &DigestHeader { + &self.header + } + + /// Returns the timestamp. + pub fn timestamp(&self) -> &Timestamp { + &self.timestamp + } + + /// Returns the allocator used by this detached timestamp. + #[inline] + pub fn allocator(&self) -> &A { + self.timestamp.allocator() } } diff --git a/crates/core/src/codec/v1/digest.rs b/crates/core/src/codec/v1/digest.rs index 536b636..5eeb85c 100644 --- a/crates/core/src/codec/v1/digest.rs +++ b/crates/core/src/codec/v1/digest.rs @@ -1,8 +1,9 @@ use crate::{ - codec::{Decode, Decoder, Encode, Encoder, v1::opcode::DigestOp}, + codec::{DecodeIn, Decoder, Encode, Encoder, v1::opcode::DigestOp}, error::{DecodeError, EncodeError}, utils::Hexed, }; +use alloc::alloc::Allocator; use core::fmt; /// Header describing the digest that anchors a timestamp. @@ -49,10 +50,10 @@ impl Encode for DigestHeader { } } -impl Decode for DigestHeader { +impl DecodeIn for DigestHeader { #[cfg_attr(feature = "tracing", tracing::instrument(skip(reader), ret, err))] #[inline] - fn decode(decoder: &mut impl Decoder) -> Result { + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { let kind = decoder.decode()?; let mut digest = [0u8; 32]; decoder.read_exact(&mut digest)?; diff --git a/crates/core/src/codec/v1/opcode.rs b/crates/core/src/codec/v1/opcode.rs index fede904..bfb8da5 100644 --- a/crates/core/src/codec/v1/opcode.rs +++ b/crates/core/src/codec/v1/opcode.rs @@ -3,18 +3,16 @@ //! It contains opcode information and utilities to work with opcodes. use crate::{ - codec::{Decode, Decoder, Encode, Encoder}, + codec::{Decode, DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; +use alloc::{alloc::Allocator, vec::Vec}; use core::{fmt, hint::unreachable_unchecked}; use digest::{Digest, OutputSizeUser, typenum::Unsigned}; use ripemd::Ripemd160; use sha1::Sha1; use sha2::Sha256; use sha3::Keccak256; -use smallvec::ToSmallVec; - -pub(crate) type OperationBuffer = smallvec::SmallVec<[u8; 64]>; /// An OpenTimestamps opcode. /// @@ -67,7 +65,7 @@ impl OpCode { /// Returns `true` for control opcodes. #[inline] pub const fn is_control(&self) -> bool { - matches!(*self, Self::APPEND | Self::PREPEND) + matches!(*self, Self::ATTESTATION | Self::FORK) } /// Returns `true` for digest opcodes. @@ -91,26 +89,41 @@ impl OpCode { /// /// Panics if the opcode is a control opcode. #[inline] - pub fn execute(&self, input: impl AsRef<[u8]>, immediate: impl AsRef<[u8]>) -> OperationBuffer { + pub fn execute(&self, input: impl AsRef<[u8]>, immediate: impl AsRef<[u8]>) -> Vec { + self.execute_in(input, immediate, alloc::alloc::Global) + } + + /// Executes the opcode on the given input data, with an optional immediate value. + /// + /// # Panics + /// + /// Panics if the opcode is a control opcode. + #[inline] + pub fn execute_in( + &self, + input: impl AsRef<[u8]>, + immediate: impl AsRef<[u8]>, + alloc: A, + ) -> Vec { if let Some(digest_op) = self.as_digest() { - return digest_op.execute(input); + return digest_op.execute_in(input, alloc); } let input = input.as_ref(); match *self { Self::APPEND => { - let mut out = OperationBuffer::from_slice(input); + let mut out = input.to_vec_in(alloc); out.extend_from_slice(immediate.as_ref()); out } Self::PREPEND => { - let mut out = OperationBuffer::from_slice(immediate.as_ref()); + let mut out = input.to_vec_in(alloc); out.extend_from_slice(input); out } Self::REVERSE => { let len = input.len(); - let mut out = OperationBuffer::with_capacity(len); + let mut out = Vec::::with_capacity_in(len, alloc); unsafe { // SAFETY: The vector capacity is set to len, so setting the length to len is valid. @@ -128,7 +141,7 @@ impl OpCode { } Self::HEXLIFY => { let hex_len = input.len() * 2; - let mut out = OperationBuffer::with_capacity(hex_len); + let mut out = Vec::::with_capacity_in(hex_len, alloc); // SAFETY: that the vector is actually the specified size. unsafe { out.set_len(hex_len); @@ -188,9 +201,9 @@ impl Encode for DigestOp { } } -impl Decode for DigestOp { +impl DecodeIn for DigestOp { #[inline] - fn decode(decoder: &mut impl Decoder) -> Result { + fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { let opcode = OpCode::decode(decoder)?; opcode .as_digest() @@ -270,13 +283,18 @@ macro_rules! define_digest_opcodes { } /// Executes the digest operation on the input data. - pub fn execute(&self, input: impl AsRef<[u8]>) -> OperationBuffer { + pub fn execute(&self, input: impl AsRef<[u8]>) -> ::alloc::vec::Vec { + self.execute_in(input, ::alloc::alloc::Global) + } + + /// Executes the digest operation on the input data. + pub fn execute_in(&self, input: impl AsRef<[u8]>, alloc: A) -> ::alloc::vec::Vec { match *self { $( Self::$variant => { paste::paste! { let mut hasher = [<$variant:camel>]::new(); hasher.update(input); - hasher.finalize().to_smallvec() + hasher.finalize().to_vec_in(alloc) } }, )* // SAFETY: unreachable as all variants are covered. diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index 9f921dc..245ce6b 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -2,7 +2,7 @@ use crate::{ codec::v1::{attestation::RawAttestation, opcode::OpCode}, - utils::Hexed, + utils::{Hexed, OnceLock}, }; use alloc::{alloc::Global, vec::Vec}; use core::{alloc::Allocator, fmt::Debug}; @@ -40,6 +40,7 @@ pub enum Timestamp { pub struct Step { op: OpCode, data: Vec, + input: OnceLock>, next: Vec, A>, } @@ -71,12 +72,104 @@ impl Debug for Step { f.field("next", &self.next).finish() } } -impl Timestamp { + +impl Timestamp { /// Returns the opcode of this timestamp node. pub fn op(&self) -> OpCode { match self { - Timestamp::Step(step) => step.op, + Timestamp::Step(step) => { + debug_assert_ne!( + step.op, + OpCode::ATTESTATION, + "sanity check failed: Step with ATTESTATION opcode" + ); + step.op + } Timestamp::Attestation(_) => OpCode::ATTESTATION, } } + + /// Returns this timestamp as a step, if it is one. + #[inline] + pub fn as_step(&self) -> Option<&Step> { + match self { + Timestamp::Step(step) => Some(step), + Timestamp::Attestation(_) => None, + } + } + + /// Returns this timestamp as an attestation, if it is one. + #[inline] + pub fn as_attestation(&self) -> Option<&RawAttestation> { + match self { + Timestamp::Attestation(attestation) => Some(attestation), + Timestamp::Step(_) => None, + } + } + + /// Returns the input data for this timestamp node, if finalized. + #[inline] + pub fn input(&self) -> Option<&[u8]> { + match self { + Timestamp::Step(step) => step.input.get().map(|v| v.as_slice()), + Timestamp::Attestation(attestation) => attestation.value.get().map(|v| v.as_slice()), + } + } + + /// Returns the allocator used by this timestamp node. + #[inline] + pub fn allocator(&self) -> &A { + match self { + Self::Attestation(attestation) => attestation.allocator(), + Self::Step(step) => step.allocator(), + } + } +} + +impl Timestamp { + /// Finalizes the timestamp with the given input data. + /// + /// # Panics + /// + /// Panics if the timestamp is already finalized with different input data. + pub fn finalize(&self, input: Vec) { + match self { + Self::Attestation(attestation) => { + if let Some(already) = attestation.value.get() { + assert_eq!(&input, already, "trying to finalize with different input"); + return; + } + let _ = attestation.value.get_or_init(|| input); + } + Self::Step(step) => { + if let Some(already) = step.input.get() { + assert_eq!(&input, already, "trying to finalize with different input"); + return; + } + let input = step.input.get_or_init(|| input); + + match step.op { + OpCode::FORK => { + debug_assert!(step.next.len() >= 2, "FORK must have at least two children"); + for child in &step.next { + child.finalize(input.clone()); + } + } + OpCode::ATTESTATION => unreachable!("should not happen"), + op => { + let output = op.execute_in(input, &step.data, step.allocator().clone()); + debug_assert!(step.next.len() == 1, "non-FORK must have exactly one child"); + step.next[0].finalize(output); + } + } + } + } + } +} + +impl Step { + /// Returns the allocator used by this step. + pub(crate) fn allocator(&self) -> &A { + self.data.allocator() + } } diff --git a/crates/core/src/codec/v1/timestamp/decode.rs b/crates/core/src/codec/v1/timestamp/decode.rs index d1218db..3980f8d 100644 --- a/crates/core/src/codec/v1/timestamp/decode.rs +++ b/crates/core/src/codec/v1/timestamp/decode.rs @@ -6,13 +6,13 @@ use crate::{ const RECURSION_LIMIT: usize = 256; -impl DecodeIn for Timestamp { +impl DecodeIn for Timestamp { fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { Self::decode_recursive(decoder, RECURSION_LIMIT, alloc) } } -impl Timestamp { +impl Timestamp { fn decode_recursive( decoder: &mut impl Decoder, recursion_limit: usize, @@ -38,17 +38,23 @@ impl Timestamp { Ok(Timestamp::Attestation(attestation)) } OpCode::FORK => { - let mut children = Vec::new_in(alloc); + let mut children = Vec::new_in(alloc.clone()); let mut next_op = OpCode::FORK; while next_op == OpCode::FORK { - let child = Self::decode_recursive(&mut *decoder, limit - 1, alloc)?; + let child = Self::decode_recursive(&mut *decoder, limit - 1, alloc.clone())?; children.push(child); next_op = OpCode::decode(&mut *decoder)?; } - children.push(Self::decode_from_op(next_op, decoder, limit - 1, alloc)?); + children.push(Self::decode_from_op( + next_op, + decoder, + limit - 1, + alloc.clone(), + )?); Ok(Timestamp::Step(Step { op: OpCode::FORK, data: Vec::new_in(alloc), + input: OnceLock::new(), next: children, })) } @@ -56,19 +62,24 @@ impl Timestamp { let data = if op.has_immediate() { const MAX_OP_LENGTH: usize = 4096; let length = decoder.decode_ranged(1..MAX_OP_LENGTH)?; - let mut data = Vec::with_capacity_in(length, alloc); + let mut data = Vec::with_capacity_in(length, alloc.clone()); data.resize(length, 0); decoder.read_exact(&mut data)?; data } else { - Vec::new_in(alloc) + Vec::new_in(alloc.clone()) }; - let mut next = Vec::with_capacity_in(1, alloc); + let mut next = Vec::with_capacity_in(1, alloc.clone()); next.push(Self::decode_recursive(decoder, limit - 1, alloc)?); - Ok(Timestamp::Step(Step { op, data, next })) + Ok(Timestamp::Step(Step { + op, + data, + input: OnceLock::new(), + next, + })) } } } diff --git a/crates/core/src/codec/v1/timestamp/encode.rs b/crates/core/src/codec/v1/timestamp/encode.rs index 185dc87..e9e97e2 100644 --- a/crates/core/src/codec/v1/timestamp/encode.rs +++ b/crates/core/src/codec/v1/timestamp/encode.rs @@ -4,7 +4,7 @@ use crate::{ error::EncodeError, }; -impl Encode for Timestamp { +impl Encode for Timestamp { fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { match self { Self::Attestation(attestation) => { diff --git a/crates/core/src/codec/v1/timestamp/fmt.rs b/crates/core/src/codec/v1/timestamp/fmt.rs index 9bacf49..17c6d6a 100644 --- a/crates/core/src/codec/v1/timestamp/fmt.rs +++ b/crates/core/src/codec/v1/timestamp/fmt.rs @@ -1,25 +1,28 @@ use super::*; -use crate::{codec::v1::opcode::OperationBuffer, utils::Hexed}; +use crate::utils::Hexed; use core::fmt; -impl fmt::Display for Timestamp { +impl fmt::Display for Timestamp { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_recurse(None, f, 0, true) } } -impl Timestamp { - pub(crate) fn fmt( - &self, - input: Option<&OperationBuffer>, - f: &mut fmt::Formatter, - ) -> fmt::Result { +impl Timestamp { + pub(crate) fn fmt(&self, input: Option<&[u8]>, f: &mut fmt::Formatter) -> fmt::Result { + let input = match input { + Some(input) => Some(input), + None => match self { + Self::Step(step) => step.input.get().map(|v| v.as_slice()), + Self::Attestation(_) => None, + }, + }; self.fmt_recurse(input, f, 0, true) } fn fmt_recurse( &self, - input: Option<&OperationBuffer>, + input: Option<&[u8]>, f: &mut fmt::Formatter, depth: usize, first_line: bool, @@ -57,8 +60,10 @@ impl Timestamp { writeln!(f, "execute {op}")?; } - let result = if let Some(input) = input { - let result = op.execute(input, &step.data); + let result = if let Some(value) = step.next.first().and_then(|next| next.input()) { + Some(value.to_vec_in(step.allocator().clone())) + } else if let Some(input) = input { + let result = op.execute_in(input, &step.data, step.allocator().clone()); indent(f, depth, false)?; writeln!(f, " result {}", Hexed(&result))?; Some(result) @@ -67,7 +72,7 @@ impl Timestamp { }; for child in &step.next { - child.fmt_recurse(result.as_ref(), f, depth, false)?; + child.fmt_recurse(result.as_deref(), f, depth, false)?; } Ok(()) } diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 8a1948b..6863042 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,19 +1,5 @@ -use core::fmt; +mod hex; +pub use hex::Hexed; -/// Zero-allocation wrapper that displays byte slices as lowercase hex. -pub struct Hexed<'a, T: ?Sized>(pub &'a T); - -impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Display for Hexed<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for b in self.0.as_ref() { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} - -impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Debug for Hexed<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(self, f) - } -} +mod sync; +pub use sync::OnceLock; diff --git a/crates/core/src/utils/hex.rs b/crates/core/src/utils/hex.rs new file mode 100644 index 0000000..8a1948b --- /dev/null +++ b/crates/core/src/utils/hex.rs @@ -0,0 +1,19 @@ +use core::fmt; + +/// Zero-allocation wrapper that displays byte slices as lowercase hex. +pub struct Hexed<'a, T: ?Sized>(pub &'a T); + +impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Display for Hexed<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for b in self.0.as_ref() { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + +impl<'a, T: ?Sized + AsRef<[u8]>> fmt::Debug for Hexed<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} diff --git a/crates/core/src/utils/sync.rs b/crates/core/src/utils/sync.rs new file mode 100644 index 0000000..660f4c9 --- /dev/null +++ b/crates/core/src/utils/sync.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "std")] +mod std; +#[cfg(feature = "std")] +pub use self::std::OnceLock; + +#[cfg(not(feature = "std"))] +mod race; +#[cfg(not(feature = "std"))] +pub use self::race::OnceLock; diff --git a/crates/core/src/utils/sync/race.rs b/crates/core/src/utils/sync/race.rs new file mode 100644 index 0000000..1ee7567 --- /dev/null +++ b/crates/core/src/utils/sync/race.rs @@ -0,0 +1,20 @@ +#[derive(Default, Debug, Clone)] +#[repr(transparent)] +pub struct OnceLock(once_cell::race::OnceBox); + +impl OnceLock { + pub const fn new() -> OnceLock { + OnceLock(once_cell::race::OnceBox::new()) + } + + pub fn get(&self) -> Option<&T> { + self.0.get() + } + + pub fn get_or_init(&self, init: F) -> &T + where + F: FnOnce() -> T, + { + self.0.get_or_init(|| alloc::boxed::Box::new(init())) + } +} diff --git a/crates/core/src/utils/sync/std.rs b/crates/core/src/utils/sync/std.rs new file mode 100644 index 0000000..3078519 --- /dev/null +++ b/crates/core/src/utils/sync/std.rs @@ -0,0 +1,20 @@ +#[derive(Default, Debug, Clone)] +#[repr(transparent)] +pub struct OnceLock(std::sync::OnceLock); + +impl OnceLock { + pub const fn new() -> OnceLock { + OnceLock(std::sync::OnceLock::new()) + } + + pub fn get(&self) -> Option<&T> { + self.0.get() + } + + pub fn get_or_init(&self, init: F) -> &T + where + F: FnOnce() -> T, + { + self.0.get_or_init(init) + } +} From cefa71b33140fc927ca883dfae6640d7d63551c3 Mon Sep 17 00:00:00 2001 From: lightsing Date: Tue, 23 Dec 2025 15:22:11 +0800 Subject: [PATCH 09/11] fmt --- Cargo.toml | 2 +- crates/core/Cargo.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 56eaa18..4887329 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ const_format = "0.2" criterion = { version = "0.8", features = ["html_reports"] } eyre = "0.6" hex = "0.4" +once_cell = { version = "1.21", default-features = false } paste = "1.0" regex = "1.12" serde = "1.0" @@ -56,7 +57,6 @@ toml = "0.9" tower-http = "0.6" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -once_cell = { version = "1.21", default-features = false } digest = "0.11.0-rc.4" ripemd = "0.2.0-rc.3" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index b7d1ca0..c6c9391 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,23 +16,23 @@ required-features = ["std"] [dependencies] auto_impl.workspace = true +bytes = { workspace = true, optional = true } digest.workspace = true hex.workspace = true +once_cell = { workspace = true, features = ["alloc"] } paste.workspace = true ripemd.workspace = true sha1.workspace = true sha2.workspace = true sha3.workspace = true -once_cell = { workspace = true, features = ["alloc"] } thiserror.workspace = true tracing = { workspace = true, optional = true } -bytes = { workspace = true, optional = true } [features] +bytes = ["dep:bytes"] default = ["std"] -tracing = ["dep:tracing"] std = [] -bytes = ["dep:bytes"] +tracing = ["dep:tracing"] [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } From 1e653384580151c831b85aaf31bb4fd266317ac7 Mon Sep 17 00:00:00 2001 From: lightsing Date: Tue, 23 Dec 2025 15:55:29 +0800 Subject: [PATCH 10/11] apply review --- crates/core/src/codec/imp/std_io.rs | 7 +++++-- crates/core/src/codec/proof.rs | 2 +- crates/core/src/codec/v1/opcode.rs | 10 +++++++--- crates/core/src/codec/v1/timestamp/decode.rs | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/core/src/codec/imp/std_io.rs b/crates/core/src/codec/imp/std_io.rs index 205eb16..2c10093 100644 --- a/crates/core/src/codec/imp/std_io.rs +++ b/crates/core/src/codec/imp/std_io.rs @@ -1,12 +1,15 @@ use crate::codec::*; -use std::io::{Read, Write}; +use std::{ + io::{Read, Write}, + slice, +}; pub struct Writer(pub W); pub struct Reader(pub R); impl Encoder for Writer { fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { - self.write_all([byte])?; + self.write_all(slice::from_ref(&byte))?; Ok(()) } diff --git a/crates/core/src/codec/proof.rs b/crates/core/src/codec/proof.rs index c01e82d..7716712 100644 --- a/crates/core/src/codec/proof.rs +++ b/crates/core/src/codec/proof.rs @@ -43,7 +43,7 @@ impl, A: Allocator> Encode for VersionedProof { } } -impl fmt::Display for VersionedProof { +impl + fmt::Display, A: Allocator> fmt::Display for VersionedProof { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Version {} Proof {}", T::VERSION, self.proof) } diff --git a/crates/core/src/codec/v1/opcode.rs b/crates/core/src/codec/v1/opcode.rs index bfb8da5..f9772f4 100644 --- a/crates/core/src/codec/v1/opcode.rs +++ b/crates/core/src/codec/v1/opcode.rs @@ -112,12 +112,16 @@ impl OpCode { let input = input.as_ref(); match *self { Self::APPEND => { - let mut out = input.to_vec_in(alloc); - out.extend_from_slice(immediate.as_ref()); + let immediate = immediate.as_ref(); + let mut out = Vec::with_capacity_in(input.len() + immediate.len(), alloc); + out.extend_from_slice(input); + out.extend_from_slice(immediate); out } Self::PREPEND => { - let mut out = input.to_vec_in(alloc); + let immediate = immediate.as_ref(); + let mut out = Vec::with_capacity_in(input.len() + immediate.len(), alloc); + out.extend_from_slice(immediate); out.extend_from_slice(input); out } diff --git a/crates/core/src/codec/v1/timestamp/decode.rs b/crates/core/src/codec/v1/timestamp/decode.rs index 3980f8d..c03e018 100644 --- a/crates/core/src/codec/v1/timestamp/decode.rs +++ b/crates/core/src/codec/v1/timestamp/decode.rs @@ -61,7 +61,7 @@ impl Timestamp { _ => { let data = if op.has_immediate() { const MAX_OP_LENGTH: usize = 4096; - let length = decoder.decode_ranged(1..MAX_OP_LENGTH)?; + let length = decoder.decode_ranged(1..=MAX_OP_LENGTH)?; let mut data = Vec::with_capacity_in(length, alloc.clone()); data.resize(length, 0); decoder.read_exact(&mut data)?; From 396975685418a7501191852bdd8c365ecf2ae69e Mon Sep 17 00:00:00 2001 From: lightsing Date: Tue, 23 Dec 2025 16:01:26 +0800 Subject: [PATCH 11/11] apply review --- crates/core/src/codec/v1/attestation.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/core/src/codec/v1/attestation.rs b/crates/core/src/codec/v1/attestation.rs index 224ab6f..ce86e19 100644 --- a/crates/core/src/codec/v1/attestation.rs +++ b/crates/core/src/codec/v1/attestation.rs @@ -157,11 +157,14 @@ impl<'a> Attestation<'a> for PendingAttestation<'a> { fn from_raw_data(data: &'a [u8]) -> Result { let data = &mut &data[..]; - let length = u32::decode(data)?; // length prefix - if length as usize > Self::MAX_URI_LEN { + let length = u32::decode(data)? as usize; // length prefix + if length > Self::MAX_URI_LEN { return Err(DecodeError::UriTooLong); } - let uri = core::str::from_utf8(data).map_err(|_| DecodeError::InvalidUriChar)?; + if data.len() < length { + return Err(DecodeError::UnexpectedEof); + } + let uri = core::str::from_utf8(&data[..length]).map_err(|_| DecodeError::InvalidUriChar)?; if !Self::validate_uri(uri) { return Err(DecodeError::InvalidUriChar); } @@ -170,7 +173,7 @@ impl<'a> Attestation<'a> for PendingAttestation<'a> { }) } - fn to_raw_data_in(&self, alloc: B) -> Result, EncodeError> { + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError> { if self.uri.len() > Self::MAX_URI_LEN { return Err(EncodeError::UriTooLong); }