Skip to content

Extract Merkle math from tlog_tiles into a new tlog_core crate#231

Open
lukevalenta wants to merge 1 commit intomainfrom
lvalenta/tlog-crate-split
Open

Extract Merkle math from tlog_tiles into a new tlog_core crate#231
lukevalenta wants to merge 1 commit intomainfrom
lvalenta/tlog-crate-split

Conversation

@lukevalenta
Copy link
Copy Markdown
Contributor

@lukevalenta lukevalenta commented May 1, 2026

First slice of #230. Extract the RFC 6962 Merkle math from tlog_tiles into a new tlog_core crate so tlog_tiles is closer to the c2sp.org/tlog-tiles HTTP wire format only.

Closes neither slice 2 (checkpoint.rstlog_checkpoint) nor slice 3 (entries.rsgeneric_log_worker); those land in follow-ups.

What moves

Symbol Old home New home
Hash, HASH_SIZE, EMPTY_HASH, Proof, HashReader, Subtree tlog_tiles::tlog tlog_core
LeafIndex tlog_tiles::entries tlog_core
record_hash, node_hash, stored_hash_* tlog_tiles::tlog tlog_core
tree_hash + _indexes, subtree_hash + _indexes tlog_tiles::tlog tlog_core
inclusion_proof + _indexes, subtree_inclusion_proof + _indexes, evaluate_subtree_inclusion_proof tlog_tiles::tlog tlog_core
consistency_proof + _indexes, subtree_consistency_proof + _indexes tlog_tiles::tlog tlog_core
verify_inclusion_proof, verify_subtree_inclusion_proof, verify_consistency_proof, verify_subtree_consistency_proof tlog_tiles::tlog tlog_core
TlogError (math-only variants) tlog_tiles::tlog tlog_core

What stays in tlog_tiles for now

  • tile.rs — the c2sp.org/tlog-tiles wire format itself.
  • checkpoint.rs — c2sp.org/tlog-checkpoint (slice 2 will move it).
  • entries.rsLogEntry/PendingLogEntry traits + UnixTimestamp/LookupKey aliases (slice 3 will move worker abstractions).

Cleanups in tlog_tiles

  • New CheckpointError type for open_checkpoint and TreeWithTimestamp::sign. Variants: MissingVerifierSignature, InvalidTimestamp, OriginMismatch, Note(NoteError), MalformedCheckpoint, Tlog(TlogError). These were previously variants on tlog_tiles::TlogError even though they're not math errors.
  • Ed25519CheckpointSigner::new is now infallible (-> Self instead of -> Result<Self, TlogError>); it never actually errored.
  • TlogTilesPendingLogEntry::ParseError is std::io::Error directly (was TlogError); the io error was the only failure path.

Crate-name choice

tlog is taken on crates.io by an unmaintained logging crate. tlog_core follows the local workspace convention (snake_case) and conveys the role accurately ("core primitives below the spec crates"). Other candidates considered: merkle_tlog, cf_tlog, tlog_proofs.

Migration

Every internal consumer (tlog_witness, tlog_cosignature, static_ct_api, bootstrap_mtc_api, bootstrap_mtc_worker, ct_worker, generic_log_worker, witness_worker, tlog_tiles_wasm, integration_tests) imports moved symbols from tlog_core directly. tlog_witness no longer depends on tlog_tiles at all.

The fuzz crate is unchanged — it already used deep-path imports (tlog_tiles::tile::Tile, tlog_tiles::checkpoint::CheckpointText).

Test relocations

  • test_tree integration test (math + tile encoding) moved from tlog_tiles/src/tlog.rs to tlog_tiles/src/tile.rs together with the TestTilesStorage helper, since it depends on Tile/TileReader/TileHashReader.
  • Pure-math unit tests (test_split_stored_hash_index, test_new_subtree, test_evaluate_subtree_inclusion_proof_rejects_out_of_range, test_verify_inclusion_proof_rejects_zero_tree_size, test_empty_tree, test_subtrees_split_interval) stayed in tlog_core.

Stats

42 files changed, +641 / −359. The bulk:

  • New tlog_core crate scaffolding (Cargo.toml, LICENSE, README).
  • The moved tlog.rstlog_core/src/lib.rs (file rename, ~80% similarity preserved per git mv).
  • Moved test_tree test (~286 lines now in tlog_tiles/src/tile.rs).
  • The LeafIndex relocation + the CheckpointError refactor.
  • Mechanical import-path updates across 10 consumer crates.

Pre-push checks

All four pass:

  • cargo clippy --workspace --all-targets -- -Dwarnings -Dclippy::pedantic
  • cargo test (29 test result blocks ok, including all 6 pure-math tests in tlog_core and the relocated test_tree)
  • cargo fmt --all --check
  • cargo machete

