# Verified validators
URL: /docs/x1id/validators

> Link an X1 validator's vote account to a handle and earn the gold shield — a trustless, attestor-free verification proven on-chain by the vote account's withdraw authority. Co-sign it, or submit a detached offline signature. How both instructions work, the challenge the withdraw key signs, and the Verify-your-validator flow.

# Verified validators

<Pill tone="mainnet">Mainnet</Pill>

<LlmActions />

A handle can prove it belongs to the operator of an X1 **validator** and earn a **gold shield** — a verification in a class of its own, above the social [blue tick](/docs/x1id/verification). The blue tick is an off-chain review that X1ID's attestor key signed off on. The validator shield is **trustless**: no attestor, no human review, no key to trust. Control is proven **on-chain**, by the only key that can prove it — the vote account's `authorized_withdrawer`.

<Callout type="info">
  These are **on-chain instructions** on the registry program (`8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P`), built into X1ID's own program. The attestation they mint is `kind 6` (`KIND_VALIDATOR`) at the usual `["attestation", handle, kind]` PDA.
</Callout>

## Why it needs no attestor

Social accounts can't be proven on chain — an X or GitHub handle has no on-chain key, so someone trusted (the X1ID attestor) has to do the off-chain check and sign the result. A validator is different: its vote account **is** an on-chain account, and it names an `authorized_withdrawer` — the key with authority over the validator's stake rewards. If you can sign as that key, you demonstrably control the validator. The program checks that signature against the authority it reads **straight from the vote account**, so the proof is self-contained.

That's why the validator kind sits deliberately **one past** the attestor-mintable range: `create_attestation` (the attestor's instruction) refuses any `kind >= KIND_COUNT`, and `KIND_VALIDATOR` **is** `KIND_COUNT` (6). The trusted attestor key **cannot** mint a validator attestation even if it wanted to — only the two cryptographic instructions below can.

Both instructions require **two authorities to agree**:

* The **handle's current authority** (its owner, or the NFT holder if the handle is tokenized) authorizes linking this validator to the handle.
* The **vote account's withdraw authority** proves control of the validator.

What stored on the minted attestation:

| Field           | Holds                                                                                              |
| --------------- | -------------------------------------------------------------------------------------------------- |
| `kind`          | `6` (`KIND_VALIDATOR`).                                                                            |
| `evidence_hash` | the validator's **vote account pubkey** (32 bytes).                                                |
| `attestor`      | the **withdraw authority** that proved control — a historical fact, **not** the x1id attestor key. |
| `attested_at`   | when the link was (last) stamped; subject to the usual staleness rule.                             |

## Two ways to prove it

Pick the path that fits how the withdraw key is held.

<Cards>
  <Card icon="PenLine" title="attest_validator — co-sign" description="The withdraw authority co-signs the same transaction as the handle owner. Simplest when both keys are on hand (e.g. the operator holds both)." />

  <Card icon="FileSignature" title="attest_validator_signed — detached" description="The withdraw authority signs a challenge OFFLINE; the handle owner submits alone, carrying that signature in an Ed25519 precompile instruction. For cold/air-gapped withdraw keys, or SSO-based owners." />
</Cards>

### `attest_validator` (co-sign)

Two signers on one transaction: the handle's current authority and the vote account's withdraw authority. The program:

1. Confirms the supplied `vote_account` is a **real vote account** — owned by the Vote program, with a recognized version tag — rejecting anything masquerading as one.
2. Reads `authorized_withdrawer` from its fixed offset in the vote account data.
3. Requires the **co-signing** `withdraw_authority` to equal that key (the trustless proof).
4. Requires the handle's current authority to sign (owner, or NFT holder if tokenized).
5. Mints `Attestation { kind: KIND_VALIDATOR }`, storing the vote account in `evidence_hash` and the withdraw authority in `attestor`.

### `attest_validator_signed` (detached signature)

When the withdraw key can't co-sign the transaction — it's cold, air-gapped, or on a different machine from the handle owner — the withdraw authority signs a **challenge offline** instead, and the handle owner submits the transaction **alone**, carrying that signature in an **Ed25519 precompile instruction**.

The client places the Ed25519 precompile instruction **immediately before** `attest_validator_signed` in the same transaction. The program:

