> For the complete documentation index, see [llms.txt](https://igra-labs.gitbook.io/igralabs-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://igra-labs.gitbook.io/igralabs-docs/for-attesters/delegated-attestation-setup.md).

# Delegated Attestation Setup

Run your attester with a **cold controller wallet** that holds the stake, and a **hot operator wallet** that submits attestations. The controller's private key never touches the server.

This is the recommended setup if your staked wallet is a hardware wallet, a high-value EOA, or any key you don't want exposed on an always-online machine.

## Concept

| Role           | Wallet      | Holds                             | Signs                             | Where it lives                                                      |
| -------------- | ----------- | --------------------------------- | --------------------------------- | ------------------------------------------------------------------- |
| **Controller** | Cold wallet | Stake + rewards accrue here       | One-time delegation authorization | Hardware wallet / air-gapped / cold laptop                          |
| **Operator**   | Hot wallet  | Only enough iKAS for gas (\~5–10) | Every attestation transaction     | On the attestor server (required — the container needs the raw key) |

**What happens if the operator key leaks?** An attacker can submit (or fail to submit) attestations in your name, including attestations that get your stake slashed. They **cannot** withdraw stake, claim rewards, or change the delegation — those all require the controller. A new delegation does not revoke the old key — see [If the operator key is compromised](#if-the-operator-key-is-compromised).

**Rewards** always accrue to the controller address. To claim, connect the **controller** to the [attester dashboard](https://attester-dashboard.igralabs.com/?network=mainnet) — signing one claim tx, then the cold wallet can go back offline.

**The operator can be any EOA** — a fresh private key generated on the server is fine. It doesn't need a prior reputation or stake; it just needs a private key the attestor container can read.

## Pre-requisites

All the [standard pre-requisites](/igralabs-docs/for-attesters/attestor-setup-guide.md#pre-requisites) apply (synced node, funded kaswallet, RPC enabled), plus:

* **Controller wallet** already registered as an attester with stake
* **Operator wallet** generated (fresh EOA is fine — just need the private key)
* **5–10 iKAS on the operator wallet** for Igra gas

## Step 1 — Generate the delegation signature (on the controller side)

The signature is a standard **EIP-712** typed-data signature. It authorizes a specific operator address to attest on behalf of the controller until a given block number expires.

**EIP-712 domain:**

```
name:              "Igra Labs"
version:           "1"
chainId:           38833  (mainnet) / 38836 (testnet)
verifyingContract: 0xc24Df70E408739aeF6bF594fd41db4632dF49188
salt:              0xd6becd43d810e50afccd2f334a5834e78cf3da756977f6f8f7a8e5e97d7acf7c
```

**Message type:** `DelegationAuth(address operator, uint64 expiry)`

Any tool that can sign an EIP-712 payload will produce a valid signature. The easiest path is the attester dashboard; CLI options below cover software keys, hardware wallets, and multisigs.

### Using the attester dashboard (recommended)

The [attester dashboard](https://attester-dashboard.igralabs.com/advanced) has a built-in **Sign DelegationAuth** flow that works with any connected wallet — MetaMask, Rabby, Ledger/Trezor via MetaMask, WalletConnect.

1. Open the [Advanced page](https://attester-dashboard.igralabs.com/advanced) and connect with your **controller** wallet
2. Enter the **operator address** and **expiry** (block number)
3. Click **Sign DelegationAuth** — your wallet prompts for an EIP-712 signature
4. The dashboard shows a **Paste into setup.sh** box with three values (`CONTROLLER_ADDRESS`, `DELEGATION_EXPIRY`, `DELEGATION_SIGNATURE`). Use **Copy all** or **Download .env** to transfer them to the attestor server.

This flow works for **any EOA controller**, including hardware wallets connected via MetaMask/Rabby.

### If the controller is a software key (CLI)

Use the official signing container on the controller's machine:

```bash
cat > delegation.env << 'EOF'
OPERATOR_ADDRESS=0xYOUR_HOT_WALLET_ADDRESS
DELEGATION_EXPIRY=<current block + months × 2,400,000>
CHAIN_ID=38833
CONTRACT_ADDRESS=0xc24Df70E408739aeF6bF594fd41db4632dF49188
EOF

printf "Controller private key: "; read -rs CONTROLLER_PRIVATE_KEY; echo
export CONTROLLER_PRIVATE_KEY

docker run --rm -it --env-file delegation.env -e CONTROLLER_PRIVATE_KEY \
  igranetwork/attestor:2.3.2 --sign-delegation

unset CONTROLLER_PRIVATE_KEY
rm delegation.env
```

The key is typed at a hidden prompt and passed to the container through the environment, so it isn't written to a file or saved in shell history. Don't put it in `delegation.env`: bash saves heredoc contents to shell history, key included.

The command prints `CONTROLLER_ADDRESS`, `DELEGATION_EXPIRY`, and `DELEGATION_SIGNATURE` to paste on the server during `./setup.sh`.

### If the controller is a Ledger / Trezor (CLI)

The dashboard flow above is usually easier (connect hardware via MetaMask/Rabby). If you prefer CLI, use [Foundry's `cast`](https://book.getfoundry.sh/reference/cast/cast-wallet-sign) to sign the EIP-712 payload via the hardware device.

1. Save this to `delegation.json` (replace the operator and expiry):

```json
{
  "domain": {
    "name": "Igra Labs",
    "version": "1",
    "chainId": 38833,
    "verifyingContract": "0xc24Df70E408739aeF6bF594fd41db4632dF49188",
    "salt": "0xd6becd43d810e50afccd2f334a5834e78cf3da756977f6f8f7a8e5e97d7acf7c"
  },
  "primaryType": "DelegationAuth",
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"},
      {"name": "verifyingContract", "type": "address"},
      {"name": "salt", "type": "bytes32"}
    ],
    "DelegationAuth": [
      {"name": "operator", "type": "address"},
      {"name": "expiry", "type": "uint64"}
    ]
  },
  "message": {
    "operator": "0xYOUR_HOT_WALLET_ADDRESS",
    "expiry": "<current block + months × 2,400,000>"
  }
}
```

2. Sign with the hardware wallet:

```bash
# Ledger
cast wallet sign --ledger --data --from-file delegation.json

# Trezor
cast wallet sign --trezor --data --from-file delegation.json
```

The output is the 65-byte `0x…`-prefixed signature — this is your `DELEGATION_SIGNATURE`. Your `CONTROLLER_ADDRESS` is the hardware wallet's address.

### If the controller is a Safe / multisig

**Not currently supported.** The on-chain verifier uses `ECDSA.recover` (EOA signatures only) and does not accept ERC-1271 contract signatures. If your controller is a Safe, you'd need to use an EOA signer from the Safe's owners as a proxy, or wait for ERC-1271 support.

## Step 2 — Run setup.sh in delegated mode

On the attestor server:

```bash
git clone https://github.com/IgraLabs/attestor-deploy.git
cd attestor-deploy/deploy
./setup.sh mainnet
```

When prompted:

1. Choose **`2) Delegated`**
2. Paste the **controller address** (the cold wallet address)
3. Paste the **delegation expiry** (the block number you signed for)
4. Paste the **delegation signature** (`0x…` + 130 hex chars)
5. Enter the **operator private key** (the hot wallet's key — goes into `secrets/private_key.txt`)

The attestor starts and submits attestations signed by the operator, authorized by the controller's delegation.

## Step 3 — Verify

```bash
curl -s localhost:8180 | jq .
```

You should see `"status": "healthy"` and `"state": "Active"`.

Monitor status and rewards via the dashboard, pointing at the **controller** address (that's where stake and rewards live):

```
https://attester-dashboard.igralabs.com/?network=mainnet&address=CONTROLLER_ADDRESS
```

## Choosing an Expiry

`DELEGATION_EXPIRY` is the **Igra block number** after which the delegation stops being valid. The contract rejects any attestation once `block.number` passes it. Igra produces about 0.92 blocks per second: **about 2.4M blocks per month, about 29M per year**.

The easiest way to set it is the [attester dashboard](https://attester-dashboard.igralabs.com/advanced) → *Sign a DelegationAuth*. It fills in the expiry from the current block and shows the calendar date. To compute it by hand:

```bash
cast block-number --rpc-url https://rpc.igralabs.com:8545   # current block
# expiry = current block + months × 2,400,000
```

* For testing: pick a nearby future value.
* For production: pick something 6–12 months ahead and set a calendar reminder. The attestor only starts warning about 3 hours (10,000 blocks) before expiry.

To renew, sign a new delegation (new expiry, same operator), then edit `deploy/.env` to update `DELEGATION_EXPIRY` and `DELEGATION_SIGNATURE` and run `docker compose down && docker compose up -d`.

## Rotating the Operator

To move attesting to a new operator key:

1. Generate a fresh operator EOA.
2. Sign a new delegation from the controller (same expiry, new operator address).
3. Re-run `./setup.sh mainnet` and paste the new delegation + operator key.
4. Move any iKAS left on the old operator wallet to the new one, then delete every other copy of the old private key (backups, snapshots, other machines). It can still act for your controller until you exit.

The stake, rewards history, and attester identity are unchanged — only the signing key rotates.

### If the operator key is compromised

Rotating does not revoke the old key. A new delegation doesn't invalidate earlier ones, and each delegation signature is public in the calldata of the attestations that used it. Treat every operator key you have authorized as live until you exit.

To cut off a compromised operator:

1. Connect the **controller** to the [attester dashboard](https://attester-dashboard.igralabs.com/?network=mainnet) and click **Pause Attester**. This requests exit and stops attestations immediately. Do not click **Resume Attester** — that restores the old delegations.
2. After the exit cooldown (\~2.5 hours), click **Exit & Withdraw**.
3. Register the stake from a **new controller address**, then set up delegation to a fresh operator. Re-registering the same controller address brings old delegations back.
