A verifiable, append-only transparency log — a tamper-evident Merkle log with inclusion and consistency proofs that anyone can verify without trusting the server.
Certificate-Transparency-style verifiable log for the JVM: Merkle tree proofs (RFC 9162 / RFC 6962), signed checkpoints, N-of-M witness co-signing, RFC 3161 qualified timestamps, and offline-verifiable proof bundles — in pure Scala 3.
New to transparency logs? Start with the plain-language overview.
Most "audit logs" are searchable, not provable. A transparency log gives you a cryptographic guarantee that the log is append-only — entries can be added but history can never be silently rewritten — and lets independent parties verify that guarantee for themselves.
The design follows the model proven by Certificate Transparency (RFC 9162, which obsoletes RFC 6962), Sigstore's Rekor, and the Go checksum database:
- Inclusion proofs — prove a specific entry is in the log.
- Consistency proofs — prove the log at size N₁ is a prefix of the log at size N₂ (nothing was reordered or removed).
- Signed checkpoints — the log periodically signs
(tree_size, root_hash). - Witnessing — independent witnesses co-sign checkpoints to detect a log that shows different histories to different clients (split-view / equivocation).
- Offline proof bundles — a single self-contained document (entry, inclusion proof, signed+cosigned checkpoint, optional RFC 3161 timestamp) that verifies with no network.
Correctness is the product. merklon ships an independent verifier (library + CLI) that checks every proof against the math, and a test suite pinned to the canonical RFC 6962 reference vectors (unchanged under RFC 9162). If the verifier disagrees with the server, the server is wrong.
v0.1.0 — the full Layer-1 log is complete: Merkle core, Postgres persistence, Ed25519-signed checkpoints, HTTP serving, N-of-M witnessing (split-view detection), RFC 3161-sealed offline proof bundles, and a standalone independent verifier.
merklon is on Maven Central and directly usable from plain Java — the
merklon-java facade exposes the core with java.util types only (no Scala type in
any public signature; the Scala runtime rides along as two ordinary transitive jars).
Requires JDK 17+.
Maven:
<dependency>
<groupId>eu.trustbeat</groupId>
<artifactId>merklon-java_3</artifactId>
<version>0.1.0</version>
</dependency>Gradle:
implementation 'eu.trustbeat:merklon-java_3:0.1.0'Build a Merkle tree over your entries, prove one entry is in it, then append and prove the grown log still contains the old one unchanged:
import merklon.javadsl.Merkle;
import java.util.ArrayList;
import java.util.List;
List<byte[]> entries = new ArrayList<>(List.of(
"user=42 action=delete".getBytes(),
"user=7 action=login".getBytes(),
"user=42 action=export".getBytes()));
byte[] root = Merkle.root(entries);
System.out.println("root: " + Merkle.toHex(root));
// Inclusion proof: entry 1 really is in the tree with this root.
List<byte[]> incl = Merkle.inclusionProof(1, entries);
boolean present = Merkle.verifyInclusion(
1, entries.size(), Merkle.leafHash(entries.get(1)), incl, root);
// Consistency proof: after appending, the old log is a prefix of the new one —
// nothing was rewritten, reordered, or removed.
int oldSize = entries.size();
entries.add("user=9 action=login".getBytes());
byte[] newRoot = Merkle.root(entries);
List<byte[]> cons = Merkle.consistencyProof(oldSize, entries);
boolean appendOnly = Merkle.verifyConsistency(
oldSize, entries.size(), root, newRoot, cons);
System.out.println("included: " + present + ", append-only: " + appendOnly);merklon.javadsl.Checkpoints parses and verifies the log's signed checkpoint notes
the same way.
Full walkthrough: Getting started from Java — embed the core, verify a live log's signed checkpoints, run the whole stack locally, and check the published jars against the RFC test vectors yourself.
libraryDependencies += "eu.trustbeat" %% "merklon-core" % "0.1.0"
libraryDependencies += "eu.trustbeat" %% "merklon-verifier" % "0.1.0" // independent verifierimport merklon.MerkleTree
val entries = List("a", "b", "c").map(_.getBytes("UTF-8"))
val root = MerkleTree.root(entries)
println(MerkleTree.toHex(root))Run the log server and append an entry:
sbt "server/runMain merklon.server.Main" # ZIO HTTP log server (see modules/server for env vars)
curl -s -XPOST localhost:8080/entries --data-binary 'hello' # → {"leaf_index":0,"tree_size":1}
curl -s localhost:8080/checkpoint # the signed (size, root) noteVerify independently — build the standalone verifier, then check a proof without trusting the server (the CLI shares none of the server's state):
sbt verifier/assembly # → modules/verifier/target/scala-3.3.4/merklon-verify.jar
java -jar merklon-verify.jar --pubkey <LOG_PUBKEY_HEX> --url http://localhost:8080 \
inclusion 68656c6c6f # hex("hello") — index is looked up by leaf hashmerklon-verify also checks checkpoint signatures, consistency proofs, witness
cosignature policies (--witness NAME=HEX --witness-threshold N), fully offline
proof bundles (bundle FILE [--tsa-cert PEM]), and standard
c2sp.org/tlog-proof documents
(tlog-proof FILE DATA_HEX). Run it with no arguments for usage.
sbt test # runs the full suite, including the RFC 6962 vector checksAll Layer-1 phases are complete as of v0.1.0:
- Phase 0 — Merkle core (done): leaf/node hashing, Merkle Tree Hash, inclusion + consistency proofs, RFC 6962 test vectors.
- Phase 1 — Persistence + checkpoints (done): Postgres storage backend, Ed25519-signed checkpoints, durable log key, sequencer with timed batching.
- Phase 2 — Serving + verifier (done): ZIO HTTP API + standalone CLI verifier.
- Phase 3 — Witnessing (done): c2sp tlog-witness service, N-of-M co-signing (cosignature/v1), split-view detection.
- Phase 4 — Pluggable attestation (done): RFC 3161 qualified timestamps and self-contained, offline-verifiable proof bundles.
Next — post-quantum signatures: ML-DSA-44 cosignatures (the c2sp 0x06 type), planned
for when the JDK 25 LTS becomes the project baseline — the JDK ships ML-DSA natively, so the
core stays dependency-free. The SHA-256 Merkle proofs themselves are already post-quantum
safe; see the post-quantum posture section in DESIGN.md.
Hash-agnostic, dependency-light core with clean extension points
(CheckpointAttestor, LeafCodec, StorageBackend, AppendAuthorizer) so the log
can be embedded in different contexts without forking.
- Overview — how the whole thing works, in plain language.
- Design — architecture, actors, extension points, PQ posture.
- Spec — wire formats: checkpoint note, proofs, bundles, HTTP API.
merklon is built and maintained by Trustbeat, makers of European timestamping and log-anchoring infrastructure. Proof bundles already carry RFC 3161 timestamps — if you need legally valid, eIDAS-qualified timestamps on your checkpoints and evidence, see trustbeat.eu.
Found a vulnerability? Please follow SECURITY.md — do not open a public issue for security reports.
See CONTRIBUTING.md. Contributions require a
Developer Certificate of Origin sign-off
(git commit -s).
Apache License 2.0. Copyright Trustbeat s.r.o.