Files
hips/hip-node-identity-wallet.tex
zeekayandHanzo Dev 4a4e45b5c3 Reach the hardware through a signature, not a second claim on the field
The confidential-tier proposal specifies the report-data field and reaches the
same conclusion about a boot-only binding. There is one such field and it has
one specification, so this document stops proposing a second. What its B1 does
not carry is a durable identity: the session keys it binds are ephemeral by
requirement, so a report says a confidential environment holds these keys and
nothing that reaches the bonded node.

The join is the epoch statement naming the same session terms, so a verifier
follows report -> receipt key <- statement -> address, checking two links
separately. Adding the address to B1 would be stronger and is offered to that
proposal rather than assumed here. States plainly that this binds identity to
session, not key to silicon.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-05 22:57:11 -07:00

1828 lines
92 KiB
TeX

% hip-node-identity-wallet.tex
% Hanzo Improvement Proposal — Node Identity: the Wallet as the Node.
% Number assigned when this lands in HIPs/.
\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[margin=1in]{geometry}
\usepackage{amsmath,amssymb,amsthm}
\usepackage{booktabs}
\usepackage{longtable}
\usepackage{array}
\usepackage{enumitem}
\usepackage{xcolor}
\usepackage{microtype}
\usepackage[colorlinks=true,linkcolor=black,citecolor=black,urlcolor=blue]{hyperref}
\newtheorem{definition}{Definition}[section]
\newtheorem{claim}[definition]{Claim}
\newtheorem{remark}[definition]{Remark}
% Long identifiers are the norm here, so let the typewriter font break.
\newcommand{\code}[1]{\texttt{\hyphenchar\font=`\-\relax #1}}
\sloppy
\emergencystretch=3em
\newcommand{\Held}{\mathrm{Held}}
\newcommand{\Ran}{\mathrm{Ran}}
\newcommand{\Conf}{\mathrm{Conf}}
\newcommand{\Vrfy}{\mathsf{Vrfy}}
\newcommand{\Sign}{\mathsf{Sign}}
\newcommand{\addr}{\mathsf{addr}}
\newcommand{\sha}[1]{\mathrm{SHA\text{-}#1}}
\newcommand{\kec}{\mathrm{Keccak256}}
\setlist[itemize]{itemsep=2pt,topsep=4pt}
\setlist[description]{itemsep=3pt,topsep=4pt}
\title{\textbf{Node Identity: the Wallet as the Node}\\[6pt]
\large A Hanzo Improvement Proposal}
\author{Hanzo AI}
\date{5 September 2026}
\begin{document}
\maketitle
\begin{center}
\begin{tabular}{@{}ll@{}}
\toprule
Number & assigned on merge into \code{HIPs/} \\
Type & Standards Track \\
Category & Core \\
Status & Draft \\
Requires & HIP-0024 (Hanzo sovereign L1), HIP-0026 (IAM), HIP-0027 (KMS),\\
& HIP-0095 (QoS challenge), HIP-0096 (compute rewards),\\
& HIP-0097 (\code{did:hanzo:}), HIP-0121 (BYO compute fleet),\\
& HIP-0134 (one process, one socket, one identity),\\
& HIP-1325 (\code{/v1/node}) \\
\bottomrule
\end{tabular}
\end{center}
\begin{abstract}
\noindent
A Hanzo node is a physical host that boots a Linux microVM, joins one k3s
cluster, and runs the Hanzo cloud. This proposal specifies the identity of such
a node: an account address on the Hanzo sovereign L1, held by the node's
\code{hanzod}, used to sign what the node says about itself, named in the node's
on-chain registration, joined by signature to the node's hardware report,
presented to the cluster at admission, and paid the node's rewards.
The proposal is written around one distinction, because every claim the system
can make about a node depends on it. A signature establishes that some party
able to evaluate the signing function chose to sign a message. It does not
establish which machine that party was, nor what software that machine was
running. An attestation rooted in a hardware vendor's certificate chain
establishes the second of those, conditionally, and only for platforms that have
the hardware, and only if the key is inside the report. We name the difference
the \emph{possession--platform gap} (\S\ref{sec:gap}), state what falls on each
side of it, and specify the custody, join, staking, reward and lifecycle rules
that follow from where a given node falls.
Four properties are load-bearing. First, a node key that can be copied is an
identity that can be forged; custody is therefore specified as a ladder with a
derived assurance class, not as a claim an operator makes. Second, the identity
key (on the box, signing constantly) and the stake owner (off the box, moving
money) are two roles and two keys, because a design in which the compromised key
can also withdraw the bond has no recovery path. Third, forfeiture on-chain is
restricted to self-proving faults --- principally equivocation, which is exactly
what an uncoordinated copy of a key produces --- so that slashing needs no quorum
and no oracle. Fourth, a hardware report binds a platform to an image and to the
ephemeral keys of a session; nothing in it reaches a durable identity, so the link
from a bonded node to attested hardware has to be made by a signature and is
specified here.
Section~\ref{sec:status} and Appendix~\ref{app:fleet} separate what is
implemented from what is proposed. Measurement, the SHA-384 register fold, and
the SEV-SNP and TDX report request paths exist in \code{hanzoai/vm}
(\code{crates/vm-measure}); a second, incompatible measurement and a host-signed
evidence type exist in \code{hanzoai/node} (\code{hanzo-libs/hanzo-attest}); the
wallet custody backends exist in \code{hanzoai/cloud} (\code{apps/wallet}) and
\code{hanzoai/cli} (\code{hanzo wallet}); the validator manager exists in
\code{luxfi/standard}. The identity binding between them, the epoch statement,
cluster admission by address, and forfeiture do not exist and are proposed here.
\end{abstract}
\tableofcontents
% =====================================================================
\section{Scope}
This document specifies the identity of a node in the Hanzo decentralized AI
cloud: how the identity is created, how it is held, what it signs, what a
verifier may conclude from what it signs, how it enters the cluster, how it
stakes, how it is paid, and how it ends.
It does not specify consensus, the scheduler, the workload API, or the
verification of AI work. Verification of advertised capability and delivered
service is HIP-0095 (QoS challenges) and HIP-0096 (compute rewards); this
document states only where the identity boundary meets those and what identity
does not supply.
Throughout, \emph{observed} marks a statement about code or hardware that was
read or measured, with the location given. \emph{Proposed} marks a change this
document asks for. Statements with neither marker are definitions or
consequences of the two.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as
in RFC 2119.
% =====================================================================
\section{Terms}
\begin{description}
\item[Node] A host that runs \code{hanzod} and offers compute to the network.
Physical, and heterogeneous: Appendix~\ref{app:fleet} records three, on three
architectures, with three different attestation capabilities.
\item[Machine] A microVM the node starts. A node serves many. The vocabulary
\emph{machine} $>$ \emph{cluster} $>$ \emph{vm} $>$ \emph{sandbox} is fixed
elsewhere in the platform; in this document \emph{machine} means a microVM the
node runs and \emph{host} means the physical box.
\item[Monitor] The \code{hanzo-vm} binary that builds and runs a machine.
Virtualization.framework on macOS, cloud-hypervisor or KVM on Linux.
\item[Node microVM] The one machine on each host that joins the k3s cluster and
runs the Hanzo cloud. It is the uniformity layer: it is why an Apple-Silicon
Mac and an x86 workstation present the same surface to the cluster. Exactly one
per node.
\item[Launch] What the hypervisor was handed: kernel, kernel command line,
initramfs where present, source root image, and shape. \emph{Observed} as the
launch log in \code{vm-measure} (\code{crates/vm-cli/src/measure.rs},
\code{launch}).
\item[Workload] What the started system was told to run, extended into a second
log by whatever drives the machine. \emph{Observed} as the second log in
\code{vm-measure}; \code{hanzo up} is the intended extender.
\item[Register] The fold of one log:
$R_{i+1} = \sha{384}(R_i \,\|\, e_i)$ from 48 zero bytes, where
$e_i = \sha{384}(\mathit{name}_i \,\|\, \mathtt{0x00} \,\|\, \sha{384}(\mathit{content}_i))$.
Written $L$ for the launch register and $W$ for the workload register.
\emph{Observed}, \code{crates/vm-measure/src/lib.rs}.
\item[Measured state] $\mu = (L, W)$. Used in \S\ref{sec:proves} where the
argument does not depend on which of the two registers is meant.
\item[Bind] The 64 bytes a hardware report carries as its caller-supplied field
(REPORT\_DATA on SEV-SNP, the report data on TDX). \emph{Observed} as
$\beta = \sha{512}(L \,\|\, W)$, \code{Measurement::bind}.
\item[Node key] The secp256k1 key whose address is the node's identity. Held by
\code{hanzod}. Signs the node's statements about itself.
\item[Node address] $A_n = \addr(pk_n)$, the low 20 bytes of $\kec$ over the
uncompressed public key. This is the identity.
\item[Owner key] The key that registers the node, posts the bond, ends the
validation, and withdraws. Address $A_o$. Held by the operator, off the node.
\item[Assurance class] What a node can \emph{prove} about the platform holding
its key. Derived from evidence, never declared. \S\ref{sec:class}.
\end{description}
% =====================================================================
\section{What signatures, measurements and attestations establish}
\label{sec:proves}
This section is the foundation. Everything later --- staking, forfeiture, reward
attribution, cluster admission, fleet enumeration --- inherits exactly the
assurance established here and no more.
Let $\Sigma = (\mathsf{Gen}, \Sign, \Vrfy)$ be a signature scheme existentially
unforgeable under chosen-message attack. Fix a keypair $(sk, pk)$ and write
$A = \addr(pk)$. Over a run of the system define three predicates:
\begin{description}
\item[$\Held(P, sk, t)$] party $P$ can evaluate $\Sign_{sk}(\cdot)$ at time $t$.
\item[$\Ran(h, I, t)$] host $h$ was executing image $I$ at time $t$.
\item[$\Conf(sk, h)$] $sk$ is confined to $h$: no party other than $h$ can
evaluate $\Sign_{sk}(\cdot)$.
\end{description}
\subsection{A signature}
\begin{claim}[What verification gives]\label{cl:sig}
If $\Vrfy_{pk}(m, \sigma) = 1$ then, under EUF-CMA, there exist a party $P$ and a
time $t$ with $\Held(P, sk, t)$, and $P$ chose the message $m$.
\end{claim}
\begin{claim}[What it does not give]\label{cl:indep}
The conclusion of Claim~\ref{cl:sig} is independent of $\Ran$ and $\Conf$. For
any host $h$ and image $I$ a verifier believes in, there is a run in which the
conclusion holds and $\Ran(h, I, t)$ is false.
\end{claim}
\noindent The proof is the attack: copy $sk$ to any machine and sign $m$ there.
Nothing in $\sigma$ distinguishes the two runs, because nothing in $\sigma$
depends on where the signing happened.
Claim~\ref{cl:indep} holds for every message, including a message that is
\emph{about} a machine. Signing a measured state does not make the signature
evidence about a boot. It makes it evidence that the key-holder asserted a
number.
\subsection{A measurement}
$L$ and $W$ are functions of files and text on a host, computed by the host,
before and during the machine's life. Two parties holding the same inputs compute
the same registers; a third party holding only a register can check a claim about
those inputs. That is the entire content.
\begin{claim}\label{cl:meas}
A register binds inputs. It does not witness execution. A host that folds one set
of files into the register and hands the monitor a different set produces a value
that verifies against the first set and describes nothing that ran.
\end{claim}
This is stated in the implementation itself (\code{crates/vm-measure/src/lib.rs},
module documentation: ``computed on the host, a measurement is a statement about
what was launched, not evidence against the host that launched it. A host that
can substitute the kernel can also substitute the program that hashes it''). We
restate it because it is the property most often lost in transit: a measurement
in a registry looks like a fact about a running system and is not one.
\subsection{An attestation}
\label{sec:att}
\begin{claim}[What a hardware report gives]\label{cl:att}
Let $Q$ be a report over 64 caller-supplied bytes $\beta$, verifying under vendor
root $R$, and carrying the platform's own launch measurement $\lambda$. Then
there exists a platform $h$ whose attestation key $R$ certifies, such that $h$
was launched in state $\lambda$ and $h$ reported the value $\beta$.
\end{claim}
Four things do not follow:
\begin{enumerate}[label=(\alph*)]
\item \textbf{A measured state is not behavior.} A register covers what was
loaded. Code loaded later, a kernel module, a \code{kexec}, a writable root
filesystem, or an interpreter reading a script from the network all change
behavior without changing $L$. A measured platform running a program that
fetches and executes arbitrary bytes measures as honestly as one that does not.
\item \textbf{$\beta$ is not $\Conf$.} Reporting a value inside a report binds it
to the platform's \emph{statement}, not to the platform. $\Conf(sk_n, h)$
follows only if $sk_n$ was generated inside $h$ and is non-extractable, and only
if the platform reports that fact in a form the verifier checks. A key generated
elsewhere and copied in attests exactly as well.
\item \textbf{A report says nothing about a key it does not name.} If $\beta$ is
a function of the image alone, the report is a statement about the image alone,
and any node holding that image can present it. \S\ref{sec:bind} develops this,
because it is the state of the implemented code.
\item \textbf{The host is in the trust base unless the VM is confidential.}
Guest-side measurement of a machine whose memory the hypervisor can read and
write establishes $\Ran$ only relative to an honest host. On AMD SEV-SNP, Intel
TDX or Arm CCA the host is removed from that base. \emph{Observed:} none of the
three hosts in Appendix~\ref{app:fleet} runs a confidential VM ---
\code{kvm\_amd.sev\_snp = N} on the x86 host, no Realm support on the aarch64
host, and Virtualization.framework on the Mac. Guest measurement on this fleet
therefore defends against a remote attacker and not against the node operator.
\end{enumerate}
Point (d) matters most for third-party nodes, where the node operator is the
adversary of interest. On the current fleet the only hardware root that speaks
about the \emph{host} is the TPM (\S\ref{sec:class}).
\subsection{The possession--platform gap}
\label{sec:gap}
\begin{definition}[Possession--platform gap]
The gap between $\Held$ and $\Conf$: a signature establishes that \emph{someone}
could sign, and never that \emph{this platform} is the only one that could.
\end{definition}
Every downstream property inherits it. If the node key can be copied, then
``which node produced this statement'' has no answer, and with it neither does
``which node earned this reward'', ``which node's stake answers for this fault'',
nor ``how many nodes are in the fleet''. Fleet enumeration by wallet is only as
meaningful as the injectivity of the map from wallets to machines, and a copied
key breaks injectivity silently: two machines answer as one node, and the console
shows one row.
Closing the gap requires two properties, both of which must be reported and
checked, not assumed:
\begin{enumerate}[label=(\roman*)]
\item the key is generated inside a platform and is non-extractable from it, and
\item the platform can produce, under a root the verifier trusts, a statement
binding that key to a measured state.
\end{enumerate}
No software arrangement supplies (i). Custody in a key-management service does
not supply it either --- it \emph{moves} the gap, which is a real improvement and
a different property; see \S\ref{sec:ladder}. The implemented report path supplies
neither, because the value it reports does not mention the key; see
\S\ref{sec:bind}.
\subsection{What an unattested but signed join does not prove}
\label{sec:notprove}
A node that signs its measured state with its node key and presents that to the
network proves that the holder of $sk_n$ asserted $\mu$ at a stated time and that
the assertion has not been altered since. It does not prove any of the following.
Each is a distinct failure, and none is covered by adding more signatures of the
same kind:
\begin{enumerate}[label=\textbf{N\arabic*.},leftmargin=2.6em]
\item \textbf{Not that the machine ran the measured image.} The registers are
folded over files chosen by the same party that signs. A node that measures
image $I$ and boots image $I'$ produces a statement that verifies
(Claim~\ref{cl:meas}).
\item \textbf{Not that the statement came from the host it describes.} The
signing key is portable; the statement can be produced anywhere, including on a
laptop, and relayed.
\item \textbf{Not that the key is on any particular host at all.} $\Conf$ is not
established (Claim~\ref{cl:indep}).
\item \textbf{Not that the identity is one machine.} Without an epoch-unique
statement (\S\ref{sec:equiv}), $N$ machines holding a copy of $sk_n$ join as one
node and are indistinguishable from one machine.
\item \textbf{Not that the advertised inventory is real.} CPU, memory and GPU
counts are numbers the node reports. The shape inside the launch log is what
\code{hanzod} \emph{asked the monitor for}, which is what it can honestly sell,
and is not a statement about the physical host.
\item \textbf{Not that the machine still runs what it reported.} The launch
register is fixed before start. Nothing in the statement constrains the machine
afterwards.
\item \textbf{Not that the node will do the work correctly.} Identity is
orthogonal to service. HIP-0095 and HIP-0096 own that.
\item \textbf{Not that the signer is authorized.} Verification of a signature is
not authorization. Whether the network accepts $A_n$ is a registration question
(\S\ref{sec:stake}), and the implementation is right to separate them
(\code{hanzo-attest/src/evidence.rs}: ``Deciding that a signer is acceptable is
a policy question \dots\ not here'').
\end{enumerate}
\subsection{Five controls, none substitutable}
\begin{center}
\small
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.15\textwidth}>{\raggedright\arraybackslash}p{0.35\textwidth}>{\raggedright\arraybackslash}p{0.40\textwidth}@{}}
\toprule
\textbf{Control} & \textbf{Establishes} & \textbf{Does not establish} \\
\midrule
Signature &
Authorship of a statement under a key &
Which machine; which image; uniqueness \\[3pt]
Custody &
Who can cause a signature to exist &
That the statement is true \\[3pt]
Attestation &
That a vendor-rooted platform reported a launch state, and reported the 64 bytes it was handed &
That the state is the behavior; that the host is outside the trust base, unless the VM is confidential; anything about a key the 64 bytes do not name \\[3pt]
Stake &
A price for a fault, once the fault is established &
Detection \\[3pt]
Challenge (HIP-0095) &
Detection of false capability and service claims &
Prevention; identity \\
\bottomrule
\end{tabular}
\end{center}
Layering is not redundancy here: each control's ``does not establish'' column is
covered by a different row, and removing any row leaves a hole no other row fills.
A system with attestation and no stake detects faults it cannot price. A system
with stake and no attestation prices faults it cannot detect.
% =====================================================================
\section{The credentials that exist, and the one that replaces them}
\label{sec:three}
\subsection{Three, observed}
A node today presents three unrelated credentials, none of which is a wallet, and
no two of which name the same subject.
\begin{center}
\small
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.16\textwidth}>{\raggedright\arraybackslash}p{0.24\textwidth}>{\raggedright\arraybackslash}p{0.24\textwidth}>{\raggedright\arraybackslash}p{0.28\textwidth}@{}}
\toprule
\textbf{Credential} & \textbf{Names} & \textbf{Custody, observed} & \textbf{What its holder gets} \\
\midrule
\code{IDENTITY\_SEC\-RET\_KEY} (ed25519) &
the fabric node, by a free-form string &
plaintext \code{KEY=value} file, or the environment variable of the same name &
the ability to sign as that node, from anywhere \\[3pt]
IAM bearer token &
the operator's org, not the node &
an owner-only credential file under the CLI's data directory,
\code{\dots/hanzo/credentials} &
every cloud call the org can make \\[3pt]
\code{K3S\_TOKEN} &
nothing --- it is shared by every agent &
root-owned \code{0600} file on each agent &
membership of the cluster under any node name \\
\bottomrule
\end{tabular}
\end{center}
\emph{Observed, in order.} \code{hanzoai/node},
\code{hanzo-bin/hanzo-node/src/utils/keys.rs}: \code{generate\_or\_load\_keys}
parses \code{IDENTITY\_SECRET\_KEY} out of a \code{KEY=value} file, falls back to
the environment variable, and otherwise generates an ephemeral key;
\code{src/runner.rs}:342--352 writes the secret back with \code{fs::write}, which
creates at the process umask rather than owner-only. \code{hanzoai/cloud},
\code{cli/link.go}: ``One identity: the IAM token \code{hanzo login} mints
authorizes every cloud call; the server derives the tenant from the token'' ---
correct for the operator, and it is the operator that is authenticated, not the
machine. On the measured fleet, the x86 host joined the cluster from
\code{/etc/systemd/system/k3s-agent.service.env}, mode \code{0600}, root-owned,
carrying \code{K3S\_TOKEN} and \code{K3S\_URL}; the control-plane holds the same
value at \code{/var/lib/rancher/k3s/server/node-token}.
\subsection{What each is worth}
The ed25519 key is at rung L0 of the custody ladder (\S\ref{sec:ladder}): a file
read is a forged identity, and by Claim~\ref{cl:indep} no statement it signs says
where it was signed. The IAM token authenticates a person or an org and is the
right credential for the act of linking a machine; it is the wrong credential for
a machine, because it is not per-machine, it carries the operator's authority, and
a node holding one can act as the operator. The k3s token is a fleet-wide shared
secret: it is not an identity at all, it is a capability to become a member, and
its holder chooses the name it joins under.
\begin{claim}
No composition of the three yields node identity. Two of them do not name a node,
and the one that does is at L0.
\end{claim}
\subsection{The replacement}
One key, one address, four uses:
\begin{enumerate}[label=(\alph*)]
\item the node signs its measured state with it, and at class \code{platform} or
above the hardware report commits to it (\S\ref{sec:bind});
\item it is the address bound to a bonded registration on the settlement L1, and
the address a forfeiture is charged against (\S\ref{sec:stake});
\item it is the address AI-work rewards pay (\S\ref{sec:reward});
\item it is the row key of the fleet view and the node's name in the cluster
(\S\ref{sec:enum}, \S\ref{sec:admit}).
\end{enumerate}
The IAM token stays where it belongs --- the operator, at the moment of linking,
authorizing the registration. It MUST NOT be the node's durable credential, and
\code{hanzod} MUST NOT hold an org-scoped bearer token that can act as the
operator. The k3s shared token is retired in favour of a short-lived credential
issued against $A_n$ (\S\ref{sec:admit}). This is HIP-0134's rule --- one process,
one socket, one identity --- applied to the machine rather than to the process.
% =====================================================================
\section{The identity}
\label{sec:identity}
\subsection{Two roles, one identity}
\label{sec:roles}
A single key cannot do both of the jobs a node needs done. The node key signs
constantly, from a box that runs other people's workloads and is, for third-party
nodes, outside our physical control. The bond must be movable only by someone we
still trust after that box is compromised. If they are the same key, compromise
of the node key also lets the attacker withdraw the bond, so the economic control
intended to price the compromise is unavailable in exactly the case it was posted
against.
Therefore:
\begin{description}
\item[Node key ($A_n$)] Lives with \code{hanzod}. Signs epoch statements and
machine records. \emph{Is the node identity.} Has no authority to move value.
\item[Owner key ($A_o$)] Lives with the operator, off the node. Registers the
node, posts and withdraws the bond, ends the validation, changes the reward
recipient. Never on the box.
\end{description}
This is one identity with an explicit controller, not two identities. $A_n$ is
what the fleet is enumerated by, what evidence is signed with, what work is
attributed to, and what rewards are paid to. $A_o$ appears once per lifecycle
event.
\emph{Observed:} the vendored validator manager sets
\code{\_posValidatorInfo[validationID].owner = \_msgSender()} at registration
(\code{luxfi/standard}, \code{contracts/validators/PoSValidatorManager.sol}:440)
and authorizes end-of-validation, reward withdrawal and recipient changes against
that one address (lines 196, 261, 303). There is no second address field. The
registration therefore knows $A_o$ and does not know $A_n$; supplying the binding
is \hyperref[sec:req]{IR-4}.
\begin{remark}
For a Hanzo-operated node, $A_o$ SHOULD be a \code{KindTreasury} or \code{KindMPC}
wallet --- threshold custody, no single holder --- and MUST NOT be a key present
on any node. \emph{Observed:} both custody kinds exist in \code{hanzoai/cloud},
\code{apps/wallet/custody.go}, and a \code{TierValidator} wallet tier already
exists in the same file.
\end{remark}
\subsection{Derivation}
$A_n$ is an EVM-runtime account address: the low 20 bytes of $\kec$ over the
uncompressed secp256k1 public key. \emph{Observed} in \code{hanzoai/cli},
\code{src/commands/wallet.rs} (\code{address\_from\_vk}), with the canonical path
\code{m/44\textquotesingle/60\textquotesingle/0\textquotesingle/0/0} for locally
derived keys.
The node key MUST be generated for the node and MUST NOT be derived from an
operator seed that also derives other nodes' keys. Derivation from a shared
mnemonic makes every node's key recoverable from one secret, which converts a
single custody failure into a fleet-wide one. Where a fleet seed is used for
operational reasons, each node's key MUST come from a distinct hardened account
index and the seed MUST NOT be present on any node.
\subsection{Why secp256k1, and not the ed25519 key that exists}
\label{sec:curve}
The choice is forced by where the identity has to be checked, not by taste.
\begin{enumerate}[label=(\roman*)]
\item \textbf{Forfeiture is verified on-chain.} An equivocation proof
(\S\ref{sec:equiv}) is two signature verifications inside a contract. The EVM
has \code{ecrecover} as a precompile at roughly 3000 gas; it has no ed25519
verifier. An ed25519 identity would make the one fault this design can prove
unprovable where it must be priced.
\item \textbf{Recovery removes a field.} \code{ecrecover} returns the address, so
a statement carries a signature and no public key, and a statement is
self-addressing: the identity is a function of the message and the signature.
With ed25519 the verifier needs the key alongside, and needs a mapping from key
to identity that is one more thing to get wrong.
\item \textbf{One key pays and signs.} The address that receives rewards is an
account address. Using a different curve for statements would reintroduce the
two-identifier reconciliation this proposal exists to remove.
\end{enumerate}
Signatures MUST be canonical: $s \le N/2$ where $N$ is the curve order (EIP-2),
and $v \in \{27, 28\}$. \emph{Observed:} the low-$S$ ceiling is already computed
and enforced in \code{hanzoai/cloud}, \code{apps/wallet/anchor.go}
(\code{secp256k1HalfN}), for the treasury anchor signer, so the rule is one the
platform already applies.
\subsection{The one object}
A node record is:
\[
\bigl(A_n,\; A_o,\; \text{chainID},\; L,\; W,\; \text{class},\;
\text{validationID},\; \text{org},\; \text{status}\bigr)
\]
and nothing else identifies a node anywhere in the system. \code{admin.hanzo.ai}
enumerates by $A_n$. The scheduler selects by $A_n$ and \code{class}. Settlement
pays $A_n$. The cluster names the node after $A_n$. The registry keys on $A_n$.
\emph{Observed gap:} \code{hanzo-attest}'s \code{Record} carries
\code{node: String} --- a free-form name --- and its \code{Signer} is an ed25519
public key (\code{evidence.rs}). Replacing both is \hyperref[sec:req]{IR-1}.
\emph{Relationship to HIP-0097.} HIP-0097 specifies \code{did:hanzo:<name>} as the
DID method for every Hanzo subject. $A_n$ is the node's key material and its
settlement address; a \code{did:hanzo:} name MAY resolve to a DID Document whose
verification method is $A_n$. Two names for one subject is acceptable only in that
direction --- the DID resolves to the address; the address is not derived from the
DID --- because settlement, admission and staking cannot depend on a resolver
being reachable.
% =====================================================================
\section{Custody}
\label{sec:custody}
\subsection{The ladder}
\label{sec:ladder}
Custody options differ in one measurable respect: what an attacker must obtain in
order to sign as $A_n$.
\begin{center}
\small
\begin{tabular}{@{}l>{\raggedright\arraybackslash}p{0.30\textwidth}>{\raggedright\arraybackslash}p{0.44\textwidth}@{}}
\toprule
& \textbf{Custody} & \textbf{To forge the identity, obtain} \\
\midrule
L0 & Plaintext file or environment variable & A file read as any user that can read it. \emph{This is the current state of \code{hanzod}.} \\
L1 & OS keychain, owner-only file & Code execution as the node's user. \\
L2 & Held by KMS/MPC; node never sees the key & A credential that authorizes KMS to sign --- which is a bearer token on the node. \\
L3 & Sealed to a platform: generated inside, non-extractable, released only under a policy over the measured state & Physical compromise of the platform, or a break in its root of trust. \\
\bottomrule
\end{tabular}
\end{center}
L2 is where the interesting mistake lives. Moving the key into KMS is a genuine
improvement --- the key is not on the box, so a stolen disk is not a stolen
identity, and every signature is centrally logged and revocable. But it does not
close the possession--platform gap; it \emph{relocates} it. The question ``which
machine holds the key'' becomes ``which principal can make KMS sign'', and on a
node that principal is authenticated by a credential that sits on the node. Under
L2 the identity is exactly as strong as the custody of that credential --- which,
absent hardware, is L0 or L1 again.
\begin{claim}[The recursion]\label{cl:recursion}
Software custody delegation does not increase assurance about \emph{location}. It
replaces custody of a signing key with custody of an authorization credential.
The chain terminates only at a key that some hardware refuses to export.
\end{claim}
This is not an argument against KMS. It is an argument for what KMS custody must
be combined with, and it produces the requirements below.
\subsection{How a node key comes into existence}
\label{sec:gen}
Generation is where the assurance class is decided, because a key cannot be moved
up the ladder afterwards: a key that once existed outside a platform has an
unknowable custody history, and no later ceremony makes it non-extractable.
Exactly one of three procedures applies, chosen by what the host has:
\begin{description}
\item[L2, the default.] The operator, authenticated by IAM, asks the wallet
service for a new wallet of kind \code{kms} or \code{mpc} in tier
\code{validator}. The key is generated inside KMS, sealed under the KMS envelope,
and never returned; the service returns $A_n$. \code{hanzod} is issued a signing
credential scoped by C3. \emph{Observed:} this is the existing
\code{/v1/wallet/*} path in \code{hanzoai/cloud}, \code{apps/wallet}, where a
secp256k1 key is generated, sealed, and every signature recovers to the wallet
address.
\item[L3, where the hardware exists.] The key is generated inside the enclave, is
not exportable, and its use is conditioned on the measured state by G1--G4. The
enclave's session keys are attested against the measured state, and the epoch
statement signed by $sk_n$ names those keys (\S\ref{sec:field}), which is what
lets a verifier record class \code{confidential} against this identity.
\item[L1, development only.] The key is generated locally and held in the OS
keychain. \emph{Observed:} \code{hanzo wallet}'s local custody, where ``the
private key / mnemonic lives in the OS keychain, never on disk, never printed''.
A node at L1 MUST be recorded at class \code{self} and MUST NOT be bonded on
mainnet.
\end{description}
Three procedures are forbidden, and each is a way an operator might reasonably
think they were being careful:
\begin{itemize}
\item Generating the key elsewhere and copying it to the node --- the key exists in
two places from the first minute, and \S\ref{sec:equiv} will eventually charge
for it.
\item Deriving it from a fleet mnemonic --- one secret then recovers every node
(\S\ref{sec:identity}).
\item Backing it up. A node key has no backup: the recovery path for a lost node
key is succession under $A_o$ (\S\ref{sec:succ}), which costs one transaction,
whereas a backup is a second copy that equivocates the day it is restored.
\end{itemize}
\subsection{Requirements}
\begin{enumerate}[label=\textbf{C\arabic*.},leftmargin=2.6em]
\item The node key MUST NOT exist in plaintext at rest anywhere: not in an
environment variable, not in a file, not in a container image, not in a
Kubernetes Secret that is not itself KMS-sealed.
\item The node key SHOULD be held at L2 --- generated inside KMS/MPC, never
returned --- with \code{hanzod} holding only $A_n$ and a signing capability.
\emph{Observed:} this is exactly the cloud-custody model already implemented in
\code{hanzo wallet} (``the private key / mnemonic lives in the OS keychain,
never on disk, never printed'' for local custody; server-side keys for cloud
custody) and in \code{cloud/apps/wallet/custody.go}, where \code{KindKMS} seals
a real secp256k1 key under the KMS envelope and every \code{Sign} recovers to
the wallet address.
\item Where the node key is at L2, the credential that authorizes signing MUST be
scoped to (a) this node's wallet only, (b) the epoch-statement and
machine-record message shapes only, and (c) a bounded rate. A node's signing
credential MUST NOT be able to sign an arbitrary transaction, because a
credential that can sign anything is the owner key by another name.
\item On a platform with a TPM 2.0, the credential in C3 MUST be sealed to a PCR
policy, so that a credential lifted from the disk is unusable on a different
platform or after a change to the boot chain.
\item On a platform with a hardware enclave (SEV-SNP, TDX, CCA), the key SHOULD be
generated inside the enclave and its release conditioned on the enclave
measurement, reaching L3.
\item Secrets MUST be stored only in Hanzo KMS. Where a digest suffices, the
digest is stored and the value is not. No node key, signing credential or
cluster token appears in a manifest, a repository or a report.
\item A signing operation MUST be refused when the node record's status is revoked.
Revocation that only stops scheduling leaves a compromised node able to make
statements the network still verifies.
\end{enumerate}
\emph{Observed} for the KMS envelope this relies on: secrets are sealed as
AES-256-GCM over a per-secret data key with the data key wrapped by ML-KEM
(\code{luxfi/kms}, \code{pkg/store/secrets.go}, \code{ModeStandard = "aead+mlkem"}),
the additional authenticated data binds path, name, environment and version, and
\code{pkg/store/crypto\_test.go:TestAADBindingPreventsSwap} demonstrates that
swapping one secret's wrapped data key into another fails. Transport is a ZAP
handshake combining X25519 and ML-KEM-768 under HKDF-SHA256
(\code{pkg/zap/handshake.go}).
\subsection{Release conditioned on the measured state}
At L3 the release policy is the point of the exercise, so it is specified rather
than left to configuration:
\begin{enumerate}[label=\textbf{G\arabic*.},leftmargin=2.6em]
\item The key material is generated inside the platform and is not exportable.
\item A report whose report-data field commits to the measured state and to keys
the epoch statement names (\S\ref{sec:field}) is the only evidence that reaches
the platform. Its freshness term MUST be one the node could not have chosen in
advance.
\item Release --- or use --- MUST be denied when the measured state is not on the
allowed list for the node's declared image. Denial MUST be the default on any
error, including an unreachable verifier.
\item The allowed list is a set of register values, published with the release,
and changed by a release of the image --- not by an operator edit on the node.
\S\ref{sec:threat} records the publisher as a trust anchor.
\end{enumerate}
\subsection{Assurance class}
\label{sec:class}
A node's assurance class is derived from evidence the network verified. It is a
field on the node record, it is visible in the console and to the scheduler, and a
node MUST NOT be able to set it.
\begin{center}
\small
\begin{tabular}{@{}l>{\raggedright\arraybackslash}p{0.22\textwidth}>{\raggedright\arraybackslash}p{0.50\textwidth}@{}}
\toprule
\textbf{Class} & \textbf{Root} & \textbf{Requires} \\
\midrule
\code{self} & the node's own key &
An epoch statement verifying to $A_n$. Trust in it is trust in the node operator.
\emph{Implementable today.} \\[3pt]
\code{platform} & TPM 2.0 &
A quote over PCRs committing to $A_n$ and a freshness input, \emph{and} an
enforced verified boot chain. Without enforcement the PCR values are
attacker-chosen and the class is \code{self}. \emph{Proposed.} \\[3pt]
\code{confidential} & SEV-SNP, TDX, CCA &
A vendor-rooted report, joined to $A_n$ by an epoch statement naming the session
keys the report attests (\S\ref{sec:field}); removes the host from the trust base.
\emph{Request path implemented, no hardware on the current fleet.} \\
\bottomrule
\end{tabular}
\end{center}
\emph{Observed} on the current fleet (Appendix~\ref{app:fleet}): both Linux hosts
have TPM 2.0 with a SHA-256 PCR bank and a firmware event log at
\code{/sys/kernel/security/tpm0/binary\_bios\_measurements}. They differ in one
respect that decides the class: the aarch64 host has SecureBoot enabled, the x86
host has it disabled. The x86 host also reports \code{sev\_snp = N}, so it cannot
reach \code{confidential} on the running configuration. The Apple-Silicon host
exposes no TPM and no confidential-VM facility to a Linux guest --- it is
\code{self}, and this is a property of the platform, not a deficiency to be worked
around.
\begin{remark}[Why the Mac is in the fleet anyway]
The microVM is the uniformity layer: it is what lets a Mac present the same k3s
node as a Linux box. Uniformity of \emph{interface} is not uniformity of
\emph{assurance}, and the design does not pretend otherwise. The Mac is a
first-class node at class \code{self}, and the scheduler is what decides which
workloads a \code{self} node may receive. One identity primitive, one enumerable
field for the difference.
\end{remark}
Given the hardware actually present, \emph{a TPM 2.0 attester and verifier is
worth more than SEV-SNP or TDX support}, because two of three hosts have the
hardware today and zero have SNP or TDX. That is \hyperref[sec:req]{IR-5}.
% =====================================================================
\section{Binding the key to the platform}
\label{sec:bind}
Both shipping confidential-computing platforms offer the same primitive: a guest
asks its firmware for a report over 64 caller-supplied bytes, and the report also
carries the platform's own measurement of how that guest was started. What goes
into those 64 bytes decides what the report is evidence of. This section states
what the implementation puts there, what that does and does not establish, and the
smallest change that makes a report evidence about a node rather than about an
image.
\subsection{What is implemented}
\emph{Observed}, \code{hanzoai/vm}, \code{crates/vm-measure}. A measurement is two
logs, each folded into a SHA-384 register by the extend rule that Intel TDX
applies to a runtime measurement register, so a register built by the launcher
reproduces an RTMR that started at zero and received the same events in the same
order. The two registers are then bound into the 64 bytes a hardware report
carries as its caller-supplied field:
\[
\beta \;=\; \sha{512}\bigl(L \,\|\, W\bigr).
\]
\code{attest::status} asks the platform for a report over $\beta$ when
\code{/dev/sev-guest} or \code{/dev/tdx\_guest} is present, and records the raw
report beside the measurement. The verifier's obligation is stated in the crate as
one line: the report's caller field must equal the measurement's bind.
The design is right in its choice of digest --- SHA-384 is what both TDX registers
and the SEV-SNP launch measurement use, so nothing is re-hashed at the boundary to
hardware --- and right in separating launch from workload, because the two are
extended by different programs at different times and map onto separate registers
on TDX.
\begin{remark}[What the launch register covers]
The launch log's events are the kernel, the command line, the initramfs where
present, the source root image and the shape --- what the hypervisor was handed.
The hypervisor itself is not among them. On a confidential VM that is correct: the
platform's own launch measurement covers the firmware and the VMM-supplied state,
so measuring the monitor from inside would duplicate a value the hardware already
signs. At class \code{self} nothing covers it, and a node that swaps its monitor
binary produces the same register. \code{hanzo-attest}'s encoding does include the
monitor's digest, which is the right choice for the class it serves. A single
measurement (IR-3) has to keep both behaviours: the monitor digest is measured
where no platform measurement exists, and omitted where one does.
\end{remark}
\subsection{Two properties $\beta$ does not have}
\begin{claim}[Transferability]\label{cl:transfer}
Let two nodes $h_1 \ne h_2$ run the same release, so
$(L_1, W_1) = (L_2, W_2)$ and therefore $\beta_1 = \beta_2$. A report obtained by
$h_1$ verifies, under the vendor root, as a report over $\beta_2$. A verifier
checking only ``the report's caller field equals the bind'' accepts $h_1$'s report
as evidence for $h_2$.
\end{claim}
\noindent The uniformity that makes the fleet operable is what makes the report
transferable: identical inputs are the goal, and identical inputs produce an
identical bind. The report does carry platform-specific material --- a chip
identifier on SEV-SNP, a TCB version --- but a verifier that does not bind those
to a node record is not using them, and there is no field in the node record for
them to bind to. Nothing in $\beta$ names the node.
\begin{claim}[Staleness]\label{cl:stale}
$\beta$ is a function of the release alone. A report over it is therefore valid
forever and replayable by anyone who obtains it once. Neither SEV-SNP nor TDX
places a timestamp in the report; freshness in both designs comes from the
caller-supplied field.
\end{claim}
Together these say that the implemented path, completed, would establish
\emph{that some platform of the right kind launched this image at some time} ---
which is worth having, and is not node identity.
\subsection{Who owns the field}
\label{sec:field}
The confidential-tier proposal in this repository
(\code{hip-confidential-tier.tex}) specifies the contents of the report-data field
and reaches Claims~\ref{cl:transfer} and~\ref{cl:stale} independently: ``a binding
over the boot log alone omits the keys the session runs on and omits any freshness
term, so the report is replayable by anyone holding a copy''. Its requirement B1
is
\[
\mathrm{rd} \;=\; \sha{512}\bigl(\text{``hanzo/rd/v1''} \,\|\, L \,\|\, W \,\|\,
pk_e \,\|\, pk_r \,\|\, \mathrm{sid} \,\|\, n_v \bigr),
\]
where $pk_e$ is the environment's key-exchange public key, $pk_r$ its receipt
verifying key, $\mathrm{sid}$ the session identifier, and $n_v$ a verifier nonce.
Its B3 requires $sk_e$ and $sk_r$ to be generated inside the environment and to
die with it.
There is one report-data field and it has one specification. This document does
not propose a second. What it observes is that B1 contains no durable identity:
$pk_e$ and $pk_r$ are ephemeral by construction, so a report under B1 establishes
that \emph{some} confidential environment holds \emph{these session keys}, and
nothing that ties that environment to the bonded, staked, paid node the rest of
this document is about. The bond, the reward and the fleet row hang off $A_n$; the
hardware evidence hangs off $pk_r$; today nothing joins the two.
\subsection{Joining identity to hardware}
\emph{Proposed.} The join is a signature, not a second field. The epoch statement
(\S\ref{sec:stmt}) carries the session terms, so that $sk_n$ endorses exactly the
keys the report attests:
\[
\text{report} \;\longrightarrow\; pk_r \;\longleftarrow\; \text{epoch statement}
\;\longrightarrow\; A_n .
\]
A verifier checks two links it can check separately: the report verifies under the
vendor root and its report-data equals B1 recomputed over $(L, W, pk_e, pk_r,
\mathrm{sid}, n_v)$; and the epoch statement naming those same $(pk_e, pk_r,
\mathrm{sid})$ recovers to $A_n$, which is bound to an active bonded registration.
\begin{claim}[What the composition establishes]\label{cl:bind}
Under B1 plus an epoch statement naming the same session terms: a platform
certified by $R$, launched in the recorded state, held the measurement log folding
to $(L,W)$ and the session keys $(pk_e, pk_r)$; and the holder of $sk_n$, bonded
under an active registration, endorsed those session keys for epoch $e$. Work
performed under $sk_r$ is therefore attributable to $A_n$, and $A_n$'s bond answers
for it.
\end{claim}
\begin{remark}[What it still does not establish]
$\Conf(sk_n, h)$ --- that the node key is on that platform. The composition binds
the identity to the \emph{session}, not the identity key to the \emph{silicon}: an
operator holding $sk_n$ off the box can endorse session keys held by a confidential
machine they also control. That is sufficient for settlement, which needs to know
whose bond answers for the work, and it is not the same statement as ``this node's
key lives in this enclave''. Reaching that requires generation inside the platform
and a non-extractability statement the verifier checks --- requirement (i) of
\S\ref{sec:gap} and requirement G1 --- and it would additionally require $A_n$ to
appear in the report-data preimage. Adding $A_n$ to B1 is therefore a strictly
stronger option, at the cost of one term in a field that already has four
claimants; it is offered as \hyperref[sec:req]{IR-2} for the confidential-tier
proposal to take or decline, not asserted here.
\end{remark}
Because $n_v$ is per-session and B2 fixes the report to a completed measurement
chain, a report is requested once per session rather than once per epoch. The
epoch statement is what carries a per-epoch freshness term for the identity
(\S\ref{sec:join}); the two freshness inputs sit at different layers and neither
substitutes for the other, which the confidential-tier proposal states for its own
pair of nonces and which holds here for the same reason.
% =====================================================================
\section{Joining}
\label{sec:join}
\subsection{Epochs come from the chain}
Let $H$ be the height of the settlement L1 and let $K$ be the epoch length in
blocks. The epoch is $e = \lfloor H / K \rfloor$.
Deriving the epoch from block height rather than wall-clock time removes clock
skew from the protocol: two verifiers agree on the current epoch because they
agree on the chain, and a node with a fast clock cannot publish into the future.
The block hash $B$ of any block in epoch $e$ is a value no node could have
predicted before that block, which is what makes the epoch statement fresh without
a round trip to a verifier. The hardware report has its own freshness term, a
verifier nonce, at its own layer (\S\ref{sec:field}).
$K$ MUST be chosen so that an epoch is minutes, not hours: short enough that a
node restarting with a new image is unavailable for a short time
(\S\ref{sec:equiv}), long enough that ordinary restarts do not dominate statement
volume.
\subsection{The epoch statement}
\label{sec:stmt}
A node publishes one statement per epoch. \emph{Proposed}, normatively:
\begin{center}
\small
\begin{tabular}{@{}llp{0.42\textwidth}@{}}
\toprule
\textbf{Field} & \textbf{Bytes} & \textbf{Meaning} \\
\midrule
tag & \code{field(hanzo.node.epoch.v1)} & Domain separation. \\
chainID & \code{u64} big-endian & The settlement L1. Mainnet 36963. \\
node & 20 & $A_n$. \\
epoch & \code{u64} big-endian & $e$. \\
block & 32 & $B$, a block hash in epoch $e$. \\
launch & 48 & $L$ of the node microVM. \\
workload & 48 & $W$ of the node microVM. \\
session & \code{field(\dots)} & $pk_e \| pk_r \| \mathrm{sid}$, or empty at class \code{self}. \\
evidence & \code{field(\dots)} & The raw hardware report, or empty. \\
\bottomrule
\end{tabular}
\end{center}
\noindent where \code{field(x)} is the length of \code{x} as a big-endian
\code{u64} followed by \code{x} --- the encoding \code{hanzo-attest} and
\code{vm-measure} already use, so that no two distinct statements encode to the
same bytes. The signature is over $\kec$ of the encoding:
\[
\sigma \;=\; \Sign^{\mathrm{rec}}_{sk_n}\bigl(\kec(S)\bigr), \qquad
A_n \;=\; \mathtt{ecrecover}\bigl(\kec(S), \sigma\bigr).
\]
Six remarks, none decorative:
\begin{description}
\item[The statement carries no public key.] The address is recovered from the
signature (\S\ref{sec:curve}), so the statement is self-addressing and there is
no key-to-identity mapping to keep consistent.
\item[chainID] prevents a statement made for testnet from being replayed on
mainnet. \emph{Observed:} the sovereign L1 uses $\text{networkID} =
\text{chainID}$, mainnet 36963, testnet 36962, devnet 36964, localnet 1337
(\code{hanzoai/cli}, \code{src/commands/network.rs}).
\item[node] appears in the preimage as well as being recoverable, so that a
statement cannot be filed under a different registration even if the recovery is
performed loosely by some reader.
\item[epoch and block] make the statement unique to an epoch and unforgeable
ahead of it. Uniqueness is what makes equivocation provable
(\S\ref{sec:equiv}); the block hash is what makes the statement fresh, so that a
node cannot pre-sign a year of epochs.
\item[launch and workload] are the registers of the \emph{node microVM} --- the
one that joins the cluster --- not of a tenant machine. The epoch statement is
distinct from the per-machine record: a node serves many machines and files many
records; it has exactly one node microVM and files exactly one epoch statement
per epoch. Conflating them would make honest multi-tenancy look like
equivocation. $L$ is fixed at launch. $W$ is the workload register \emph{as of
the moment the statement is made}, and an extension after that moment changes
$W$ without producing a new statement --- which is the time-of-check to
time-of-use residue, stated again here because this is the field where a reader
is most likely to assume otherwise. A node MUST NOT publish a second statement to
report the new $W$; it waits for the next epoch, for the reason in
\S\ref{sec:equiv}.
\item[session] carries the environment's session keys and session identifier, so
that the signature under $sk_n$ endorses exactly the keys the hardware report
attests (\S\ref{sec:field}). Empty at class \code{self}, where there is no
report and no environment to hold keys.
\item[evidence] carries the report and not a class. A node states what it can
show; the verifier derives the class. A statement that asserted its own class
would be a node setting a field it MUST NOT set (\S\ref{sec:class}).
\end{description}
\emph{Observed gap:} the preimage implemented in \code{hanzo-attest} is
$(\text{tag}, \text{kind}, \text{measurement}, \text{timestamp})$
(\code{evidence.rs}, \code{preimage}). It carries no chain, no address and no
epoch, so it is presentable under any registration on any chain and equivocation
over it is undefined; its timestamp is the node's own clock. Replacing it is
\hyperref[sec:req]{IR-3}.
\subsection{What the network verifies}
On receiving a join or an epoch statement, the verifier MUST, in order, and
refusing at the first failure:
\begin{enumerate}[label=\textbf{V\arabic*.},leftmargin=2.6em]
\item \textbf{Shape.} $S$ parses; \code{chainID} is this chain; $e$ is the current
epoch or the immediately previous one; $\sigma$ is canonical ($s \le N/2$).
\item \textbf{Signature.} $\mathtt{ecrecover}(\kec(S), \sigma)$ equals the
\code{node} field.
\item \textbf{Freshness.} $B$ is the hash of a block this verifier knows, at a
height inside epoch $e$.
\item \textbf{Registration.} $A_n$ is bound to an active \code{validationID} whose
bond meets the minimum, the binding was written by $A_o$, and the record is not
revoked.
\item \textbf{Uniqueness.} No other statement exists for $(\text{chainID}, A_n, e)$.
A second, differing statement is not an error to discard --- it is evidence to
keep (\S\ref{sec:equiv}).
\item \textbf{Image.} $(L, W)$ is on the allowed list for the release the node
claims. A node running an unpublished image is admitted at class \code{self}
with no image assertion, or refused, according to network policy; it MUST NOT be
admitted as though it were running a published image.
\item \textbf{Class.} Any hardware evidence verifies under the vendor root, and its
report-data field equals the confidential-tier proposal's B1 recomputed over
$(L, W)$ from this statement and the session terms in this statement's
\code{session} field. A report whose session terms are not the ones $sk_n$
signed is a report about a different environment. The class recorded is the
highest class \emph{proved}, defaulting to \code{self}.
\item \textbf{Rate.} At most one accepted statement per $(A_n, e)$ enters the
record, and unverified statements are dropped before signature verification
exceeds a per-source budget. Signature verification is the expensive step and
MUST NOT be reachable in unbounded volume by an unregistered source.
\end{enumerate}
Every failure is a refusal. A verifier that cannot reach the vendor root records
class \code{self}; it MUST NOT record a higher class on an unverified claim, and
it MUST NOT refuse the join for that reason alone --- the two are different
decisions, and conflating them turns an availability failure into a fleet outage.
\subsection{What the network has after V1--V8}
Precisely: that the holder of $sk_n$, bound to an active bonded registration,
asserted image $(L, W)$ for epoch $e$ on this chain, within the freshness window
that $B$ defines; and that at class \code{platform} or above, a vendor-rooted
platform reported that state together with this address. Nothing in
\S\ref{sec:notprove} that is not covered by class \code{platform} or above becomes
true because V1--V8 passed.
\subsection{Admission to the cluster}
\label{sec:admit}
The chain says who may join. The cluster has to enforce it, and today it does not:
membership is granted by a shared bearer token (\S\ref{sec:three}), which is a
capability, not an identity, and which every agent holds in full.
\emph{Observed:} the mechanism for doing this correctly is already present and
unused. k3s issues Kubernetes bootstrap tokens --- \code{k3s token create} takes
\code{--ttl}, \code{--groups} and \code{--usages} --- and the cluster carries the
standard \code{system:node} role binding. What is missing is not a protocol. It is
the decision of whom to issue a token to, and the retirement of the shared secret
that makes the decision unnecessary today.
\emph{Proposed.} No new authentication is invented; the existing flow is driven by
the chain.
\begin{enumerate}[label=\textbf{A\arabic*.},leftmargin=2.6em]
\item The node's name in the cluster is derived from its address:
\code{n-}\textit{40 hex characters}, lowercase, 42 characters --- a valid
DNS-1123 label. One name, derived, not chosen.
\item \code{hanzod} presents its epoch statement to an admission service on the
control plane. The service performs V1--V8 and, on success, creates a bootstrap
token with a short \code{--ttl} --- minutes, against the 24-hour default --- for
that node's join and no other. The service holds the server token; the node
never sees it.
\item The kubelet uses the bootstrap token for TLS bootstrap only: it obtains a
client certificate with subject \code{system:node:n-\dots} in group
\code{system:nodes}, and the bootstrap token is then spent or expires.
\item The API server runs the Node authorizer and the \code{NodeRestriction}
admission plugin, so that a node's certificate authorizes it to read and write
its own \code{Node} object and the objects scheduled to it, and nothing else.
\item The k3s server token is a cluster secret, held in KMS, delivered to the
control plane and to no agent. \code{K3S\_TOKEN} on an agent is retired, and the
server token is rotated once it is (\code{k3s token rotate}) because every agent
has held it.
\item A revoked node's certificate is not renewed and its \code{Node} object is
cordoned and drained. Certificate lifetime bounds how long a revoked node remains
a member; it MUST be short relative to the unbonding period.
\end{enumerate}
This closes the loop the wallet-as-identity design opens: the address is not only
the name in the console and the account on the chain, it is what the cluster
admits. Everything above it --- workloads, scheduling, settlement --- then refers
to one identifier. It is \hyperref[sec:req]{IR-6}.
% =====================================================================
\section{Stake and forfeiture}
\label{sec:stake}
\subsection{What exists}
\emph{Observed}, in \code{luxfi/standard}, \code{contracts/validators/} --- the
vendored ACP-77 validator manager, renamed to Network/L1 nomenclature with the ABI
preserved:
\begin{itemize}
\item \code{initializeValidatorRegistration} is \code{payable}. The native value
sent stays in the manager and is the bond; \code{\_lock} records the amount and
\code{\_unlock} returns it with \code{payable(to).sendValue(value)}
(\code{NativeTokenStakingManager.sol}).
\item The registration is keyed by \code{validationID} (\code{bytes32}) and
records \code{owner = \_msgSender()}, a delegation fee, a minimum stake
duration, and an uptime counter (\code{PoSValidatorManager.sol}:440).
\item \code{submitUptimeProof} and the end-of-validation path condition rewards on
uptime; below the threshold, rewards are forfeit and the bond is still returned.
\item The registration input carries \code{nodeID} (\code{bytes}) and
\code{blsPublicKey}. The contract checks the BLS key's \emph{length} and that
\code{nodeID} is non-empty and not already registered
(\code{ValidatorManager.sol}). It does not verify possession of either key.
\item \textbf{There is no slashing.} A case-insensitive search for \code{slash}
across \code{contracts/validators/} returns nothing.
\end{itemize}
So the economic instrument that exists today is uptime-conditioned reward
forfeiture plus bond return. Any statement that a bad attestation ``slashes the
stake'' is, as of this writing, a proposal. This document treats it as one.
\subsection{Registration binds the identity}
\emph{Proposed.} A separate, minimal registry contract --- not a fork of the
vendored manager, whose ABI is preserved deliberately for in-place upgradability:
\begin{quote}\ttfamily
mapping(bytes32 validationID => address node) nodes;\\
mapping(address node => bytes32 validationID) registrations;\\
\\
function bind(bytes32 validationID, address node) external;\\
event Bound(bytes32 indexed validationID, address indexed node);
\end{quote}
\code{bind} is callable only by the \code{owner} of \code{validationID} as the
manager reports it; it refuses a node address bound to any registration, live or
retired (\S\ref{sec:decom}); and it refuses to rebind a live registration except
by the succession path of \S\ref{sec:succ}. One mapping pair, one function, one
event. Everything else --- weight, churn, uptime, delegation --- stays where it
already works.
\code{bind} SHOULD require a proof of possession of $A_n$: a signature by $sk_n$
over $(\text{chainID}, \text{validationID}, A_o)$, verified with
\code{ecrecover}. Without it, an operator can bind an address they do not hold,
which does not steal anything --- rewards pay an address the operator chose to
name --- but does let them file a registration against someone else's identity and
so pollute the injectivity the fleet view depends on.
\subsection{Forfeiture for self-proving faults}
\label{sec:forfeit}
\emph{Proposed.} On-chain forfeiture applies to faults that are
\emph{self-proving}: a verifier can establish them from the submitted evidence
alone, in constant time, without a vote, an oracle, or a trusted reporter. This
restriction is the whole design, and it is what keeps the mechanism from becoming
a governance surface.
\begin{quote}\ttfamily
function forfeit(bytes32 validationID, bytes calldata proof) external;\\
event Forfeited(bytes32 indexed validationID, uint256 amount, bytes32 fault);
\end{quote}
The contract verifies \code{proof} itself and burns or redistributes the bond
according to the network's reward policy. Anyone may submit; a submitter reward
paid from the forfeited amount is what makes detection someone's job. A
\code{validationID} already forfeited MUST reject a second submission, and a proof
whose epoch is older than the unbonding period MUST be refused, so that a bond can
eventually settle.
Faults that are \emph{not} self-proving --- poor service, wrong answers,
unavailability, a false inventory claim --- are handled by the mechanisms that
already exist for them: uptime-conditioned reward forfeiture in the manager, QoS
challenges (HIP-0095), and delisting by the scheduler. They MUST NOT be routed
through \code{forfeit}, because each would require a trusted reporter, and a
trusted reporter that can burn a stranger's bond is a larger risk than the
behavior it polices.
\subsection{Equivocation}
\label{sec:equiv}
\begin{definition}[Equivocation]
Two epoch statements $S_1, S_2$ with the same $(\text{chainID}, A_n, e)$,
differing in any other field, both recovering to $A_n$.
\end{definition}
The proof submitted to \code{forfeit} is $(S_1 \| \sigma_1 \| S_2 \| \sigma_2)$.
The contract computes two Keccak digests and two \code{ecrecover} calls, checks
that both recover to the node bound to \code{validationID}, that the triples
agree, and that the statements differ. That is constant work, roughly two
precompile calls, and it needs nothing off-chain.
\begin{claim}
Equivocation is publicly verifiable in two signature verifications and one
inequality, and an honest node running one node microVM cannot produce it.
\end{claim}
\paragraph{No supersede.} A node that restarts with a different image mid-epoch
MUST NOT publish a second statement; it waits for epoch $e+1$. An earlier draft of
this design permitted a \emph{supersede} statement naming the prior statement's
digest, to spare the node that wait. That is unsound: two machines holding the
same key can chain supersede statements at each other and never equivocate, which
removes the only fault the design can prove. The wait is the price of a detector
that cannot be talked out of firing, and $K$ is chosen (\S\ref{sec:join}) to make
the price small.
\paragraph{What it catches.} Uncoordinated duplicate use of the key: a backup
restored onto a second box, a stolen key run in parallel with the honest node, a
misconfigured redeployment. In each case two parties publish for the same epoch
without agreeing, the statements differ, and either party's statement together with
the other's is the proof. Detection requires no monitoring: the statements are the
evidence, and both are public.
\paragraph{What it does not catch.} A single adversary running $N$ coordinated
copies that agree to publish exactly one statement per epoch. Equivocation is
silent there, and this must be stated rather than assumed away. What bounds the
damage is the reward design rather than the detector:
\begin{claim}[A stolen node key does not move money]\label{cl:nomoney}
Rewards pay $A_n$, and the reward recipient is changed only by $A_o$
(\S\ref{sec:reward}). A party holding $sk_n$ and not $sk_o$ can make statements
attributed to the node and cannot redirect a payment. Coordinated copies therefore
earn into the honest operator's address.
\end{claim}
\noindent The residual gain to a coordinated adversary is the ability to serve
work under the node's identity --- which is bounded by the work being real work,
priced by HIP-0096 --- and to consume the node's standing, for which the remedy is
succession (\S\ref{sec:succ}). The custody requirements of \S\ref{sec:custody}
exist because this residue cannot be removed by a detector.
% =====================================================================
\section{Reward}
\label{sec:reward}
Rewards pay $A_n$ --- the identity address --- from two independent sources:
\begin{description}
\item[Validation rewards] From the validator manager, conditioned on uptime.
\emph{Observed:} the manager keeps a \code{\_rewardRecipients} mapping that
\emph{defaults to} \code{owner} (\code{PoSValidatorManager.sol}) and can be
pointed elsewhere by \code{changeValidatorRewardRecipient}.
\item[Work rewards] From compute settlement (HIP-0096), for AI work the node
actually served.
\end{description}
Two consequences follow from the observed divergence, and both must be stated
rather than assumed away:
\begin{enumerate}[label=\textbf{R\arabic*.},leftmargin=2.6em]
\item The reward recipient MUST default to the identity address. Divergence is
permitted --- an operator consolidating payouts is a legitimate need --- but it
is an explicit on-chain act by $A_o$, visible in the fleet view next to the node
it applies to.
\item Accounting, attribution and enumeration key on $A_n$, \textbf{never} on the
recipient. A recipient is where money goes; it is not who did the work, it is
not unique to a node, and treating it as an identifier silently merges distinct
nodes into one row.
\end{enumerate}
Paying the address the node signs with means ``which node earned this'' and ``which
node said this'' resolve through the same lookup. That is the practical content of
using one primitive for attestation, staking, payment and enumeration instead of
four identifiers that have to be reconciled. R1 is also what gives
Claim~\ref{cl:nomoney} its force: the money follows an address the node key cannot
change.
% =====================================================================
\section{Enumeration}
\label{sec:enum}
\code{admin.hanzo.ai} lists the fleet by $A_n$: one row per address, carrying
class, operator org, bond, validationID, current $(L, W)$, last epoch seen, reward
recipient where it diverges from $A_n$, and status. The surface is HIP-1325's
\code{/v1/node}; this document adds only the requirement that the row key is the
address and that no other identifier is offered as one.
Two properties of the view follow from earlier sections and belong in the
interface rather than in a footnote:
\begin{itemize}
\item \textbf{The count is a count of addresses, not of machines.} By
\S\ref{sec:gap} the two coincide only where custody makes the key unique to a
machine. A console that reports ``$n$ nodes'' is reporting $n$ registered
identities. Where class is \code{self} for most rows, that distinction is the
honest reading and the view SHOULD present class beside the count rather than
behind a detail pane.
\item \textbf{Recipient divergence is a visible fact.} R1 permits it; the view
shows it on the row, because a payout address that differs from the identity is
the one place where the single-primitive design admits a second address, and an
invisible second address is how reconciliation errors start.
\end{itemize}
% =====================================================================
\section{Lifecycle}
\label{sec:lifecycle}
\subsection{Rotation is succession}
\label{sec:succ}
An EVM address is a hash of the public key. There is no operation that changes the
key and keeps the address. A design that wanted stable identifiers across key
changes would need a level of indirection, and indirection here would be a
mistake: after a compromise, carrying the identifier forward carries the
compromised node's standing forward with it.
So rotation is \textbf{succession}: a new key, a new address, an explicit link.
\begin{enumerate}[label=\textbf{S\arabic*.},leftmargin=2.6em]
\item The operator generates $sk_n'$ at the target assurance class and obtains
$A_n'$.
\item $A_o$ calls \code{bind} to move the registration's node address from $A_n$ to
$A_n'$, emitting \code{Bound}. The bond does not move; the registration does not
end; no unbonding period is incurred. The prior binding is retained as retired,
so $A_n$ can never be bound again.
\item The outgoing key SHOULD sign a statement naming $A_n'$, which lets an
observer follow the chain without consulting the registry. It is corroboration,
not authority: the authority for succession is $A_o$, precisely because the case
that matters most is the one where $sk_n$ is in someone else's hands.
\item The node's cluster identity changes with its address: a new name, a new
bootstrap token, a new client certificate (\S\ref{sec:admit}). The old \code{Node}
object is drained and deleted.
\item Assurance class does not transfer. The successor is measured on what it can
prove.
\item Accrued reputation and service history transfer only when the succession is
\emph{planned} --- a hardware refresh or a key-hygiene rotation. After a
suspected compromise they MUST NOT transfer.
\end{enumerate}
Because $A_o$ is the authority, succession works when the node key is compromised,
which is the case it exists for. This is the concrete reason for the two-key split
in \S\ref{sec:roles}: with one key there is no step S2 an honest operator can take
that the attacker cannot.
\subsection{Revocation}
Revocation is immediate and off-chain-effective:
\begin{itemize}
\item $A_o$, or the network on a proven fault, marks $A_n$ revoked.
\item The scheduler stops placing work on the node at once. Running work is drained
under the workload's own policy.
\item Statements signed by $A_n$ after the revocation height are refused (V4), and
the node's KMS signing credential is revoked (C7), so the node cannot produce
them either.
\item The node's cluster certificate is not renewed, and its \code{Node} object is
cordoned and drained.
\item The bond does \textbf{not} unlock at revocation. It unlocks after the
unbonding period, so that a node cannot escape a forfeiture that is still being
submitted. Making revocation fast and unbonding slow is the whole point of
separating them.
\end{itemize}
\subsection{Decommission}
\label{sec:decom}
The orderly path, for a node leaving without fault: drain, stop accepting work,
publish a final epoch statement, and let $A_o$ call the manager's existing
end-of-validation path. Uptime is settled, rewards pay out, the bond returns to
$A_o$ after the unbonding period, and the record is retired.
$A_n$ MUST NOT be reused, and the registry retains the retired binding permanently
so that \code{bind} can enforce it. A retired address has an unknowable custody
history: whoever held the key still holds it, and nothing distinguishes a returning
node from an old copy.
\subsection{Compromise}
There is no procedure that un-leaks a key. What the procedure can do is stop the
attacker from profiting and stop the network from believing the wrong things.
Assume $sk_n$ is disclosed at unknown height $H_c$:
\begin{enumerate}[label=\textbf{K\arabic*.},leftmargin=2.6em]
\item \textbf{Revoke immediately} (previous section). Scheduling stops, cluster
membership lapses, the bond stays locked.
\item \textbf{Do not use the node key.} It is the attacker's key too. Every
recovery action goes through $A_o$.
\item \textbf{Treat statements as unattributable from $H_c$.} Not false ---
unattributable. Work attributed to $A_n$ after $H_c$ cannot be assigned to the
operator or the attacker on the evidence available.
\item \textbf{Expect equivocation, and let it settle.} If the attacker runs the key
while the honest node runs, the two statements collide and the bond is forfeit.
That is the mechanism working: the bond is what the operator posted against
exactly this.
\item \textbf{Succeed to $A_n'$} at a class at least as high as the one that failed.
If the failure was custody, the successor MUST be at a strictly higher class, or
the same failure is available again.
\item \textbf{Rotate the credential, not only the key.} At L2 the compromise may be
of the signing \emph{authorization}, not the key --- see
Claim~\ref{cl:recursion}. Revoke the node's KMS credential, and check the KMS
audit log for signatures in $[H_c, \text{now}]$ that the node cannot account for.
\end{enumerate}
\begin{remark}
If $A_o$ is compromised, this is not a node incident. The bond is movable and the
registration is controllable by the attacker. Recovery is at the operator level:
threshold custody for $A_o$ is what makes the event survivable, which is why
\S\ref{sec:roles} requires it rather than suggesting it.
\end{remark}
% =====================================================================
\section{Hanzo-operated and third-party nodes}
\label{sec:same}
They are the same object. A Hanzo node is a node whose owner key Hanzo holds.
There is no Hanzo-only field, no privileged registration path, no separate
enumeration, and no code path that branches on who operates the box. The console
lists them in one table; the scheduler selects from one pool; settlement pays them
through one mechanism; the cluster admits them by the same certificate flow.
Three differences remain. Each is a value on the record, and none is a difference
in the identity:
\begin{enumerate}[label=\textbf{D\arabic*.},leftmargin=2.6em]
\item \textbf{Who can authorize signing.} For a Hanzo node, $A_o$ and the node's
KMS credential live in Hanzo's KMS under Hanzo's IAM org; for a third party, in
theirs. This changes who can cause a signature, not what the signature means.
Both are subject to \S\ref{sec:custody} identically.
\item \textbf{Physical control, and therefore who the adversary is.} For a
third-party node, the operator is a possible adversary, and \S\ref{sec:att},
point~(d), applies at full strength: without a confidential VM, guest measurement
does not defend against the operator. For a Hanzo node the host is under our
control, which is a real assurance difference and is \emph{not} expressible as a
cryptographic proof to a third party. It is recorded as the operator org on the
node record and priced by policy, not asserted as a security property.
\item \textbf{Whose money is at risk.} Hanzo's own nodes are bonded by Hanzo, so a
forfeiture is an internal transfer rather than a loss to a counterparty. The
incentive differs; the mechanism does not.
\end{enumerate}
Scheduling policy MAY restrict workloads by assurance class and by operator org ---
for instance, requiring \code{confidential} for a tenant's sensitive inference, or
restricting a regulated workload to a named org. That is a constraint over fields,
evaluated by the scheduler, and it is the one place the difference is allowed to
appear. It is not a second identity model.
\begin{remark}
No field marks a node as trusted for skipping a verification step, and none should
be added. Such a field is a target: whoever obtains it obtains the assurance the
network attaches to our own hardware, without the hardware. Hanzo nodes reach their
class through the same evidence as everyone else, which is cheap for us because we
control the hosts and can enable SecureBoot on them.
\end{remark}
% =====================================================================
\section{Threat model}
\label{sec:threat}
Assets: the node key and the credential that authorizes it to sign; the bond; the
reward stream; cluster membership; tenant workloads and their data; the integrity
of the fleet view.
\begin{center}
\small
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.19\textwidth}>{\raggedright\arraybackslash}p{0.36\textwidth}>{\raggedright\arraybackslash}p{0.37\textwidth}@{}}
\toprule
\textbf{Adversary} & \textbf{Capability assumed} & \textbf{What this design does} \\
\midrule
Remote attacker &
Network access; no code on the node &
Statements are signed and epoch-bound, so replay across chains, epochs and
registrations fails (V1--V3). Cluster admission needs a token issued against a
verified statement (A2). \\[3pt]
Malicious tenant &
Code inside a machine on the node &
Out of scope for identity; the node key is not in a tenant machine, and C3 bounds
what a leaked signing credential can sign. \\[3pt]
Careless operator &
Copies a key, restores a backup onto a second box &
Equivocation forfeits the bond, with no monitoring required (\S\ref{sec:equiv}). \\[3pt]
Malicious node operator &
Full physical control of the host, root in the microVM &
At class \code{self}, only the bond and HIP-0095 constrain them; \S\ref{sec:att}(d)
is why. At \code{confidential} the host leaves the trust base. This is the
adversary the class field exists for. \\[3pt]
Thief of the node key &
Holds $sk_n$, not $sk_o$ &
Cannot redirect rewards (Claim~\ref{cl:nomoney}); is detected if they run in
parallel with the honest node; is evicted by succession under $A_o$. \\[3pt]
Thief of the owner key &
Holds $sk_o$ &
Not a node incident. The registration and bond are lost; threshold custody for
$A_o$ is the control (\S\ref{sec:roles}). \\[3pt]
Release publisher &
Chooses the allowed list of register values &
A trust anchor. A compromised publisher can allow an image of its choosing on
every node at once. The allowed list MUST be published under threshold control,
never a single key, and changes MUST be observable. \\[3pt]
Control-plane operator &
Issues bootstrap tokens &
Can admit a machine to the cluster. Admission is bounded by V1--V8 being performed
against the chain, so an issued token names a node the chain already accepts; the
control plane cannot invent a bonded identity. \\[3pt]
Chain adversary &
Reorgs, censors &
Epochs and freshness derive from the chain, so a deep reorg moves the epoch
boundary. $K$ MUST exceed the chain's finality depth; a statement referring to a
block that did not survive is refused at V3, not accepted with a warning. \\
\bottomrule
\end{tabular}
\end{center}
% =====================================================================
\section{Residual risk}
What this design does not fix, stated plainly so nobody has to discover it later:
\begin{itemize}
\item \textbf{Class \code{self} is an honesty assumption.} Most of the fleet is at
\code{self} today. For those nodes, every property in \S\ref{sec:notprove}
stands, and the only controls are the bond and the challenge system. That is a
real security posture --- economic rather than cryptographic --- and it must be
described that way in anything a customer reads.
\item \textbf{Time-of-check to time-of-use.} A register describes a launch. A node
measured honestly at height $H$ can \code{kexec}, load a module, or write its
root filesystem at $H + 1$. Continuous attestation --- runtime measurement of
what is loaded, not only what was --- is out of scope here and is the natural
next specification. The workload register is where it would attach.
\item \textbf{The bond bounds the loss, not the damage.} A node that leaks a
tenant's data loses its bond; the tenant's data is still out. Confidentiality of
tenant workloads on non-confidential VMs is a scheduling policy question, not an
identity question.
\item \textbf{Sybil resistance is economic.} Nothing prevents one operator from
running many nodes, each correctly identified. That is a feature; it becomes a
problem only where a protocol counts nodes rather than weighing stake. No
protocol specified here counts nodes.
\item \textbf{Vendor roots are trust anchors.} At class \code{platform} and above
the verifier trusts a manufacturer's certificate chain and its revocation
behavior. That is a smaller assumption than trusting every node operator, and it
is not zero.
\item \textbf{The registration does not prove possession of the consensus key.}
\emph{Observed:} the manager checks the BLS public key's length and the
\code{nodeID}'s uniqueness, and verifies possession of neither. Uniqueness
prevents two registrations of one \code{nodeID} on the same manager; it does not
establish that the registrant holds the corresponding secrets. The primitive
already exists (\code{luxfi/keys}, \code{pop.go}, \code{ProofOfPossession}).
\item \textbf{A coordinated operator can run copies undetected.} \S\ref{sec:equiv}
says exactly what the detector does and does not see, and
Claim~\ref{cl:nomoney} bounds the gain. It does not remove it.
\end{itemize}
% =====================================================================
\section{Implementation status and interface requests}
\label{sec:status}
\subsection{Implemented}
\begin{center}
\small
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{0.28\textwidth}>{\raggedright\arraybackslash}p{0.64\textwidth}@{}}
\toprule
\textbf{Where} & \textbf{What} \\
\midrule
\code{hanzoai/vm},\newline \code{crates/vm-measure} &
SHA-384 event log and register fold matching the TDX RTMR extend rule; launch and
workload logs kept apart; \code{bind} as $\sha{512}(L \| W)$; SEV-SNP and TDX
platform detection, request ABIs and ioctl encodings, with tests pinning structure
sizes against the kernel headers. The launch log is built in
\code{crates/vm-cli/src/measure.rs}. Not exposed by the \code{hanzo-vm} 2.0.0
binary installed on the fleet. \\[3pt]
\code{hanzoai/node},\newline \code{hanzo-libs/hanzo-attest} &
A second measurement: BLAKE3 over a \code{Boot} struct; \code{Evidence} with a
domain-separated \code{Kind}; an ed25519 \code{Kind::Host} attester and verifier;
\code{Record} with \code{coherent()}; an in-memory registry. \\[3pt]
\code{hanzoai/cli},\newline \code{src/commands/wallet.rs} &
Two custody models, no plaintext secrets: cloud custody where the key never leaves
KMS/MPC, and local custody in the OS keychain. EVM address derivation at
\code{m/44\textquotesingle/60\textquotesingle/0\textquotesingle/0/0}. \\[3pt]
\code{hanzoai/cloud},\newline \code{apps/wallet} &
\code{/v1/wallet/*} with four custody backends (\code{kms}, \code{mpc},
\code{treasury}, \code{safe}), key rotation, audit emission, a \code{TierValidator}
tier, EIP-2 low-$S$ normalization, and fail-closed behavior when the MPC cluster
is absent. \\[3pt]
\code{luxfi/kms} &
AES-256-GCM with an ML-KEM-wrapped data key, AAD bound to path/name/environment/version,
with a test that a swapped data key fails; hybrid X25519 + ML-KEM-768 transport. \\[3pt]
\code{luxfi/standard},\newline \code{contracts/validators} &
ACP-77 validator manager: bonded registration, \code{owner}, uptime-conditioned
rewards, churn limits, end-of-validation. No slashing. \\[3pt]
\code{hanzoai/cloud},\newline \code{cli/link.go} &
\code{hanzo link}: starts \code{hanzod} via \code{hanzo node up} and registers the
host in the org's compute fleet, authorized by the IAM token. \\
\bottomrule
\end{tabular}
\end{center}
\subsection{Interface requests}
\label{sec:req}
These are changes in code owned elsewhere. They are requested here, with the reason
and the minimal shape, and are not made by this document.
\begin{description}
\item[IR-1 --- \code{hanzod}: the identity key is the wallet key, and is never
plaintext.] \code{hanzo-bin/hanzo-node/src/utils/keys.rs} loads an ed25519 secret
from a plaintext \code{KEY=value} file or the environment;
\code{src/runner.rs}:342--352 writes it back with \code{fs::write}, at the umask.
Requested: the node identity becomes a secp256k1 key whose address is $A_n$,
obtained through the wallet custody path that already exists, so that
\code{hanzod} holds an address and a signing capability rather than a secret.
Rationale: \S\ref{sec:custody} C1--C2 and \S\ref{sec:curve}; with a copyable key
none of the properties in this document hold, and with an ed25519 key the
forfeiture in \S\ref{sec:forfeit} cannot be verified on-chain. If the change must
land in two steps, the first step is the file mode and the second is the custody.
\item[IR-2 --- the report-data preimage MAY carry the node address.] The
confidential-tier proposal's B1 binds the boot registers, the ephemeral session
keys, the session identifier and a verifier nonce. It carries no durable
identity, so a report under it establishes that some confidential environment
holds these session keys and nothing that reaches the bonded node
(\S\ref{sec:field}). The composition of \S\ref{sec:field} closes this without
touching the field, and is what this document specifies. Offered, not asserted:
adding $A_n$ as a term of B1 would make the platform itself report the identity,
which is strictly stronger and costs one term. The decision belongs to the
confidential-tier proposal, which owns that field; this document's requirement is
only that the epoch statement name the same session terms the report does.
\item[IR-3 --- one measurement, not two.] \code{hanzo-attest} folds a \code{Boot}
struct with BLAKE3; \code{vm-measure} folds an event log with SHA-384. They
measure the same kernel and the same root image to different numbers, and only
the second matches what TDX and SEV-SNP registers use. Requested:
\code{hanzo-attest} stops defining a measurement and signs \code{vm-measure}'s
registers, with the epoch-statement preimage of \S\ref{sec:stmt} replacing
\code{evidence::preimage}. A release manifest MUST publish the digest function
the measurement uses; today the monitor's local cache records SHA-384 while
\code{hanzo-attest} would need BLAKE3, and a verifier holding the release can
reproduce neither from the other.
\item[IR-4 --- a node-binding registry contract.] The vendored manager preserves
its ABI deliberately and should not be forked. Requested: the two-mapping,
one-function contract of \S\ref{sec:stake}, so that \code{validationID} and $A_n$
agree on-chain, with a proof of possession of $A_n$ at \code{bind}, and
\code{forfeit} taking the equivocation proof of \S\ref{sec:equiv}.
\item[IR-5 --- a TPM 2.0 attester and verifier, ahead of SNP and TDX.] On the
measured fleet, two of three hosts have TPM 2.0 with a SHA-256 PCR bank and a
firmware event log, and zero have SNP or TDX. Requested: a quote over PCRs whose
caller field carries $\sha{256}(A_n \| B)$, so the same binding rule as IR-2
applies to the one hardware root the fleet actually has.
\item[IR-6 --- cluster admission by address.] The shared \code{K3S\_TOKEN} grants
membership under any name to anyone holding it. The replacement needs no new
protocol: \code{k3s token create --ttl} and the \code{system:node} binding are
already there. Requested: an admission service that performs V1--V8 and mints a
minutes-long bootstrap token for a node name derived from $A_n$; the server token
moved to KMS, removed from every agent, and rotated; the Node authorizer and
\code{NodeRestriction} confirmed on the API server.
\item[IR-7 --- \code{hanzod} does not hold an operator token.] \code{hanzo link}
authorizes registration with the operator's IAM bearer, which is correct for the
act of linking. Requested: the token is not retained as the node's durable
credential, and any long-lived cloud call \code{hanzod} makes is authorized by
its own address, not by the operator's org.
\end{description}
\subsection{Operational requests}
\begin{itemize}
\item Enable SecureBoot on the x86 host. It has a TPM 2.0 and SecureBoot is
currently disabled, which caps it at class \code{self} even once IR-5 lands: an
unenforced boot chain means the PCR values are chosen by whoever controls the
boot.
\item Retire the local credential files under \code{\textasciitilde/.hanzo/} on
every host that runs a node. Owner-only file permissions are L1 by the ladder of
\S\ref{sec:ladder}; C1 requires L2 or better, and a node's credential is what C3
scopes.
\item Move the k3s server token into KMS and rotate it when IR-6 lands. Until then
it is a fleet-wide shared secret held on every agent.
\end{itemize}
% =====================================================================
\appendix
\section{The fleet, as measured}
\label{app:fleet}
Measured 5 September 2026 by direct inspection of the three hosts. Recorded because
the assurance classes in \S\ref{sec:class} are claims about real hardware and
should be checkable.
\begin{center}
\footnotesize
\begin{tabular}{@{}llll@{}}
\toprule
& \textbf{aarch64 host} & \textbf{x86 host} & \textbf{Apple-Silicon host} \\
\midrule
Machine & NVIDIA DGX Spark (P4242) & GMKtec NucBox EVO-X2 & MacBook Pro \\
CPU & NVIDIA GB10 & AMD Ryzen AI MAX+ 395 & Apple M4 Max \\
Arch & aarch64 & x86\_64 & arm64 \\
OS & Ubuntu 24.04.4 & Ubuntu 24.04.4 & macOS 26.5.2 \\
Kernel & 6.17.0-1031-nvidia & 7.0.0-31-generic & Darwin 25.5.0 \\
TPM & 2.0 & 2.0 & none exposed \\
Event log & \code{binary\_bios\_measurements} & \code{binary\_bios\_measurements} & --- \\
SecureBoot & enabled & \textbf{disabled} & n/a \\
SEV-SNP / TDX & n/a (aarch64) & \code{sev\_snp = N} & n/a \\
Monitor & KVM & cloud-hypervisor & Virtualization.framework \\
k3s & control-plane, v1.35.5+k3s1 & agent, v1.35.5+k3s1 & not joined \\
OS assets & 2.0.0 & 2.0.0 & 2.0.0 \\
Kernel image & 9,710,080 bytes & 23,587,632 bytes & 9,710,080 bytes \\
Class today & \code{self} (\code{platform} after IR-5) & \code{self} & \code{self} \\
\bottomrule
\end{tabular}
\end{center}
\noindent
PCR~0, SHA-256 bank, read on each Linux host:
\begin{center}\small\ttfamily
aarch64\quad 3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969\\
x86\quad\ \ \ \ \ 2af943023aa8bdf10890398588b1315325e085645fcdf29d6596fa73db2b6375
\end{center}
\noindent
Only the aarch64 value is meaningful as an assurance claim: with SecureBoot
disabled on the x86 host, its PCR values record a boot chain nothing enforced.
Both Linux hosts are joined to one cluster; the Apple-Silicon host is not currently
a member. The microVM is what would let it join on the same terms, which is the
architectural point of \S\ref{sec:class}: uniform interface, honestly non-uniform
assurance.
Three details of this table bear on the specification rather than on the hardware.
First, the OS assets are at version 2.0.0 on all three hosts and the kernel images
differ in size, because they are different kernels for different architectures.
The launch register is therefore per-architecture at a fixed asset version, and the
allowed list checked at V6 is a set indexed by (release, architecture), not by
release alone. A verifier that expects one value per release will refuse every host
but one.
Second, the monitor is not uniform either. The two Apple-Silicon hosts carry
\code{hanzo} 8.5.157 and \code{hanzo-vm} 2.0.0 on the path; the Linux hosts carry
the Go \code{hanzo} CLI (\code{v1.801.358} and \code{v1.801.498}) and no
\code{hanzo-vm} on the path, though both hold the 2.0.0 OS assets. The monitor
binary is one of the things a launch measurement should commit to
(\code{hanzo-attest} includes its digest; \code{vm-measure}'s launch log does not),
and on this fleet it varies.
Third, \code{hanzo-vm} 2.0.0 as installed exposes \code{run}, \code{init},
\code{upgrade}, \code{checkpoint} and \code{prune}. It does not expose a measure
verb: \code{vm-measure} exists in the repository and not in the binary the fleet
runs. Nothing on this fleet has produced a measurement yet.
\section{Two encodings, and the digest problem}
\label{app:encoding}
\emph{Observed.} Two crates measure the same assets to different numbers.
\paragraph{\code{vm-measure} (\code{hanzoai/vm}).} An ordered event log folded into
one register:
\[
e_i = \sha{384}\bigl(\mathit{name}_i \,\|\, \mathtt{0x00} \,\|\, \sha{384}(\mathit{content}_i)\bigr),
\quad R_0 = 0^{48}, \quad R_{i+1} = \sha{384}(R_i \,\|\, e_i).
\]
The launch log's events are, in order: \code{kernel} (file), \code{cmdline} (text),
\code{initrd} (file, when present), \code{root} (file, the source image and never
the per-instance copy), \code{shape} (text,
\code{cpus=\dots\ memory=\dots MB disk=\dots MB}). The workload log is extended by
whatever drives the machine. The name is inside its own event digest, so events
cannot trade places; the fold commits to order, so a prefix and a whole log are
different registers; paths never enter, so the same assets measure identically on
every host. The wire form carries both the value and the events and is refused on
read if the value does not fold from the events.
\paragraph{\code{hanzo-attest} (\code{hanzoai/node}).} A single BLAKE3 digest over a
canonical encoding of a \code{Boot} struct: the domain tag \code{hanzo.boot.v1},
then the monitor binary digest, the OS-asset version string, the kernel digest and
the root filesystem digest --- each length-prefixed with a big-endian \code{u64} ---
then \code{cpus}, \code{memory\_mb} and \code{disk\_mb} as big-endian \code{u64},
then the argv count and each argument length-prefixed.
\paragraph{The digest problem.} The kernel image on the Apple-Silicon host has three
different digests in three places that all describe ``the kernel'':
\begin{center}\footnotesize\ttfamily
\begin{tabular}{@{}ll@{}}
\toprule
SHA-384, \code{\textasciitilde/.hanzo/vm/digests.json} & 40c33bf220f5315d\dots{}b3b26f7 \\
BLAKE3, what \code{hanzo-attest} would fold & ee841563267f54a2\dots{}62638930 \\
SHA-256, what a release note would quote & 2f71e062b952e5f6\dots{}7bba3025 \\
\bottomrule
\end{tabular}
\end{center}
\noindent
A verifier who holds the published release must be able to recompute the value the
epoch statement commits to. That is possible only if the release publishes the
digest function the measurement uses. SHA-384 is the right one, because it is what
the hardware registers use and it is therefore the only choice that needs no
re-hashing at the boundary to a report. IR-3 is the request to make it the only one.
\end{document}