WASM build also clean (cargo check -p witness_worker --target wasm32-unknown-unknown).

Stacking

Independent of the in-flight witness PRs (#228, #229), based directly on main. Those branches will need rebasing onto this one's eventual merge to pick up the import path changes.

Refs #230.

@lukevalenta lukevalenta self-assigned this May 1, 2026
@lukevalenta
Copy link
Copy Markdown
Contributor Author

/bonk review

Slice 1 of #230: the c2sp.org/tlog-tiles spec covers an HTTP wire
format for tile-encoded transparency logs. Until now `tlog_tiles`
also carried the underlying RFC 6962 Merkle math (`Hash`,
`HashReader`, the proof builders/verifiers, the `Subtree` type +
the draft-ietf-plants-merkle-tree-certs subtree variants). That math
isn't part of `tlog-tiles` itself — it's the algorithm-only core
that every transparency-log spec in this workspace builds on.

This commit moves that nucleus into a new `tlog_core` crate and
migrates every internal consumer to import from it directly.

New crate `crates/tlog_core`:

- Carries `Hash`, `HASH_SIZE`, `EMPTY_HASH`, `HashReader`, `Proof`,
  `Subtree`, `LeafIndex`, `record_hash`, `node_hash`,
  `stored_hash_index` + variants, `tree_hash` + variants,
  `subtree_hash` + variants, `inclusion_proof` + verifiers,
  `consistency_proof` + verifiers, the subtree variants of all
  proofs, `evaluate_subtree_inclusion_proof`, and `TlogError` (now
  scoped to math errors only).
- Dependencies are `base64`, `serde`, `sha2`, `thiserror` only —
  no `ed25519-dalek`, `signed_note`, `length_prefixed`, `url`,
  `rand` dragged along.

Cleanups in `tlog_tiles`:

- New `CheckpointError` type local to `checkpoint.rs` covers
  `open_checkpoint` and `TreeWithTimestamp::sign` failure modes
  (`MissingVerifierSignature`, `InvalidTimestamp`, `OriginMismatch`,
  `Note(NoteError)`, `MalformedCheckpoint`, `Tlog(TlogError)`).
  Previously these were variants on `tlog_tiles::TlogError` even
  though they had nothing to do with the math layer.
- `Ed25519CheckpointSigner::new` is now infallible (`-> Self` instead
  of `-> Result<Self, TlogError>`); it never actually errored.
- `TlogTilesPendingLogEntry::ParseError` is now `std::io::Error`
  directly — it parses big-endian uint16 length-prefixed blobs and
  the IO error was the only failure path.

The `test_tree` integration test (which exercises the math against
tile-encoded storage) moved to `tlog_tiles/src/tile.rs` together with
the `TestTilesStorage` helper, since it depends on `Tile`,
`TileReader`, `TileHashReader`. Pure-math tests stayed in
`tlog_core`.

Migration: `tlog_witness`, `tlog_cosignature`, `static_ct_api`,
`bootstrap_mtc_api`, `bootstrap_mtc_worker`, `ct_worker`,
`generic_log_worker`, `witness_worker`, `tlog_tiles_wasm`, and
`integration_tests` all import moved symbols from `tlog_core`
directly. `tlog_witness` no longer depends on `tlog_tiles` at all.

The `fuzz` crate is unchanged: it already used deep-path imports
(`tlog_tiles::tile::Tile`, `tlog_tiles::checkpoint::CheckpointText`)
that are still valid.

Slices 2 (move `checkpoint.rs` to a new `tlog_checkpoint` crate)
and 3 (move `entries.rs` into `generic_log_worker`) are deferred
to follow-up PRs per the issue's staging plan.
@lukevalenta lukevalenta force-pushed the lvalenta/tlog-crate-split branch from 099a81b to ae9e983 Compare May 1, 2026 18:52
@lukevalenta
Copy link
Copy Markdown
Contributor Author

/bonk review

Copy link
Copy Markdown

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed locally on lvalenta/tlog-crate-split (HEAD ae9e983).

Pre-push checks

All four pass cleanly on this branch:

  • cargo clippy --workspace --all-targets -- -Dwarnings -Dclippy::pedantic — clean
  • cargo test — 29 result blocks, all ok; the 6 pure-math tests are present in tlog_core::tests and the relocated test_tree runs in tlog_tiles::tile::tests
  • cargo fmt --all --check — clean
  • cargo machete — clean

What I verified

The split lines up with the PR description's table. Spot-checked importers and the new crate boundary holds:

  • tlog_core/Cargo.toml only depends on base64, serde, sha2, thiserror — no signed_note, no length_prefixed, no worker. That matches the "transport-agnostic, no HTTP, no tile encoding, no checkpoint signing" docstring on the crate root and is exactly the boundary AGENTS.md calls out for spec-level crates.
  • tlog_witness/Cargo.toml previously listed tlog_tiles.workspace = true; now it's tlog_core.workspace = true and tlog_witness/src/add_checkpoint.rs imports tlog_core::{Hash, HASH_SIZE} directly. The crate-level rustdoc links to tlog_core::verify_consistency_proof for the consistency check the witness intentionally doesn't perform itself — and that link is now valid against the actual location of the function.
  • The math-only TlogError variants (InvalidProof, MalformedHash, InvalidTile, BadMath, RecordedTilesOnly, InconsistentTile, IndexesNotInTree, IndexesOutOfOrder, ConditionNotMet, InvalidBase64) stay on tlog_core::TlogError. The non-math variants (MissingVerifierSignature, InvalidTimestamp, OriginMismatch, Note, MalformedCheckpoint) move to a new tlog_tiles::CheckpointError with #[from] TlogError for the math-error case. This is a clean separation and the #[from] chain means ? continues to work for the TreeWithTimestamp::sign / from_hash_reader call sites.
  • Ed25519CheckpointSigner::new going from Result<Self, TlogError> to Self: the old body never returned Err, so this is a purely API-shape cleanup. Confirmed against git show main:crates/tlog_tiles/src/checkpoint.rs.
  • TlogTilesPendingLogEntry::ParseError going from TlogError to std::io::Error: previously the only failure path was read_length_prefixed returning io error, wrapped through the now-removed TlogError::IO variant. Direct std::io::Error is correct and removes a layer.
  • The tree_size == 0 behavioral change for verify_inclusion_proof (now ConditionNotMet rather than InvalidProof) is documented in-place at crates/tlog_core/src/lib.rs:493-498 and pinned by test_verify_inclusion_proof_rejects_zero_tree_size. Grepped all in-tree callers — none match on the variant, and the only tree_size: 0 site is the test itself, so this is a safe strict-subset change.
  • The test_tree integration test correctly belongs in tlog_tiles/src/tile.rs because it exercises Tile, TileReader, and TileHashReader against the math layer; the standalone TestHashStorage newtype duplication in tlog_core/src/lib.rs::tests and tlog_tiles/src/tile.rs::tests is unavoidable given orphan-rule constraints and is acceptable for test-only code.
  • The fuzz crate's deep-path imports (tlog_tiles::tile::Tile, tlog_tiles::checkpoint::CheckpointText) are unaffected — confirmed fuzz was not touched and still builds clean under clippy.

Documentation / minor

  • crates/tlog_core/README.md line 14 references [\tlog_checkpoint`](../tlog_tiles)— the link target is intentional (it lives intlog_tilestoday; the parenthetical "Currently still insidetlog_tiles`" makes that explicit). Worth re-checking when slice 2 lands so the link target updates.
  • The tlog_core rustdoc mentions "MTC-related crates for the IETF Merkle Tree Certificates work" which is forward-looking — bootstrap_mtc_api is the current consumer; fine as-is.

Stacking note

The PR description's claim that #228 / #229 are based on main and not on this branch matches what I'd expect — those are the witness PRs and don't depend on the import-path changes here. They will need a rebase after this lands so they pick up tlog_core::Hash (etc.) instead of tlog_tiles::Hash.

LGTM. The slice is mechanical for ~80% of the diff, the boundary cleanups (CheckpointError, Ed25519CheckpointSigner::new, TlogTilesPendingLogEntry::ParseError) are individually well-scoped, and the future-slice plan (#230) is concretely tracked in the README.

@ask-bonk
Copy link
Copy Markdown

ask-bonk Bot commented May 1, 2026

Review posted on PR #231.

Summary of findings:

  • All four pre-push checks (clippy --pedantic, test, fmt, machete) pass cleanly on the branch.
  • The new tlog_core crate has the right boundary: only base64, serde, sha2, thiserror deps — no signed_note, no Workers concerns, matching the spec-crate boundary rule in AGENTS.md.
  • tlog_witness correctly drops its tlog_tiles dep and depends on tlog_core only.
  • The error-type split is clean: math errors stay on tlog_core::TlogError, signed-note/timestamp/origin errors move to a new tlog_tiles::CheckpointError with #[from] TlogError so ? keeps working.
  • The two API cleanups (Ed25519CheckpointSigner::new → infallible; TlogTilesPendingLogEntry::ParseErrorstd::io::Error) are valid simplifications I confirmed against main.
  • The tree_size == 0 behavioral change in verify_inclusion_proof (now ConditionNotMet instead of InvalidProof) is documented in-place and pinned by a test; no in-tree callers match on the variant.
  • One minor note: the tlog_checkpoint link in the tlog_core README points at tlog_tiles intentionally (with a parenthetical) and will need updating when slice 2 lands.

Verdict: LGTM.

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant