4.8 KiB
4.8 KiB
RecursiveEd25519Codec
(no submodules)
Purpose
RecursiveEd25519Codec is a fixed-Ed25519 alternate wire format for signed key chains in @suaveplan/crypto-keys. It trades the algorithm-flexibility of TlvChainCodec for a denser, predictable byte layout suitable for embedded systems, replicated logs, and any context where chains are known to be purely Ed25519. The codec walks bytes linearly leaf-first using the fixed 32-byte public key and 64-byte signature lengths defined by Ed25519, with a single u16 length prefix for each variable-length payload.
Features
- Fixed 32-byte Ed25519 public keys and 64-byte Ed25519 signatures — no per-level algorithm byte.
- Compact 2-byte header carrying
version=1plus the activehasherNamebyte (raw/domain/merkle). - u16 big-endian payload length (up to 65,535 bytes per level).
- Leaf-first emission; the parent chain is appended back-to-back with no inter-level separator.
- Strict validation: bad version byte, unknown hasher byte, truncated buffers, and out-of-bound payload lengths all raise
CHAIN_KEYS.INVALID_FORMAT. - Non-Ed25519 algorithms raise
CHAIN_KEYS.UNSUPPORTED_ALGORITHMwith thealgorithmdiscriminant populated, mirroring thesignWithSlotfailure surface. - After
decode(...),hasherNameis set from the wire byte so a downstreamChainedKeySignercan reconfigure its hasher without an out-of-band channel.
Usage: Basic
import { RecursiveEd25519Codec } from "@suaveplan/crypto-keys/codecs/recursive-ed25519/recursive-ed25519";
const codec = new RecursiveEd25519Codec();
const level = {
algorithm: "ed25519" as const,
signerPublicKey: new Uint8Array(32),
payload: new Uint8Array([1, 2, 3, 4]),
signature: new Uint8Array(64),
};
const bytes = codec.encode([level]);
const decoded = codec.decode(bytes);
// decoded[0].algorithm === "ed25519"
Usage: Advanced
Round-tripping a multi-level chain and surfacing hasher state:
import { RecursiveEd25519Codec } from "@suaveplan/crypto-keys/codecs/recursive-ed25519/recursive-ed25519";
import { ChainKeysError } from "@suaveplan/crypto-keys/error-codes/error-codes";
const writer = new RecursiveEd25519Codec();
writer.hasherName = "merkle";
const wire = writer.encode([leafLevel, midLevel, rootLevel]);
const reader = new RecursiveEd25519Codec();
try {
const levels = reader.decode(wire);
// reader.hasherName is now "merkle" — propagate to the ChainedKeySigner
// before verifying signatures.
handleChain(levels, reader.hasherName);
} catch (err) {
if (err instanceof ChainKeysError && err.code === "CHAIN_KEYS.UNSUPPORTED_ALGORITHM") {
// Routing decision: switch to TlvChainCodec or CborChainCodec.
fallback(err.algorithm);
} else {
throw err;
}
}
API Reference
class RecursiveEd25519Codec implements ChainCodec
readonly name: "recursive-ed25519"— codec identifier surfaced in diagnostics.hasherName: "raw" | "domain" | "merkle"— hasher byte written on encode and overwritten on decode. Default"raw".encode(levels: readonly ChainLevel[]): Uint8Array— emits[version=1, hasher, (pub32, payload_len_u16_be, payload, sig64)+]leaf-first. ThrowsChainKeysErrorwith codeCHAIN_KEYS.UNSUPPORTED_ALGORITHMfor any non-Ed25519 level (withalgorithmdiscriminant set), orCHAIN_KEYS.INVALID_FORMATfor wrong public-key/signature sizes or oversized payloads.decode(bytes: Uint8Array): readonly ChainLevel[]— walks the buffer from offset 2 until end-of-buffer. Every decoded level carriesalgorithm: "ed25519"unconditionally. ThrowsCHAIN_KEYS.INVALID_FORMATon a wrong version byte, unknown hasher byte, truncated read, or an in-streampayload_lenexceeding the remaining bytes.
Implementation Notes
- The encoder pre-computes total length and writes into a single
Uint8Array, avoiding per-level allocations and intermediate concatenations. payload_lenuses u16 big-endian viaDataView.setUint16(offset, len, false)— keeping a network-byte-order convention consistent with TLV.decode(...)callsbytes.slice(...)for each field so the returnedUint8Arrays do not alias the input buffer; consumers may safely mutate or zero the wire bytes after decoding.- The hasher byte is validated as
< HASHER_FROM_BYTE.lengthrather than a hard-coded<= 2so adding a fourth hasher only requires extending the lookup table. - Encoding a non-Ed25519 level raises
UNSUPPORTED_ALGORITHMeven when the supplied key/signature happen to be 32/64 bytes — the algorithm check fires first to keep the error surface predictable. - Decoded levels are returned
readonlyat the type level but the underlyingUint8Arrays are fresh copies; callers can freeze them at a higher layer if mutation must be statically forbidden.