Files
zeekayandHanzo Dev e699578ba8
Docker Build & Push (Reusable) / create-manifest (push) Blocked by required conditions
Docker Build & Push (Reusable) / tag-single-arch (push) Blocked by required conditions
Docker Build & Push (Reusable) / deploy (push) Blocked by required conditions
Docker Build & Push (Reusable) / sbom (push) Blocked by required conditions
Docker Build & Push (Reusable) / build-amd64 (push) Waiting to run
Docker Build & Push (Reusable) / build-arm64 (push) Skipped
Build kms / build (push) Waiting to run
Release / Build binaries (linux/amd64) (push) Waiting to run
Release / GitHub Release (push) Blocked by required conditions
Release / Multi-arch Docker (ghcr.io/hanzoai/kms) (push) Blocked by required conditions
Release / Notify universe (auto-bump pin) (push) Blocked by required conditions
Release / Build binaries (linux/arm64) (push) Failing after 0s
Release / Build binaries (darwin/arm64) (push) Failing after 0s
Release / Build binaries (darwin/amd64) (push) Failing after 0s
feat(custody): hold node identity keys, sign on their behalf
A Hanzo node is named by a wallet: one secp256k1 keypair is its identity on
the settlement L1, the account it stakes from, the account its work rewards
pay to, and the row the fleet view lists it under. One key with four uses is
a key whose copy is a forged identity and a spendable stake, so the node
does not hold it. The KMS generates it, seals it, and returns it to nobody;
the node asks for signatures.

The measurement is not compared, it is required. A key is sealed with
store.Seal at path="node", name=address, env=measurement, so the image is
the envelope's associated data, and unseal overwrites all three coordinates
with what the CALLER presents. A wrong image is a GCM authentication
failure, not a policy check that a later edit could drop.

The signing surface is a closed set. Every digest is keccak-256 over a
fixed-length preimage led by a purpose tag and committing to the address,
the sealed measurement and a strictly increasing epoch, so no digest minted
here is an RLP transaction hash: the wallet cannot be spent through this
surface, and a captured signature cannot be replayed forward. Signatures are
canonical (low-s), so one statement has one encoding.

Rotation is a change of identity, because a wallet's key cannot change under
a fixed address: the outgoing key signs a succession naming its successor,
that statement is recorded where it stays readable, and the outgoing
material is destroyed in the same transaction. Revocation destroys material
and keeps the row, so a slashed node stays attributable.

custody is a library, not a surface. It holds keys and enforces what is a
property of a key; it derives no permission from a credential and never sees
one. That follows the division this repository already made for secrets: it
answers who signed a token, and the org-scoped plane with its authorization
lives in cloud (apps/kms). Paths therefore carry no tenant — the server
reads it from the credential, as the secret surface does. custody/mount_test.go
is the reference mount, written out in full and driven by the real client, so
the serving side has a worked example rather than a description.

The contract has one definition. sdk/go/node holds the statements, the wire
types and the verification routine, with no store, transport or server
dependency; the KMS, the node daemon and the chain indexer all import it,
and custody builds its digests by calling the same package a verifier does.
Callers reach the surface through sdk/go/kmsclient rather than hand-rolling
requests, and refusals arrive as *kmsclient.Failure carrying the status.
Verification takes the payload the caller already holds — a receipt carries
the measurement it attests but never the payload it was given.

On hardware without SEV-SNP or TDX the node declares its measurement, which
pins the key to one image and detects drift but does not prove the image.
A claim of a hardware TEE with no linked verifier is refused rather than
downgraded: recording "attested" on an unchecked claim would make the fleet
view a lie. docs/custody.tex states what is enforced today against what a
TEE would add, and what a caller is assumed to do.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-05 23:15:43 -07:00

449 lines
24 KiB
TeX