1. Validates the vote account and reads the withdraw authority exactly as above.
2. Requires the handle's current authority to sign (and be the sole transaction signer).
3. **Rebuilds the exact challenge** (below) from on-chain state.
4. Introspects the instruction sysvar: the instruction **directly before** this one must be the real Ed25519 program, verifying a single signature over that exact challenge, by that exact withdraw authority. Anything else fails with `BadValidatorProof`.

Same result as the co-sign path, same trustless guarantee — only the real withdraw key can produce a signature over the challenge.

#### The challenge

The withdraw authority signs these **96 bytes**, in order:

| Segment         | Bytes | Value                                                           |
| --------------- | ----- | --------------------------------------------------------------- |
| Domain prefix   | 24    | the ASCII string `x1id:validator-attest:v1`                     |
| Handle PDA      | 32    | the `["handle", name]` account address                          |
| Vote account    | 32    | the validator's vote account pubkey                             |
| `registered_at` | 8     | the handle's current `registered_at`, as an `i64` little-endian |

Binding the challenge to the handle's **current** `registered_at` is what makes a prior owner's signature unusable after a transfer or re-registration: the epoch in the signed bytes no longer matches, so the proof is rejected — the same discipline the non-SVM record proofs use.

<Callout type="warn">
  The withdraw authority's signature carries **no expiry**. It stays valid for that exact `(handle, vote account, registered_at)` tuple until the handle is re-registered or the withdraw key is rotated. This is intended: a validator attestation is a self-evident cryptographic proof, not an attestor endorsement, so the operator can always re-establish it. Re-submitting only re-stamps the same link and is owner-gated, so it is harmless.
</Callout>

## The gold shield

A live validator attestation surfaces as a **gold shield** next to the handle — distinct from the social blue tick, and a higher class of verification because it is trustless. Where both are present, **gold wins**: a validator-verified name shows the gold shield as its single badge, with the full detail (the linked vote account, any social platforms) on the handle's own page.

Reading it mirrors the blue tick exactly. A validator attestation is `kind 6` in a handle's attestations, with its vote account in `evidence_hash`. It is subject to the **same staleness rule** as every other attestation: a stale one (predating the current `registered_at`, or a `clear_records`) never lights the shield. See [Verified identity](/docs/x1id/verification#the-staleness-rule) for `fetchAttestations` and how staleness is judged from chain.

```ts
import { fetchAttestations, liveAttestations } from '@x1id/resolve';

const atts = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);

// The validator attestation is kind 6; its evidence_hash is the vote account.
const validator = liveAttestations(atts).find((a) => a.kind === 6);
if (validator) {
  // Show the gold shield. validator.evidenceHash is the vote account pubkey.
}
```

## Verify your validator

In the app, linking a validator to your handle follows the detached-signature path — the withdraw key never has to touch the browser.

<Steps>
  <Step>
    ### Enter your vote account

    On your handle's settings, choose to verify a validator and paste your validator's **vote account** address. The app reads its `authorized_withdrawer` from chain and shows you the exact challenge to sign.
  </Step>

  <Step>
    ### Sign the challenge offline

    Sign the shown challenge with your **withdraw authority key**, wherever it lives — your CLI, a hardware wallet, an air-gapped machine. The key never leaves that environment.
  </Step>

  <Step>
    ### Paste the signature

    Paste the resulting signature back into the app. It assembles the transaction: the Ed25519 precompile instruction carrying your signature, immediately followed by `attest_validator_signed`.
  </Step>

  <Step>
    ### Submit

    You (the handle owner) sign and send the transaction. On success, the gold shield appears on your handle — proven on-chain, no attestor involved.
  </Step>
</Steps>

<Callout type="info">
  **Rotated your withdraw authority?** Re-run the flow. The attestation overwrites in place with the new signer, so the stored proof always reflects the current `authorized_withdrawer`.
</Callout>

## Next

<Cards>
  <Card href="/docs/x1id/verification" icon="BadgeCheck" title="Verified identity" description="The social blue tick, the attestation-kinds table, and the staleness rule the shield shares." />

  <Card href="/docs/x1id/contracts" icon="FileCode2" title="Contracts" description="The registry program these instructions live on, and how to verify it yourself." />

  <Card href="/docs/x1id/records" icon="Link" title="Address records" description="The per-chain records a verified handle sits alongside." />
</Cards>
