MetamorphicLog.Anchor (metamorphic_log v0.1.5)

Copy Markdown View Source

Backend-agnostic anchoring / attestation (Slice 8).

A transparency log's anti-equivocation guarantee is only as strong as a relying party's ability to detect a split view — an operator showing one checkpoint to Alice and a different, inconsistent one to Bob. Independent witnesses (MetamorphicLog.Note) are one defence; anchoring is the other: periodically committing a checkpoint's signed tree head to an external, hard-to-equivocate medium (a blockchain transaction, an RFC 3161 notary receipt, object-lock / WORM storage, or another transparency log) so the operator cannot later present a tree that disagrees with what was anchored without that contradiction being publicly visible.

This module surfaces the engine's contribution to anchoring — the format and the verification — and is deliberately backend-agnostic and I/O-free. There is no network client, chain RPC, or notary integration here, and no anchor cadence / fee / confirmation-depth policy: those belong to the operator (the paid mosskeys app). What lives here is:

  • record_canonical_bytes/6 / parse_record/1 — the canonical, byte-locked attestation record binding a checkpoint head (origin, size, root_hash) to an opaque locator and an agnostic medium tag.
  • anchor_commitment/1 — the fixed-size, medium-independent commitment an operator publishes to (and re-fetches from) the medium.
  • verify_anchored/4 — the third-party audit that an attestation binds a checkpoint and that successive anchored heads are append-only consistent, trusting neither the operator nor the medium.
  • verify_commitment/2 — the medium-side counterpart: check bytes fetched from the medium equal the recomputed commitment.

I/O stays on the BEAM side

The Rust core's CommitmentSink trait (and its logic-only *_via bridges) is intentionally not wrapped: a sink with an associated error type and a real backend (chain/notary/object store) is idiomatically BEAM code. Your operator publishes/fetches the commitment bytes itself, then uses anchor_commitment/1

Honest framing (no zero-knowledge)

This is plain anchoring: publish a checkpoint-head commitment and prove consistency between successive anchored heads. It involves zero zero-knowledge machinery; the optional ZK-anchoring enhancement is a separate effort and is not coupled to this format.

Binary values are base64-encoded (standard padded alphabet), byte-identical to the native Rust core and the browser WASM SDK.

Summary

Types

A parsed anchor record

Functions

The fixed-size commitment over the record's checkpoint head — the value an operator publishes to (and re-fetches from) the external medium.

Parse a canonical anchor record into a anchor_record/0 map. Validates the layout, format version, algorithm tag, medium grammar, and non-empty origin/locator. Returns {:ok, record} or {:error, reason}.

Build the canonical bytes of an anchor attestation record from an explicit checkpoint head.

The RFC 6962 Merkle leaf hash of the record's canonical bytes, so an operator may also log its attestations as Layer-0 leaves. Returns {:ok, hash_b64} or {:error, reason}. Dirty CPU.

Medium-side check: the bytes fetched_commitment_b64 retrieved from the external medium equal the commitment recomputed from record_b64's checkpoint head.

Types

anchor_record()

@type anchor_record() :: %{
  origin: String.t(),
  size: non_neg_integer(),
  root: String.t(),
  commitment_alg: String.t(),
  medium: String.t(),
  locator: String.t()
}

A parsed anchor record:

  • :origin — the bound checkpoint origin (log identity)
  • :size — the bound checkpoint tree size
  • :root — base64 RFC 6962 root hash at size (32 bytes)
  • :commitment_alg — the safe-menu commitment algorithm ("sha3_512")
  • :medium — the medium identifier (e.g. "ethereum/mainnet")
  • :locator — base64 opaque external-commitment locator

Functions

anchor_commitment(record_b64)

@spec anchor_commitment(record_b64 :: String.t()) ::
  {:ok, String.t()} | {:error, String.t()}

The fixed-size commitment over the record's checkpoint head — the value an operator publishes to (and re-fetches from) the external medium.

