HIP-1049: Validator — A Token Redeemed for a Node. Status Final. Hanzo architectural specification.
/v1/validator turns proof that a wallet holds a validator-tier NFT into a provisioned node: prove the slot, get a staking identity generated and sealed, get a node custom resource written, and get a registration QUEUED for the owner to co-sign. It is served by apps/validator in hanzoai/cloud.
The token id IS the slot. Everything else — the challenge, the signature, the on-chain ownership read, the sealed keys, the queued registration — exists to make that claim provable and its consequences reversible.
Onboarding a validator by hand is a sequence in which every step can be done wrong: keys generated on somebody's laptop, a node pointed at the wrong network, a registration submitted before anyone checked the stake. The sequence is worth automating exactly once, server-side, with each step failing closed.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
slot), stored server-side, with the EXACT message to sign (apps/validator/validators.go:246).
forged nonce dies before it can cost an RPC call (apps/validator/validators.go:326).
validated org, the slot and the nonce — never from a message the caller supplied.
the registration.
Steps 1 and 3 together are the property: signing anything other than the server's own message cannot claim a slot, and a signature obtained for one org or one slot cannot be replayed for another.
A slot outside the validator tier is refused at the challenge, before anyone signs.
The staking identity is generated and sealed into the key management plane BEFORE the claim row is written (apps/validator/validators.go:363). A claim MUST NOT exist without its keys.
Key material MUST NOT be returned, logged or stored in the clear. It seals under an org-scoped coordinate, and the reader that materialises it into a node is admitted only for that same org (apps/validator/validators.go:621-624).
The pipeline ENQUEUES an owner-gated registration and MUST NOT submit it to any chain. The owner co-signs out of band, and the stake weight is set at co-sign time — never derived from the NFT (apps/validator/validators.go:412-419).
This is the line between "provisioning a node" and "committing stake". The first is automatable; the second is a decision a person makes.
Provisioning writes a resource for a node this pipeline owns. It MUST NOT touch a running node, and the guard is structural rather than procedural: the resource name is always the claim's own prefixed form, reserved namespaces are refused, and the legacy resource groups are refused, so even an org name that folds toward a reserved word cannot escape the prefix (apps/validator/validators_test.go:332).
With no cluster reachable, the slot is still claimed, the keys are still sealed and the registration is still queued; the node is reported PENDING (apps/validator/validators.go:444-448). A provisioner that cannot provision MUST report that rather than fake a success.
The org is the validated principal, never a client header, and every store query filters on it. A slot held by ANOTHER org is a 404 on the read path — the same answer as a slot nobody holds — so the surface cannot be used to probe which slots are taken (apps/validator/validators.go:531). On the WRITE path a slot held by another org is a conflict, which discloses only what the on-chain ownership read already established for this caller.
Re-claiming a slot the caller's org already holds is IDEMPOTENT: the node resource is re-applied and the existing identity is returned, keys and node id stable. A first claim answers 201, a re-claim 200.
A write with no validated principal is refused BEFORE the request body is decoded (apps/validator/validators.go:172). A typed operation runs after decoding, so a check inside the handler answers 400 to an unauthenticated caller whose body is also malformed — telling them the shape of a surface they may not use. This is pinned by a test, because it is a property of where the check sits and not of what it says.
Four operations, every one typed with no exception (apps/validator/typed_wire_test.go:23): GET /v1/validator lists the caller's own claims, GET /v1/validator/challenge issues §1's nonce, POST /v1/validator is the claim, and GET /v1/validator/{tokenId} reads one slot (plugin/validator/openapi.json).
The one store it owns is a single SQLite file holding every org's entitlements, the owner-gated registration queue and the short-lived challenges, tenant-isolated on the org column of every scoped query (apps/validator/store.go:25-30). The staking keys are NOT in it — they seal into the key plane, which is §2.
The capability is METERED (plugin/validator/main.go:27, Price: cloud.Metered), and the billed act is the MATERIALIZATION — one validator node applied to the cluster — not the claim: the NFT makes a caller eligible, but eligibility is not settlement, and the node runs on rented capacity. The fee is VALIDATORS_FEE_CENTS[_NODE], resolving through the fleet's ordinary provision default; it is authorized BEFORE the CR is applied and debited only after one actually was, so a claim that stays pending — and a reprovision of a slot the org already holds — bills nothing (apps/validator/meter.go). Stake is still committed only at the owner's co-sign (§3). It publishes no event, so a customer's webhooks receive nothing from it, and it emits nothing to observability beyond the request span every route already gets. Its stage is beta (manifest/apps.go:277, Stage: Beta). It derives from no outside project.
The slot id and the page limit each have exactly ONE parse rule, and the typed inputs carry them as strings for that reason: the rule that has always served these routes trims surrounding whitespace, and one rule is better than two (apps/validator/validators.go:632). Path and query MUST NOT outrank a claim body — the claim's fields are body-only, so there is no second way to address the write.
Proof of ownership could be a signature over a client-chosen message, which is one fewer round trip. It also lets a signature harvested anywhere else be replayed here. A server-issued, server-stored, single-use nonce costs a call and closes that.
Burning the challenge before the chain read, rather than after a successful claim, means a flood of forged nonces costs one store write each instead of one on-chain call each.
This capability mints a node identity and commits infrastructure, so its refusals are the specification. Every gate fails closed: a bad signature, a non-owner, an out-of-tier slot or an unreachable key plane all leave no claim persisted and no key material exposed.
The queued registration is the last containment. Even a caller who defeated everything above obtains a provisioned node and a pending request, not a validator with stake — because nothing in this pipeline can submit one.
Sealed staking keys are the highest-value material here. They are generated server-side, never leave the seal, and are addressed under an org-scoped coordinate the reader admits only for that org; a coordinate that could be named across orgs would make every other control cosmetic.
Released under CC0 1.0 Universal Public Domain Dedication.