HIPsHanzo Proposals
Back to HIPs
HIP-1049FinalStandards TrackInfrastructure

Validator — A Token Redeemed for a Node

Hanzo AI
Created: 2026-08-20
Requires: HIP-0027, HIP-0106, HIP-0139

HIP-1049: Validator — A Token Redeemed for a Node

Abstract

/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.

Motivation

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.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

1. The claim, in order

  1. A challenge is issued for a slot: a single-use nonce bound to (validated org, slot), stored server-side, with the EXACT message to sign (apps/validator/validators.go:246).
  2. The claim burns the challenge FIRST, before any chain read, so a replayed or forged nonce dies before it can cost an RPC call (apps/validator/validators.go:326).
  3. The signer is recovered from the message THIS SERVER REBUILDS from the validated org, the slot and the nonce — never from a message the caller supplied.
  4. That recovered wallet MUST hold the validator-tier NFT for that slot on chain.
  5. Only then: generate a staking identity, seal it, write the node resource, queue 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.

2. Keys are sealed before anything is persisted

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).

3. Registration is queued, never submitted

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.

4. The new node is a NEW node

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).

5. Degrade honestly

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.

6. Tenancy, and the two 404s

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.

7. Identity refusal precedes the body

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.

8. The surface, whole

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.

9. One parse rule per value

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.

Rationale

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.

Security Considerations

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.

References

  • HIP-0027 — Secrets Management Standard
  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-0519 — One Identity Boundary

Copyright

Released under CC0 1.0 Universal Public Domain Dedication.