-
-
Notifications
You must be signed in to change notification settings - Fork 1
merkle
Trust-anchored transparency logs. MerkleVerifier is the primary normie surface for verifying signed checkpoints, inclusion proofs, and consistency proofs against a fixed log identity. MerkleLog is the memory-backed producer companion for in-process logs, fixtures, and demo code. Both are safe-by-default: the dangerous parts of merkle composition (raw trees, custom storage, the wire-format codec) sit behind a divider further down this page.
A transparency log is an append-only sequence of leaves whose Merkle root is signed at every step. A verifier with a trusted log identity (origin string, public key, signature suite, hash function) can decide three independent questions from short proofs alone:
- Checkpoint: does this signed envelope come from the log I trust?
- Inclusion: is this leaf at this index actually in the tree that envelope commits to?
- Consistency: is the tree at envelope B a superset (an append-only extension) of the tree at envelope A?
The wire format follows the C2SP family: c2sp.org/tlog-checkpoint for the body, c2sp.org/signed-note for the envelope, c2sp.org/tlog-cosignature for the cosignature lines. The Merkle math is RFC 9162 §2.1.1 (tree hash), §2.1.3 (inclusion), and §2.1.4 (consistency).
leviathan ships two hash specialisations and two cosignature suites. Both produce envelopes consumers of any C2SP-conformant log can verify, including Sigsum logs (Ed25519) and any future ML-DSA-44-cosigning log.
| Layer | Choice | When to pick it |
|---|---|---|
| Hashing |
'sha256' (default) |
C2SP-interop, broadly familiar, default for new logs |
| Hashing | 'blake3' |
Native-BLAKE3 stacks; uses BLAKE3 §2.5 tree-mode parent compress directly |
| Suite |
MlDsa44Suite (default) |
C2SP-recommended PQ default per c2sp.org/tlog-checkpoint §Format |
| Suite | Ed25519Suite |
Sigsum interop, deterministic per RFC 8032 §5.1.6 |
MerkleLog and MerkleVerifier need three module families: sha2 (always, for the key-ID SHA-256), the suite's modules, and the hasher's modules. The exact init({...}) call depends on the combination you pick.
| Hashing | Suite |
init({...}) modules |
|---|---|---|
'sha256' |
Ed25519Suite |
sha2, ed25519
|
'sha256' |
MlDsa44Suite |
sha2, sha3, mldsa
|
'blake3' |
Ed25519Suite |
sha2, blake3, ed25519
|
'blake3' |
MlDsa44Suite |
sha2, sha3, mldsa, blake3
|
The class refuses to construct if any required module has not been initialised; MerkleLogError('module-not-initialized') names the missing one.
import { init, MerkleLog, MerkleVerifier, MlDsa44Suite } from 'leviathan-crypto';
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded';
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded';
import { mldsaWasm } from 'leviathan-crypto/mldsa/embedded';
await init({ sha2: sha2Wasm, sha3: sha3Wasm, mldsa: mldsaWasm });The ./merkle subpath (leviathan-crypto/merkle) exposes the same surface tree-shakeably when you want to import only the merkle module without pulling the root barrel.
Trust-anchored verifier. Construct it once with the identity of a log you trust, then call the three verify methods as input arrives. Methods always return boolean; the class only throws at construction time, on a violated contract.
import {
init, MerkleVerifier, MlDsa44Suite, utf8ToBytes,
} from 'leviathan-crypto';
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded';
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded';
import { mldsaWasm } from 'leviathan-crypto/mldsa/embedded';
await init({ sha2: sha2Wasm, sha3: sha3Wasm, mldsa: mldsaWasm });
// `pubkey` is the trusted log's public key, obtained out-of-band
// (e.g. baked into the client, distributed via a key-management
// surface). The verifier never fetches it.
const verifier = new MerkleVerifier({
origin: 'example.com/log42',
pubkey,
hashing: 'sha256',
suite: MlDsa44Suite,
});
// 1. Verify a signed checkpoint envelope as it arrives.
if (!verifier.verifyCheckpoint(envelopeBytes)) reject('bad checkpoint');
// 2. Verify a leaf's inclusion in the tree that envelope commits to.
const ok = verifier.verifyInclusion({
envelopeBytes,
leafBytes, // raw leaf bytes; verifier re-hashes with `hashing`
leafIndex,
proof, // Uint8Array[] from the log's inclusion-proof endpoint
});
if (!ok) reject('bad inclusion proof');
// 3. Verify that newEnvelope's tree is an append-only extension of oldEnvelope.
const extended = verifier.verifyConsistency({
oldEnvelopeBytes,
newEnvelopeBytes,
proof,
});
if (!extended) reject('log forked or rolled back');| Member | Signature | Description |
|---|---|---|
origin |
string |
The trusted origin captured at construction. Read-only. |
pubkey |
Uint8Array |
A private copy of the trusted pubkey. Read-only. |
hasher |
Hasher |
Resolved Sha256Hasher / Blake3Hasher. Read-only. |
suite |
SignatureSuite |
The trusted cosignature suite. Read-only. |
verifyCheckpoint(envelopeBytes) |
(Uint8Array) => boolean |
Parses the envelope, checks origin, finds the signature line by keyId, decodes the timestamped_signature payload, reconstructs the cosignature signed message per the suite's messageConstruction, and runs suite.verify. |
verifyInclusion(opts) |
({envelopeBytes, leafBytes, leafIndex, proof}) => boolean |
Runs verifyCheckpoint first; on success, hashes leafBytes with the configured Hasher and calls verifyInclusionProof against the body's treeSize and rootHash per RFC 9162 §2.1.3. |
verifyConsistency(opts) |
({oldEnvelopeBytes, newEnvelopeBytes, proof}) => boolean |
Verifies both envelopes, then calls verifyConsistencyProof per RFC 9162 §2.1.4. |
Construction throws MerkleLogError with one of these discriminators:
-
'origin-invalid'iforiginis empty, contains whitespace, or contains+. -
'pubkey-size'ifpubkeyis not aUint8Arrayor its length is notsuite.pkSize. -
'unsupported-hashing'ifhashingis not'sha256'or'blake3'. -
'unsupported-suite'ifsuite.formatEnumhas no entry in the c2sp.org/tlog-cosignature §Format algorithm-byte registry. -
'module-not-initialized'if any required WASM module has not beeninit()d.
Memory-backed producer. Build a log from caller-supplied keys with MerkleLog.create, or let the class generate a fresh keypair with MerkleLog.generate and persist the keys yourself. Hot-path methods (append, head, size, rootHash, inclusionProof, consistencyProof) are synchronous; only create and generate are async.
import { init, MerkleLog, MerkleVerifier, utf8ToBytes } from 'leviathan-crypto';
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded';
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded';
import { mldsaWasm } from 'leviathan-crypto/mldsa/embedded';
await init({ sha2: sha2Wasm, sha3: sha3Wasm, mldsa: mldsaWasm });
// Defaults: hashing = 'sha256', suite = MlDsa44Suite.
const { log, signingKey, pubkey } = await MerkleLog.generate({
origin: 'example.com/log42',
});
// Persist signingKey + pubkey wherever you keep secrets. The library
// does not touch disk or any network.
try {
const { leafIndex, inclusionProof } = log.append(utf8ToBytes('leaf-zero'));
const envelope = log.head(); // signed checkpoint envelope bytes
// ... publish envelope and proofs to your consumers
} finally {
log.dispose(); // wipes the signing-key copy
}import { init, MerkleLog, Ed25519Suite } from 'leviathan-crypto';
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded';
import { ed25519Wasm } from 'leviathan-crypto/ed25519/embedded';
await init({ sha2: sha2Wasm, ed25519: ed25519Wasm });
const { log, signingKey, pubkey } = await MerkleLog.generate({
origin: 'example.com/sigsum-log',
suite: Ed25519Suite,
});| Member | Signature | Description |
|---|---|---|
MerkleLog.create(opts) |
static (MerkleLogCreateOpts) => Promise<MerkleLog> |
Validates the suite against the C2SP registry, instantiates the inner tree + SignedLog, returns the log. |
MerkleLog.generate(opts) |
static (MerkleLogGenerateOpts) => Promise<{log, signingKey, pubkey}> |
Calls suite.keygen() and delegates to create. Returns the keypair so the caller can persist it. |
origin |
string |
The origin captured at construction. |
hasher |
Hasher |
Resolved hasher for the tree. |
suite |
SignatureSuite |
The cosignature suite. |
append(leafBytes) |
(Uint8Array) => {leafIndex, leafHash, inclusionProof} |
Append a leaf; returns its index, leaf hash, and inclusion proof against the post-append tree size. |
head(opts?) |
({timestamp?: number}) => Uint8Array |
Emit a signed-note envelope per c2sp.org/signed-note §Format with the current tree size and root hash. Re-signed on every call. Timestamp defaults to Math.floor(Date.now() / 1000). |
size() |
() => number |
Current leaf count. |
rootHash() |
() => Uint8Array |
Current Merkle root hash. |
inclusionProof(leafIndex, treeSize?) |
(number, number?) => Uint8Array[] |
RFC 9162 §2.1.3 inclusion proof. |
consistencyProof(oldSize, newSize) |
(number, number) => Uint8Array[] |
RFC 9162 §2.1.4 consistency proof. |
dispose() |
() => void |
Wipes the stored signing-key copy. Idempotent; subsequent method calls throw. |
create's signingKey is copied internally; modifying the caller's view of the buffer after construction does not affect the log. generate's returned signingKey and pubkey are independent allocations; the log keeps its own copies.
The suite option on MerkleLog and MerkleVerifier must be a leviathan SignatureSuite whose formatEnum is registered in the c2sp.org/tlog-cosignature §Format algorithm-byte registry. As of v3.0.0 the registry covers two suites:
| Suite | leviathan formatEnum
|
C2SP algorithm byte | Notes |
|---|---|---|---|
Ed25519Suite |
0x01 |
0x04 |
Sigsum interop, deterministic per RFC 8032 §5.1.6 |
MlDsa44Suite |
0x03 |
0x06 |
Default, C2SP-recommended PQ, hedged per FIPS 204 §3.4 |
Any other suite (EcdsaP256Suite, prehash variants, ML-DSA-65/87, SLH-DSA, hybrids) raises MerkleLogError('unsupported-suite') at MerkleLog.create, MerkleLog.generate, or MerkleVerifier constructor time. The registry grows additively: when C2SP registers a new algorithm byte, the table here updates in the same PR that lands the registry extension in src/ts/merkle/signed-note.ts. The single source of truth is the ALGO_REGISTRY const in signed-note.ts; lookupAlgoEntryByFormatEnum and lookupAlgoEntryByByte expose the registry to power users.
The defaults are intentionally not exported as named constants. MerkleLog resolves them internally so call sites stay terse; pinning an explicit value is the way to opt out.
Important
Key custody is the caller's responsibility. MerkleLog does not persist keys, send them over the network, or write them to disk. MerkleLog.generate returns the freshly generated signing key once; if you lose it, the log can no longer sign new checkpoints under its identity. dispose() wipes the log's internal copy but does not touch any copies you held.
Important
The verifier hashes leaves itself. MerkleVerifier.verifyInclusion takes raw leafBytes, not a pre-computed leaf hash, and runs hasher.hashLeaf(leafBytes) internally before calling the proof verifier. This closes the "we trust the proof because we trust the leaf hash the caller passed us" gap. If your transport hands you a leaf hash, you must re-fetch the raw bytes to verify; trusting the hash alone is unsound.
Caution
verifyCheckpoint runs before proof verification. verifyInclusion and verifyConsistency both call verifyCheckpoint on the relevant envelope(s) first, and short-circuit to false on signature failure. Skipping this ordering (verifying a proof against an unsigned root) lets any party fabricate a root and pair it with a structurally valid proof. The verifier enforces the order; never reimplement this dance manually with the free-function verifiers below unless you understand the bound-to-root-via-signature property.
Caution
Suite-selection constraint. MerkleLog and MerkleVerifier only accept C2SP-registered cosignature suites (currently Ed25519 and ML-DSA-44). The leviathan signature catalog contains many more suites (EcdsaP256Suite, prehash variants, SLH-DSA, hybrids), but none of those have a registered c2sp.org/tlog-cosignature §Format algorithm byte yet, so they cannot produce or verify wire-format-conformant cosignature envelopes. Trying to pass one raises MerkleLogError('unsupported-suite') at construction time, before any signing.
Caution
MerkleLog is memory-backed. The only storage backend Phase 7 ships is MemoryStorage, which keeps every leaf hash and every perfect internal node in a Map. Suitable for in-process logs, test fixtures, small embedded uses, and demo code. Real deployments needing file or database storage drop down to the danger-zone surface below: construct SignedLog<S> directly with a custom MerkleStorage implementation.
Caution
head() re-signs on every call. Each invocation produces a fresh signed envelope with the current tree size and root hash, and (by default) the current wall-clock timestamp. For deterministic byte-stable output (tests, KAT vectors), pass { timestamp } explicitly. The c2sp.org/tlog-witness add-checkpoint rule mandates a non-zero timestamp on production cosignatures; the API accepts 0 for test reproducibility but real witnesses reject envelopes that carry it.
Caution
The classes below give you direct access to unwrapped Merkle trees, signature wrapping, the wire-format codec, and the storage extension point. They exist for protocol implementors, custom-storage deployments, and advanced use cases. If you are building general-purpose transparency logging, stop here and use MerkleVerifier and MerkleLog above.
The danger-zone surface inherits the same suite-selection constraint as the normie surface: only suites whose formatEnum maps to a C2SP-registered algorithm byte can drive the cosignature wire format. The single source of truth is the ALGO_REGISTRY const in src/ts/merkle/signed-note.ts; lookupAlgoEntryByFormatEnum and lookupAlgoEntryByByte expose it to power users.
SignedLog<S extends SignatureSuite> ties a MerkleTree (Sha256Tree or Blake3Tree), a registered cosignature SignatureSuite, and an origin string into one object that produces signed checkpoints and verifies received ones.
import {
init, SignedLog, Sha256Tree, MemoryStorage, MlDsa44Suite, utf8ToBytes,
} from 'leviathan-crypto';
const tree = new Sha256Tree(new MemoryStorage());
const log = new SignedLog({
tree,
suite: MlDsa44Suite,
origin: 'example.com/log42',
signingKey,
pubkey,
});
try {
for (const leafBytes of incoming) tree.append(leafBytes);
const envelope = log.signCheckpoint({ timestamp: 1740000000 });
if (!log.verifyCheckpoint(envelope)) throw new Error('verify');
} finally {
log.dispose();
}MerkleStorage is the extension point for file, database, or hybrid backends. Implement the interface and pass an instance to the tree constructor:
import { MerkleStorage } from 'leviathan-crypto';
class FileStorage implements MerkleStorage {
// ... implementation, sync everywhere per the merkle layer's invariant
size(): number { /* ... */ return 0; }
appendLeaf(leafIndex: number, leafHash: Uint8Array): void { /* ... */ }
getLeaf(leafIndex: number): Uint8Array { /* ... */ return new Uint8Array(); }
putNode(level: number, index: number, hash: Uint8Array): void { /* ... */ }
getNode(level: number, index: number): Uint8Array { /* ... */ return new Uint8Array(); }
hasNode(level: number, index: number): boolean { /* ... */ return false; }
}
const tree = new Sha256Tree(new FileStorage());If your backend genuinely needs async IO, wrap it externally: pre-load the leaves and nodes synchronously into memory at the boundaries of your operation, then drive the merkle layer.
| Member | Signature | Description |
|---|---|---|
new SignedLog(opts) |
(SignedLogOpts<S>) => SignedLog<S> |
Validates suite registry, copies signing key and pubkey, derives keyId, checks module init readiness. |
tree, suite, origin, pubkey, wasmModules
|
readonly |
Captured construction inputs. |
signCheckpoint(opts?) |
({timestamp?}) => Uint8Array |
Emit a signed-note envelope per c2sp.org/tlog-cosignature §Format. |
verifyCheckpoint(bytes) |
(Uint8Array) => boolean |
Parses, checks origin and keyId, verifies the cosignature. |
parseCheckpoint(bytes) |
(Uint8Array) => SignedTreeHead |
Structured form: {checkpoint, signatures, timestamp}. |
append, size, rootHash, getInclusionProof, getConsistencyProof
|
tree passthroughs | Match MerkleTree semantics. |
dispose() |
() => void |
Wipes the signing-key copy. Idempotent. |
SignedLog rejects unregistered suites with SigningError('sig-unsupported-suite'); the normie classes wrap this with MerkleLogError('unsupported-suite') so callers can branch on either.
Two MerkleTree specialisations sharing the same algorithmic core. Both produce 32-byte root hashes and consume / emit 32-byte proof entries; the wire format is identical and only differs in how hashLeaf and hashInternal are computed.
| Tree | Domain separation | Source |
|---|---|---|
Sha256Tree |
RFC 9162 §2.1.1 prefix bytes: 0x00 for leaves, 0x01 for internal nodes |
`SHA-256(prefix |
Blake3Tree |
BLAKE3 §2.4 chunk flags (CHUNK_START, CHUNK_END, ROOT) for leaves; BLAKE3 §2.5 parent compress (PARENT flag) for internal nodes | Native BLAKE3 |
Blake3Tree.hashInternal does not stack RFC 6962 prefix bytes on top of BLAKE3. The BLAKE3 spec already provides domain separation via flag bytes; the parent compress is called with modeFlags = 0 (default mode) and isRoot = 0 at every internal node, including the top of the tree. Stacking 0x00 / 0x01 prefixes on top of this would be redundant separation and would also discard compress4 parallelism at the internal-node layer.
| Member | Signature | Description |
|---|---|---|
new Sha256Tree(storage) |
(MerkleStorage) => Sha256Tree |
Constructed empty. append is the only mutator. |
new Blake3Tree(storage) |
(MerkleStorage) => Blake3Tree |
Same surface as Sha256Tree. |
hasher |
Hasher |
Sha256Hasher / Blake3Hasher const. |
size() |
() => number |
Leaf count from the storage layer. |
rootHash() |
() => Uint8Array |
Recompute from stored perfect subtrees. |
append(leafBytes) |
(Uint8Array) => {leafIndex, leafHash} |
Hash with hashLeaf, persist, propagate completed internal nodes up the right edge. |
getInclusionProof(leafIndex, treeSize?) |
(number, number?) => Uint8Array[] |
RFC 9162 §2.1.3. |
getConsistencyProof(oldSize, newSize) |
(number, number) => Uint8Array[] |
RFC 9162 §2.1.4. |
Sha256Hasher and Blake3Hasher are exported as standalone Hasher consts; pass them directly to the free-function verifiers below if you do not need a stateful tree.
verifyInclusionProof and verifyConsistencyProof are the thin-verifier path. They take a Hasher and the proof bytes, decide RFC 9162 §2.1.3 / §2.1.4 in isolation, and return boolean. Use them when you want a checkpoint-less verification (e.g. a witness verifying against a root it already holds), or when you want to integrate the merkle math with a non-leviathan signature stack.
import { Sha256Hasher, verifyInclusionProof } from 'leviathan-crypto';
const ok = verifyInclusionProof({
hasher: Sha256Hasher,
leafHash: hasher.hashLeaf(leafBytes),
leafIndex,
treeSize,
proof,
rootHash,
});| Function | Behaviour |
|---|---|
verifyInclusionProof(input) |
RFC 9162 §2.1.3. Returns false on wrong proof length, wrong sibling-hash size, or a reconstructed root that does not match. Throws RangeError on contract violations (out-of-range leafIndex, treeSize < 1, wrong-sized rootHash). |
verifyConsistencyProof(input) |
RFC 9162 §2.1.4. Returns false on wrong proof length or mismatched reconstruction. Throws RangeError on oldSize > newSize or wrong-sized roots. Empty proof against oldSize == newSize requires identical roots. |
buildInclusionProof(input) |
RFC 9162 §2.1.3 builder. Takes a getNode(level, index) callback so storage backends can drive the same proof builder. |
buildConsistencyProof(input) |
RFC 9162 §2.1.4 builder. Returns [] for oldSize == newSize or oldSize == 0. |
The verifier-and-builder pair is hash-agnostic by design; the Hasher argument fully determines the domain separation. SHA-256 and BLAKE3 trees produce identical proof wire format and consume the same algorithmic core.
Two layered codecs implement the C2SP wire format.
Per c2sp.org/tlog-checkpoint §Note text. Three newline-terminated lines:
<origin>\n
<treeSize-decimal>\n
<base64(rootHash)>\n
origin is non-empty UTF-8, no whitespace, no plus characters. treeSize is ASCII decimal with no leading zeroes. rootHash uses RFC 4648 §4 standard base64 with padding (not the URL-safe variant). Extension lines are rejected: the cosignature wire format does not commit to them.
import { serializeCheckpointBody, parseCheckpointBody } from 'leviathan-crypto';
const body = serializeCheckpointBody({ origin, treeSize, rootHash });
const cp = parseCheckpointBody(body, 32 /* expectedHashLen */);Per c2sp.org/signed-note §Format. Body, blank separator line, then one or more — <name> <base64(keyId||signature)> signature lines.
import { emitSignedNote, parseSignedNote } from 'leviathan-crypto';
const envelope = emitSignedNote(body, [signatureLine]);
const env = parseSignedNote(envelope);
// env.body is the body region (including its trailing newline)
// env.signatures is the parsed signature lines
// env.ignoredCount is the number of signature lines that failed structural validationStructural envelope errors throw RangeError; individual signature lines that fail structural validation are silently discarded into ignoredCount per the signed-note §Signatures rule "unknown signatures MUST be ignored".
Per c2sp.org/tlog-cosignature §Format timestamped_signature struct:
uint64 timestamp_be || signature[N]
N is the suite's raw signature size (64 for Ed25519, 2420 for ML-DSA-44).
import {
emitCosigSignaturePayload, parseCosigSignaturePayload,
} from 'leviathan-crypto';
const payload = emitCosigSignaturePayload(timestamp, sigBytes);
const parsed = parseCosigSignaturePayload(payload, 64 /* sigSize */);Two constructions dispatch on the algorithm byte registry's messageConstruction field:
messageConstruction |
Bytes the suite signs | Used by |
|---|---|---|
'cosig' |
cosignature/v1\ntime <decimal>\n<body> |
Ed25519 (algo byte 0x04) |
'cosigned-message' |
cosigned_message TLS-Presentation struct (label subtree/v1\n\0, length-prefixed cosigner_name + log_origin, BE timestamp / start / end, 32-byte hash) |
ML-DSA-44 (algo byte 0x06) |
buildCosigSignedMessage and buildCosignedMessage are exported for code paths that need to reconstruct the signed message manually. SignedLog, MerkleLog, and MerkleVerifier dispatch internally so you never have to choose.
keyId = SHA-256(utf8(origin) || 0x0A || algoByte || pubkey)[:4]
Per c2sp.org/tlog-cosignature §Format. The key ID is the first 4 bytes of the signature line's base64 payload; the verifier matches it against the keyId derived from the trusted identity to find the right signature line.
import { deriveKeyId, ALGO_BYTE_MLDSA44_COSIG } from 'leviathan-crypto';
const keyId = deriveKeyId('example.com/log42', ALGO_BYTE_MLDSA44_COSIG, pubkey);MerkleStorage is the per-node persistence interface a MerkleTree drives. Two-axis key: (level, index). Level 0 is the leaf row; level >= 1 stores the hash of a perfect aligned subtree covering [index * 2^level, (index + 1) * 2^level). Roots of partial right-edge subtrees are recomputed on demand by the tree.
export interface MerkleStorage {
size(): number;
appendLeaf(leafIndex: number, leafHash: Uint8Array): void;
getLeaf(leafIndex: number): Uint8Array;
putNode(level: number, index: number, hash: Uint8Array): void;
getNode(level: number, index: number): Uint8Array;
hasNode(level: number, index: number): boolean;
}Sync everywhere. The merkle layer is synchronous; callers that need async IO wrap externally.
MemoryStorage is the only backend leviathan ships. It is suitable for tests, witnesses without persistent storage, and the MerkleVerifier short-lived flow. Production logs that need durability implement the interface over a file or database and feed an instance to Sha256Tree / Blake3Tree.
A bare skeleton for a file backend:
import { MerkleStorage } from 'leviathan-crypto';
class JsonlFileStorage implements MerkleStorage {
// Two files: leaves.jsonl (append-only), nodes.jsonl (level/index -> hex).
// size() reads the leaf-count footer; appendLeaf appends to leaves.jsonl
// and updates the count; putNode appends to nodes.jsonl with a (level, index)
// header; getNode / getLeaf / hasNode use an in-memory index map populated
// at startup. Sync IO via Node's fs.readFileSync / writeFileSync /
// appendFileSync, which keeps the surface compatible with the merkle
// layer's sync invariant.
// ...
}
const tree = new Sha256Tree(new JsonlFileStorage());The merkle module surfaces three error classes plus standard RangeError / TypeError for caller-side contract violations.
| Class | Discriminator | Trigger |
|---|---|---|
MerkleLogError |
'origin-invalid' |
MerkleLog / MerkleVerifier origin empty, has whitespace, or contains +
|
MerkleLogError |
'pubkey-size' |
pubkey not a Uint8Array or length != suite.pkSize
|
MerkleLogError |
'unsupported-hashing' |
hashing not 'sha256' or 'blake3'
|
MerkleLogError |
'unsupported-suite' |
suite.formatEnum not in the C2SP cosignature algorithm-byte registry |
MerkleLogError |
'module-not-initialized' |
required WASM module not init()d |
SigningError |
'sig-unsupported-suite' |
SignedLog with a suite not in the registry (lower-level path) |
SigningError |
'sig-malformed-input' |
SignedLog.signCheckpoint got a signature whose size disagrees with the registry entry |
MerkleCodecError |
'timestamp-out-of-range' |
timestamp / start / end not a non-negative safe integer (emit path) |
MerkleCodecError |
'timestamp-exceeds-safe-integer' |
wire u64 timestamp > Number.MAX_SAFE_INTEGER (parse path) |
MerkleCodecError |
'cosig-payload-length-mismatch' |
payload bytes != expected 8 + sigSize
|
MerkleCodecError |
'cosigner-name-length' |
UTF-8 cosigner_name empty or > 255 bytes |
MerkleCodecError |
'log-origin-length' |
UTF-8 log_origin empty or > 255 bytes |
MerkleCodecError |
'cosigned-message-state' |
start != 0 with timestamp != 0 (spec MUST) |
RangeError / TypeError
|
n/a |
parseSignedNote / parseCheckpointBody envelope structural failures; storage layer out-of-order index; proof-builder out-of-range leafIndex
|
MerkleVerifier's verifyCheckpoint / verifyInclusion / verifyConsistency return boolean on every input; they only throw the construction-time MerkleLogErrors above. SignedLog.verifyCheckpoint matches.
| Document | Description |
|---|---|
| signaturesuite.md | Signature suite catalog; Ed25519Suite and MlDsa44Suite shapes |
| sha2.md | SHA-256 primitive details used by Sha256Tree and deriveKeyId
|
| blake3.md | BLAKE3 primitive details used by Blake3Tree
|
| mldsa.md | ML-DSA-44 primitive details used by MlDsa44Suite
|
| ed25519.md | Ed25519 primitive details used by Ed25519Suite
|
| c2sp.org/tlog-checkpoint | Canonical checkpoint body format |
| c2sp.org/signed-note | Signed-note envelope format |
| c2sp.org/tlog-cosignature | Cosignature payload format and algorithm-byte registry |
| c2sp.org/tlog-witness | Witness HTTP protocol (out of leviathan scope as of v3.0.0) |
| RFC 9162 | Certificate Transparency Version 2.0; §2.1.1, §2.1.3, §2.1.4 Merkle math |
| exports.md | Full export catalog |
- Sign Tools
-
SignatureSuite
- format-byte catalog, hybrid composite encodings, custom suite contract
- Serpent-256 TypeScript | WASM
-
Serpent,SerpentCtr,SerpentCbc,SerpentGenerator
-
- ChaCha20 TypeScript | WASM
-
ChaCha20,Poly1305,ChaCha20Poly1305,XChaCha20Poly1305,ChaCha20Generator
-
- AES TypeScript | WASM
-
AES,AESCbc,AESCtr,AESGCM,AESGCMSIV,AESGenerator
-
- ML-DSA TypeScript | WASM
- pure (FIPS 204):
MlDsa44,MlDsa65,MlDsa87 - pure-mode suites:
MlDsa44Suite,MlDsa65Suite,MlDsa87Suite - prehash suites:
MlDsa44PreHashSuite,MlDsa65PreHashSuite,MlDsa87PreHashSuite
- pure (FIPS 204):
- SLH-DSA TypeScript | WASM
- pure (FIPS 205):
SlhDsa128f,SlhDsa192f,SlhDsa256f - pure-mode suites:
SlhDsa128fSuite,SlhDsa192fSuite,SlhDsa256fSuite - prehash suites:
SlhDsa128fPreHashSuite,SlhDsa192fPreHashSuite,SlhDsa256fPreHashSuite
- pure (FIPS 205):
- Ed25519 TypeScript | WASM
-
Ed25519(pure + Ed25519ph),Ed25519Suite,Ed25519PreHashSuite
-
- ECDSA-P256 TypeScript | WASM
-
EcdsaP256(hedged + RFC 6979),EcdsaP256Suite - DER codec:
ecdsaSignatureToDer,ecdsaSignatureFromDer,encodeEcPrivateKey,decodeEcPrivateKey,pointDecompress
-
- Hybrid composites PQ-only | Classical+PQ
- PQ-only:
MlDsa44SlhDsa128fSuite,MlDsa65SlhDsa192fSuite,MlDsa87SlhDsa256fSuite - Classical+PQ:
MlDsa44Ed25519Suite,MlDsa65Ed25519Suite,MlDsa44EcdsaP256Suite,MlDsa65EcdsaP256Suite
- PQ-only:
- X25519 TypeScript | WASM
-
X25519,KeyAgreementError(RFC 7748)
-
- ML-KEM TypeScript | WASM
-
MlKem512,MlKem768,MlKem1024
-
-
Ratchet (SPQR)
-
KDFChain,ratchetInit,kemRatchetEncap,kemRatchetDecap,RatchetKeypair,SkippedKeyStore
-
- Hashing overview
- SHA-2 TypeScript | WASM
-
SHA256,SHA384,SHA512,SHA224,SHA512_224,SHA512_256 -
HMAC_SHA256,HMAC_SHA384,HMAC_SHA512,HKDF_SHA256,HKDF_SHA512
-
- SHA-3 TypeScript | WASM
-
SHA3_224,SHA3_256,SHA3_384,SHA3_512,SHAKE128,SHAKE256
-
- BLAKE3 TypeScript | WASM
-
BLAKE3,BLAKE3Stream,BLAKE3KeyedHash,BLAKE3KeyedHashStream -
BLAKE3DeriveKey,BLAKE3DeriveKeyStream,BLAKE3OutputReader,BLAKE3Hash
-
-
KMAC
-
CSHAKE128,CSHAKE256,KMAC128,KMAC256,KMACXOF128,KMACXOF256
-
-
Merkle
-
MerkleVerifier,MerkleLog -
SignedLog,Sha256Tree,Blake3Tree,MemoryStorage
-
-
Fortuna CSPRNG
-
Fortuna,SerpentGenerator,ChaCha20Generator,AESGenerator,SHA256Hash,SHA3_256Hash,BLAKE3Hash
-
- Utils TypeScript | WASM
-
constantTimeEqual,randomBytes,wipe, encoding helpers
-
-
TypeScript interfaces
-
Hash,KeyedHash,Blockcipher,Streamcipher,AEAD,Generator,HashFn
-