Medium- and locator-independent: the same head yields the same commitment regardless of where it is anchored. Returns {:ok, commitment_b64} or {:error, reason}. Runs on a dirty CPU scheduler.

parse_record(record_b64)

@spec parse_record(record_b64 :: String.t()) ::
  {:ok, anchor_record()} | {:error, String.t()}

Parse a canonical anchor record into a anchor_record/0 map. Validates the layout, format version, algorithm tag, medium grammar, and non-empty origin/locator. Returns {:ok, record} or {:error, reason}.

record_canonical_bytes(origin, size, root_b64, medium, locator_b64, commitment_alg \\ "sha3_512")

@spec record_canonical_bytes(
  origin :: String.t(),
  size :: non_neg_integer(),
  root_b64 :: String.t(),
  medium :: String.t(),
  locator_b64 :: String.t(),
  commitment_alg :: String.t()
) :: {:ok, String.t()} | {:error, String.t()}

Build the canonical bytes of an anchor attestation record from an explicit checkpoint head.

medium is a printable-ASCII identifier (no whitespace/control bytes, / allowed for hierarchy, e.g. "ethereum/mainnet"); locator_b64 is the opaque external-commitment handle (tx id, block height, receipt, object key); commitment_alg is a safe-menu tag ("sha3_512", the v0.1 default and only entry). Returns {:ok, record_b64} or {:error, reason}.

rfc6962_leaf_hash(record_b64)

@spec rfc6962_leaf_hash(record_b64 :: String.t()) ::
  {:ok, String.t()} | {:error, String.t()}

The RFC 6962 Merkle leaf hash of the record's canonical bytes, so an operator may also log its attestations as Layer-0 leaves. Returns {:ok, hash_b64} or {:error, reason}. Dirty CPU.

verify_anchored(note_text, vkeys, record_b64, opts \\ [])

@spec verify_anchored(
  note_text :: String.t(),
  vkeys :: [String.t()],
  record_b64 :: String.t(),
  opts :: keyword()
) :: :ok | {:error, String.t()}

Verify an anchored checkpoint.

Checks that record_b64 binds the checkpoint carried by note_text (verified against the trusted vkeys), and — when a previous anchored checkpoint is supplied — that the newer checkpoint is an append-only extension of it.

This is the log-side audit of "the operator never equivocated between anchored heads": it trusts neither the operator nor the medium. Pair it with verify_commitment/2 (the medium-side check) for the full guarantee. Runs on a dirty CPU scheduler.

Options

  • :prev_note — a previously-anchored checkpoint signed note. When given, consistency from it to note_text is verified.
  • :consistency_proof — a list of base64 RFC 9162 consistency-proof hashes from :prev_note to note_text (each 32 bytes). Required (and only used) when :prev_note is set.

With no :prev_note, only the attestation-binds-checkpoint check runs.

Returns :ok on success or {:error, reason} (a failed binding/consistency check or malformed input).

Examples

# binding-only
:ok = MetamorphicLog.Anchor.verify_anchored(note, [vkey], record)

# binding + consistency from a previous anchored head
:ok =
  MetamorphicLog.Anchor.verify_anchored(newer_note, [vkey], record,
    prev_note: older_note,
    consistency_proof: proof_b64
  )

verify_commitment(record_b64, fetched_commitment_b64)

@spec verify_commitment(
  record_b64 :: String.t(),
  fetched_commitment_b64 :: String.t()
) ::
  :ok | {:error, :commitment_mismatch | String.t()}

Medium-side check: the bytes fetched_commitment_b64 retrieved from the external medium equal the commitment recomputed from record_b64's checkpoint head.

On :ok the medium genuinely attests to this checkpoint head. This is the counterpart to verify_anchored/4: together they prove the head was anchored and the operator never equivocated between anchored heads. The fetch itself is your operator's job (this library performs no I/O).

Returns :ok, {:error, :commitment_mismatch}, or {:error, reason} if the record is malformed.