\documentclass[11pt,a4paper]{article}
\usepackage[margin=1in]{geometry}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{booktabs}
\usepackage{longtable}
\usepackage{hyperref}
\usepackage{enumitem}
\newcommand{\code}[1]{\texttt{#1}}
\title{Node Identity Custody in the Hanzo KMS}
\author{Hanzo Industries}
\date{}
\begin{document}
\maketitle
\begin{abstract}
A Hanzo node is identified by a wallet: a secp256k1 keypair whose address names
the node on the Hanzo settlement L1, bonds its stake, receives its work rewards,
and indexes it in the fleet view. A key that can be copied is therefore an
identity that can be forged and a stake that can be spent by someone else. This
document specifies how the Hanzo KMS holds that keypair on the node's behalf:
the key is generated inside the KMS, sealed under associated data that names the
node and the image it is running, and returned to no caller. A node does not
hold its key; it asks for signatures. We state precisely what the image binding
enforces on hardware without a CPU trusted execution environment (TEE), and what
changes when one is present. We give the wire contract the node daemon calls.
\end{abstract}
\section{Problem}
A decentralized compute network has to answer four questions about a machine
that offers it capacity, and normally answers them with four different
identifiers: a host name for attestation, a validator key for consensus
participation, a payment account for settlement, and an inventory row for
operations. Four identifiers means three mappings that can disagree, and every
disagreement is either an outage or an accounting error.
Collapsing them into one wallet removes the mappings. It also concentrates
risk: possession of that one private key is possession of the node's identity,
its stake and its earnings. So the question the design has to answer is not
``where do we store the key'' but ``how do we make sure nothing --- including
the node itself --- ever possesses it.''
\section{What the KMS already provides}
The statements in this section are observed in the implementation at
\code{github.com/hanzoai/kms}, which composes the primitives in
\code{github.com/luxfi/kms}.
\begin{itemize}[leftmargin=*]
\item \textbf{Record envelope.} \code{store.Seal} draws a fresh 256-bit data
encryption key (DEK), encrypts the payload under AES-256-GCM with the
record's coordinates as associated data, and wraps the DEK under a 32-byte
master key. \code{store.Open} inverts it. Associated data is
$\mathit{path} \Vert \texttt{"/"} \Vert \mathit{name} \Vert \texttt{"/"}
\Vert \mathit{env}$ for the payload and $\mathit{name}$ for the DEK wrap.
\item \textbf{Two independent keys.} \code{KMS\_ENCRYPTION\_KEY\_B64} encrypts
the ZapDB volume; \code{KMS\_MASTER\_KEY\_B64} is the record master the
envelope above and the ZAP transport use. They are distinct, so a leaked
volume key alone does not open a sealed record.
\item \textbf{Storage.} ZapDB, an embedded log-structured store, with
age-encrypted incremental replication to S3 when configured.
\item \textbf{Identity.} Every HTTP request carries a Hanzo IAM bearer, verified
per request under RFC 7519 with an asymmetric algorithm allowlist. Claims
read: \code{iss}, \code{sub}, \code{owner} (the tenant), \code{roles}.
\item \textbf{Attestation vocabulary.} \code{luxfi/kms/pkg/attestation} defines
\code{NodeAttestation} --- CPU TEE measurement, GPU report, driver and binary
hashes, policy root, node identity, epoch, TEE family, I/O level --- together
with a composite root over their canonical serialization and a baseline
check. It is used today to gate release of epoch keys.
\item \textbf{Threshold signing.} \code{keys.Manager} delegates validator BLS and
Ed25519 signing to an external \code{luxfi/mpc} $t$-of-$n$ cluster; the KMS
holds a wallet handle rather than key material for those keys.
\end{itemize}
\section{Design}
\subsection{Custody, not distribution}
Node custody extends the existing store rather than introducing a second one.
Its records live in the same ZapDB, seal under the same record master, and are
audited through the same ledger. Two properties distinguish them.
\paragraph{Disjoint keyspaces.} A node's public record is stored at
\code{kms/nodes/\{org\}/\{address\}} and its sealed key at
\code{kms/custody/\{org\}/\{address\}}. Neither the secret API's keyspace
(\code{kms/secrets/}) nor an enumeration of node records can reach key material.
The record type carries no field for key material at all, so no serialization of
it can leak one.
\paragraph{Binding through associated data.} A node's key is sealed with
\code{store.Seal} at the coordinates
\[
\mathit{path} = \texttt{"node"}, \qquad
\mathit{name} = \mathit{address}, \qquad
\mathit{env} = \mathit{measurement},
\]
so the AEAD authenticates the key against the identity it is and the image it may
be used from. The unseal path overwrites those three coordinates with the values
the \emph{caller} presents before calling \code{store.Open}; it never reads them
from the stored record. A mismatched measurement is therefore a GCM
authentication failure rather than a comparison, and an edit that dropped a
policy check would still not produce plaintext under the wrong image.
\subsection{One definition of the contract}
The KMS mints a signature, the node daemon carries it, and the settlement
chain's indexer settles on it. Three programs in three repositories must agree
on a preimage byte for byte, and a preimage reimplemented from prose is a second
definition that can drift from the first.
The statements, the wire types and the verification routine therefore live in a
single package, \code{github.com/hanzoai/kms/sdk/go/node}, which all three
import. It depends only on a secp256k1 and keccak-256 library: it has no store,
no transport and no server dependency, so importing it does not pull in the KMS.
The custody package in the server holds what the others must not --- the key,
and the rules about when it may be used --- and constructs its digests by calling
the same package a verifier does. There is no second implementation to drift.
The client for the surface below ships alongside it in the same module, so a
caller obtains both the calls and the checker from one import and never
constructs a request by hand.
\subsection{The signing surface is a closed set of statements}
The key is a wallet, so an interface that signs caller-supplied bytes is an
interface that spends the wallet. The KMS signs only digests it constructs
itself, over fixed-length preimages whose first field is a purpose tag:
\begin{align}
d_{\mathrm{attest}} &= H\big(H(\text{``hanzo.node.attest.v1''}) \Vert a \Vert m \Vert e \Vert H(p)\big), \label{eq:attest}\\
d_{\mathrm{succ}} &= H\big(H(\text{``hanzo.node.succession.v1''}) \Vert a_{\mathrm{prev}} \Vert a_{\mathrm{next}} \Vert e\big), \label{eq:succ}
\end{align}
where $H$ is keccak-256, $a$ is the 20-byte signing address, $m$ the 32-byte
measurement the key is sealed to, $e$ a 64-bit big-endian epoch, and $p$ the
payload the node wants attested. Every field is fixed length, so the encoding is
injective and the two purposes cannot collide.
Two consequences follow. First, an Ethereum transaction hash is
$H(\mathrm{RLP}(\mathit{tx}))$ with no such prefix, so a signature minted here
cannot be a valid transaction signature other than by second preimage on
keccak-256. The node key controls funds, and this interface cannot move them.
Second, \eqref{eq:attest} always commits to the node's own address and to the
measurement its key is sealed to; a node cannot obtain a signature that omits
which image produced it.
\subsection{Epoch monotonicity}
The KMS records the last epoch it signed for an identity and refuses any epoch
that does not exceed it. A captured signature is therefore not replayable into a
later epoch. The epoch is an opaque unsigned 64-bit value supplied by the caller;
the settlement chain's height is the intended source, with wall-clock seconds as
a floor.
\section{Lifecycle}
\paragraph{Enrolment} mints one active identity per IAM subject per tenant. A
repeat enrolment presenting the same measurement returns the existing record, so
a node that restarts recovers its own address without holding any state. A
repeat presenting a \emph{different} measurement is refused: an image that was
not approved must not be able to walk away with a fresh wallet. Revoked and
superseded records are excluded from this lookup, so a decommissioned node's
operator can bring up a new identity without an administrator.
\paragraph{Rotation} replaces the keypair. A wallet's private key cannot change
while its address stays fixed, so rotation is a change of identity and has to be
provable as one. The outgoing key signs \eqref{eq:succ} naming its successor;
that statement is recorded on the outgoing record, where it can be read again;
the outgoing material is then destroyed. Successor creation, succession
recording and predecessor destruction commit in one store transaction, because
both intermediate states are wrong --- two active identities for one subject, or
a node with no usable key. The settlement L1 moves a stake by verifying the
succession against the address it already knows.
\paragraph{Rebinding} re-seals an existing key under a new measurement. It is
what an image upgrade requires and the only thing that provides it: a key bound
to one measurement stops working the moment the node boots a different image,
which is the property the binding exists for. Rebinding requires the current
evidence, because the key is recoverable only under it, and it is
administrative: the new measurement asserts which image the fleet may now run,
and a node must not make that assertion about itself.
\paragraph{Revocation} ends an identity. The material is destroyed, so the
identity cannot sign again even if the master key later leaks; the record
survives as a tombstone, so a slashed node stays attributable in the fleet view
and to the settlement L1. Revocation is terminal; the path back is a fresh
enrolment.
\section{Authority, and where it is decided}
Custody is a library. It holds keys and enforces what is a property of a key;
it derives no permission from a credential, and it never sees one. That division
follows the one the secret plane already makes: this repository answers who
signed a token, and the org-scoped HTTP plane and its authorization live in the
cloud repository (\code{apps/kms}). The two obligations are:
\begin{center}
\begin{tabular}{ll}
\toprule
\textbf{The serving side} & \textbf{Custody} \\
\midrule
verifies the bearer & takes org and subject as given \\
derives $(\mathit{org}, \mathit{subject}, \mathit{administrator})$ from it & seals under the presented image \\
passes org and subject in & confines an identity to its subject \\
refuses rebind and revoke without the role & refuses a non-increasing epoch \\
reads no tenant from the URL & keeps tenants disjoint in the keyspace \\
\bottomrule
\end{tabular}
\end{center}
The right column holds whether or not the left one is correct. A mount that
forgot the role check could rebind an identity, but it could not sign as a
node that did not enrol, produce a signature under an image the key was not
sealed to, replay an epoch, or reach another tenant's rows --- those refusals
are the cipher's and the store's, not a policy's.
The subject binding is the one that stops a compromised node signing as its
neighbour: a fleet credential is valid for the whole tenant, and without the
binding any node holding one could keep a decommissioned sibling looking alive.
It is in custody rather than in the mount precisely because it must not be
something a serving side can forget.
\code{custody/mount\_test.go} is the reference mount, written out in full and
exercised against the real client, so the serving side has a worked example
rather than a description.
\section{What the image binding enforces}
This is the part of the design whose strength depends on hardware, so we state
it separately from the mechanism.
The measurement is \code{NodeAttestation}'s \code{CpuTeeMeasurement}. On a host
running AMD SEV-SNP or Intel TDX it is the launch measurement the CPU computes
over the guest image and signs into an attestation report: an unforgeable
statement of which image is running. On hardware without such a facility ---
Apple Silicon under the Virtualization framework, and x86-64 or arm64 hosts with
no SEV-SNP or TDX --- no report exists, and the node \emph{declares} the value:
the digest of the microVM kernel and root filesystem it booted.
\paragraph{Enforced today, on any hardware.} The private key is generated inside
the KMS and never returned. It is sealed at rest under a master key distinct
from the volume key. Signing is confined to \eqref{eq:attest} and
\eqref{eq:succ}, and every signature is canonical, so a statement has one
encoding. Epochs are strictly increasing per identity. Only the subject
an identity was enrolled to may use it. Tenants are isolated: the tenant is part
of every store key, and an administrator of one tenant cannot reach another's
fleet. Revocation and rotation destroy key
material irrecoverably. The key is usable from exactly one measurement, and a
change of image makes every unseal and every signature fail until an
administrator rebinds it.
\paragraph{Not enforced today.} That the declared measurement is the image
actually running. The value is chosen by the node, so the binding detects drift
--- an image change cannot silently continue using the identity --- but it does
not prove the image. An adversary with code execution on the host can present
the measurement of the image it is impersonating.
\paragraph{What a TEE adds, and what it does not change.} With SEV-SNP or TDX the
same field carries a hardware-signed launch measurement, and the property
upgrades from \emph{declared and pinned} to \emph{proven}. No other part of this
design changes: the binding, the closed signing surface, the epoch rule, the
lifecycle and the wire contract are identical. What has to be added is a
verifier that checks the attestation report against the vendor's root (the
AMD VCEK chain, or Intel's provisioning certification service) and extracts the
measurement from the verified report.
Until such a verifier is linked, an evidence submission naming a hardware TEE is
\emph{refused}, not downgraded to declared. This is deliberate and is the
fail-closed direction: accepting an unchecked report would let any caller mark
its node as TEE-backed in the fleet view, which is worse than declaring nothing,
because a reader would then treat an unverified claim as a verified one.
\paragraph{On the strength of the cryptographic binding.} Because the KMS stores
the measurement beside the record, an honest KMS could in principle unseal
without the caller's assertion. The associated-data binding is therefore not an
authenticity claim about the caller; it is a structural one about the code: the
only implemented unseal path takes the measurement from the presented evidence,
so no code path exists that can produce plaintext under a measurement nobody
presented. When the measurement becomes hardware-attested, that same path
becomes an authenticity claim with no change.
\section{Wire contract}
All paths are under \code{/v1/kms}; there is no \code{/api/} segment and no
alias. Every request carries a Hanzo IAM bearer. Hashes and addresses are
\code{0x}-prefixed hexadecimal; byte strings (\code{quote}, \code{payload}) are
base64.
The path carries no tenant. The server takes the org from the credential the
request is made with and folds it into the storage key, which is what the secret
plane does after an org in the path was measured against the live edge and found
to reach nothing. An org in the URL would be a second answer to a question the
bearer already settles, and the two could disagree.
\begin{longtable}{p{0.06\textwidth}p{0.40\textwidth}p{0.46\textwidth}}
\toprule
\textbf{Verb} & \textbf{Path} & \textbf{Meaning} \\
\midrule
\endhead
POST & \code{/v1/kms/nodes} & enrol; 201 new, 200 existing \\
GET & \code{/v1/kms/nodes} & the tenant's fleet, by wallet \\
GET & \code{/v1/kms/nodes/\{address\}} & one identity \\
POST & \code{/v1/kms/nodes/\{address\}/sign} & one attestation signature \\
POST & \code{/v1/kms/nodes/\{address\}/rotate} & successor plus signed succession \\
POST & \code{/v1/kms/nodes/\{address\}/rebind} & re-seal under a new image (admin) \\
POST & \code{/v1/kms/nodes/\{address\}/revoke} & destroy the key (admin) \\
\bottomrule
\end{longtable}
\paragraph{Evidence} is the object every request that touches key material
carries:
\begin{verbatim}
{ "tee": "none", "measurement": "0x<32 bytes>", "quote": "<base64>" }
\end{verbatim}
\code{tee} is one of \code{none}, \code{sev-snp}, \code{tdx}, \code{sgx}; empty
reads as \code{none}. \code{quote} is the hardware report and is present only
with a hardware family; supplying one under \code{none} is a contradiction and
is refused.
\paragraph{Bodies.}
\begin{itemize}[leftmargin=*]
\item enrol: \code{\{"evidence": E\}}
\item sign: \code{\{"evidence": E, "epoch": u64, "payload": base64\}}
\item rotate: \code{\{"evidence": E, "epoch": u64\}}
\item rebind: \code{\{"from": E, "to": E\}}
\item revoke: \code{\{"reason": string\}}
\end{itemize}
\paragraph{Record} is what enrol, read, rebind and revoke return: \code{address},
\code{org}, \code{subject}, \code{public\_key}, \code{measurement}, \code{tee},
\code{attested}, \code{status} (\code{active}, \code{superseded},
\code{revoked}), \code{epoch}, \code{prev}, \code{next}, \code{succession},
\code{reason}, \code{created\_at}, \code{updated\_at}.
\paragraph{Receipt} is what sign returns: \code{address}, \code{measurement},
\code{epoch}, \code{digest}, and \code{signature} --- 65 bytes, $r \Vert s \Vert
v$ with $v \in \{0,1\}$, so \code{ecrecover} yields the signing address directly.
It carries the measurement so a verifier learns which image signed without a
second lookup, and it does \emph{not} carry the payload: the verifier already
holds the bytes it wants checked, and echoing them back would invite checking
against the server's copy rather than its own.
Verification recomputes \eqref{eq:attest} from the receipt's fields and the
caller's payload, requires the result to equal the stated \code{digest}, and
requires the recovered address to equal the stated \code{address}. Both halves
are necessary: checking the signature against the stated digest alone would
establish only that some node signed something.
\paragraph{Canonical signatures.} A recovered signature is accepted only in
canonical form: $v \in \{0,1\}$, $r$ and $s$ in range, and $s$ in the lower half
of the group order. secp256k1 admits $(r,s)$ and $(r, n-s)$ as signatures by the
same key over the same digest, so without this rule one statement has two valid
encodings and a verifier that deduplicates by signature bytes can be shown the
same statement twice. The signer never produces the high-$s$ form, so rejecting
it costs nothing.
\paragraph{Status codes.} 401 no or invalid bearer; 403 wrong tenant, wrong
subject, or an administrative operation attempted without the role; 404 unknown
identity; 409 measurement drift, non-increasing epoch, or an identity that is not
active; 410 key material destroyed; 400 malformed input, including an evidence
submission naming a TEE for which no verifier is linked.
\section{What the node daemon calls}
The daemon imports \code{github.com/hanzoai/kms/sdk/go/kmsclient} and configures
it for HTTP with its own IAM machine credentials. The node surface is HTTP only:
a node asks for its identity before it has joined the cluster, so the in-cluster
binary transport is not yet reachable to it, and a client configured for that
transport is refused rather than left to fail later.
\begin{verbatim}
c, _ := kmsclient.New(kmsclient.Config{
Endpoint: "https://api.hanzo.ai", IAMEndpoint: "https://hanzo.id",
ClientID: ..., ClientSecret: ..., Org: "hanzo"})
// Org configures the secret calls' paths; the node calls do not use it,
// because the server reads the tenant from the credential.
ev := node.Declare(measurement) // no CPU TEE: declared digest
rec, created, err := c.Enroll(ctx, ev) // idempotent per IAM subject
receipt, err := c.Sign(ctx, rec.Address, ev, epoch, report)
err = receipt.Verify(report) // anyone can run this
next, handover, err := c.Rotate(ctx, rec.Address, ev, epoch)
rec, err = c.Rebind(ctx, addr, from, to) // administrator
rec, err = c.Revoke(ctx, addr, reason) // administrator
fleet, err := c.Fleet(ctx) // the tenant's nodes by wallet
\end{verbatim}
Every refusal arrives as a \code{*kmsclient.Failure} carrying the status, so a
daemon branches on the distinction the surface draws --- 409 for a measurement
that no longer matches the sealed image or an epoch that did not advance, 403 for
a credential that does not own the identity, 410 for material already destroyed
--- rather than on message text.
Two obligations fall on the caller. Each node needs its own IAM machine
identity, because the subject binding is what confines a node to its own key;
a credential shared across a fleet collapses the fleet onto one wallet. And the
epoch must strictly increase per identity, which is what makes a captured
receipt unreplayable; the settlement chain's height is the intended source.
\section{Assumptions and limitations}
\begin{enumerate}[leftmargin=*]
\item \textbf{The KMS is trusted.} It holds every node key in the fleet. Its
compromise is a fleet compromise. Node identity keys are deliberately not
threshold-held: a $t$-of-$n$ ring signing every node's boot report would make
node liveness depend on ring liveness, and the threat this design addresses
is copying of a per-node key rather than misuse of a systemic one. Keys whose
compromise is systemic --- the settlement L1's own validators --- stay in the
existing MPC path.
\item \textbf{Each node needs its own IAM machine identity.} The subject
binding is what confines a node to its own key. A credential shared across a
fleet degrades the guarantee to ``any node in this tenant may sign as any
other node in this tenant.''
\item \textbf{No approved-measurement set is enforced.} Enrolment pins whatever
measurement is first presented; rebinding requires an administrator. There is
no per-tenant baseline of measurements the KMS will accept. Where the fleet's
approved images should be enumerated --- here, in the staking contract, or in
the operator's declared state --- is an open question, not an oversight.
\item \textbf{Epoch source.} Monotonicity is enforced; the meaning of the
epoch is the caller's. Two nodes may use unrelated epoch scales without the
KMS noticing.
\item \textbf{Zeroing is best effort.} Byte buffers holding a private scalar are
overwritten after use. The big-integer representation inside the reconstructed
key cannot be wiped in place, so the copy's lifetime is bounded rather than
its existence eliminated.
\end{enumerate}
\section{Next steps}
\begin{enumerate}[leftmargin=*]
\item A verifier for SEV-SNP and TDX reports, checking the vendor certificate
chain and extracting the measurement from the verified report. This is the
one change that moves the image binding from declared to proven.
\item An approved-measurement set per tenant, so an enrolment or a rebinding
can be checked against the images the fleet is allowed to run rather than
against an administrator's assertion alone.
\item An on-chain verifier for \eqref{eq:attest} and \eqref{eq:succ}, so
registration, attestation freshness and stake succession are checked by the
settlement L1 rather than off chain. The digests and the recovery rule are
already shared as a library; what remains is the contract that reads them,
and a decision on where the epoch comes from so that freshness means the
same thing to every node.
\end{enumerate}
\end{document}