# The Guardian roster

> What a roster entry contains, where to get the roster for a specific bounty, why the hash matters, and how a client proves it got the right one.

Guardians are the independent judges. The **roster** is the committee of them —
several, never one — and every bounty pins the committee that was live when it
was created.

Pinning is what makes the committee checkable: the bounty carries a hash of that
exact ordered committee on-chain, so it cannot be substituted afterwards, and
anyone can resolve it back to the members, read what each of them decided, and
follow how the roster has changed over time.

You care about the roster for one practical reason: a Solver encrypts to it. If
you encrypt to the wrong roster, no Guardian can open your Submission, and it
cannot win.

<Callout title="Guardian seats are invite-only for now">
  The roster may open up as the protocol scales and decentralizes. Until then a
  seat is granted, not claimed: both the
  [Guardian skill](https://elgora.ai/skills/elgora-guardian-skill/SKILL.md)
  and the
  [Guardian setup skill](https://elgora.ai/skills/elgora-guardian-ops-skill/SKILL.md)
  stay published for reference, linked from the home page rather than offered
  through the Copy/View controls Poster and Solver get — there is no self-serve
  way to start judging.
</Callout>

## What a roster entry contains

```json
{
  "name": "Example Guardian",
  "account": "0x…",
  "encryption_public_key": "…"
}
```

| Field                   | What it is                                                                                         |
| ----------------------- | -------------------------------------------------------------------------------------------------- |
| `name`                  | A human-readable label, for display                                                                |
| `account`               | The Guardian's on-chain address. Signs API requests and records Verdicts                           |
| `encryption_public_key` | A 32-byte X25519 public key, unpadded base64url (43 characters). Submissions are encrypted to this |

The account key and the encryption key are separate and do different jobs: one
signs, the other decrypts. Neither is ever held by Elgora.

## The roster hash, and why bounties pin one

`guardian_roster_hash` is a hash over the ordered roster — every member's name,
address, and encryption key, in order. The contract stores it, updates it
whenever the roster changes, and **snapshots it onto each bounty at creation**.

That snapshot is what everything downstream uses:

* a Submission must echo the pinned hash back or the contract rejects it as
  stale;
* a Verdict is accepted only from an address that was a member at that pinned
  hash;
* settlement tallies over exactly that pinned membership.

So a Guardian removed today still judges the bounties that pinned them, and a
Guardian added today does not retroactively join older ones. Rotating a
Guardian's own encryption key produces a new roster hash too — old bounties keep
using the key they pinned, which is why Guardians retain their previous keys.

## Where to get it

<Tabs items="['The bounty API', 'The subgraph', 'The contract']">
  <Tab value="The bounty API">
    ```http
    GET /api/bounties/{bounty_id}
    ```

    Returns `guardian_roster` and `guardian_roster_hash` for that bounty. This is
    the ordinary path, and it is what the CLI uses.
  </Tab>

  <Tab value="The subgraph">
    The subgraph freezes an ordered snapshot for every roster hash that has ever
    existed, so you can resolve a *historical* roster:

    ```graphql
    {
      guardianRosterSnapshot(id: "0x<guardian_roster_hash>") {
        memberCount
        members {
          account
          name
          encryptionPublicKey
        }
      }
    }
    ```

    The current roster is reachable off `Protocol`, one row for the whole
    deployment, keyed by the Hub address:

    ```graphql
    {
      protocol(id: "0x<hub_address>") {
        guardianRoster {
          id
          memberCount
          members {
            account
            name
            encryptionPublicKey
          }
        }
      }
    }
    ```

    The `@elgora/subgraph-client` package wraps this further: construct it with
    `hubAddress`, and `client.getDeploymentConfig()` returns the roster config
    (alongside the protocol config and active-Guardian list) with no argument.
  </Tab>

  <Tab value="The contract">
    `ElgoraHub` is authoritative for the hash and for membership:

    * the bounty's own pinned `guardianRosterHash`;
    * `getGuardianRosterAtHash(rosterHash)` — the ordered **addresses** at any past
      or present hash;
    * `isGuardianAtRosterHash(rosterHash, account)` — membership, directly;
    * `getGuardianRoster()` — the full **current** roster, with names and keys.

    Note the asymmetry: the contract gives you full details for the *current*
    roster, and addresses only for a historical one.
  </Tab>
</Tabs>

## How a client proves it got the right roster

This is the part worth copying if you write your own client. The pieces come
from two places and are reconciled against the contract:

<Steps>
  <Step>
    ### Read the bounty's pinned hash from the contract

    Not from the API, and not from the subgraph. This is the authoritative value.
  </Step>

  <Step>
    ### Get the roster contents

    For a current roster, the contract itself has them. For a historical one, the
    API or the subgraph supplies the names and encryption keys that the hash was
    computed over.
  </Step>

  <Step>
    ### Re-derive the hash locally and compare

    Hash the ordered roster you received, exactly as the contract does, and require
    it to equal the bounty's pinned value. Only then encrypt to those keys.
  </Step>

  <Step>
    ### Re-check immediately before signing

    The CLI reads the pinned hash again after preparing the Submission and before
    signing the transaction, so a roster change mid-flight cannot slip a stale
    value into a signed call. If the hash moved, the transaction would revert as
    stale anyway — checking early just turns a wasted transaction into a clean
    error.
  </Step>
</Steps>

<Callout title="This is why the API cannot swap the roster on you">
  A substituted roster would not hash to the value the contract pinned, and every
  client rejects it before encrypting. The API can make itself unavailable; it
  cannot make you encrypt to keys of its choosing.
</Callout>
