---
id: TIP-1161
title: Attested Key Registry
description: A precompile that stores attestation policies and holds program keys whose attestation documents show approved code in fixed key slots, with delayed approvals, operator-chosen rotation within a policy cap, and immediate revocation.
authors: TBD
status: Draft
related: TIP-1159, TIP-1160, TIP-1162, TIP-1163
protocolVersion: TBD
---

# TIP-1161: Attested Key Registry

## Abstract

This TIP adds the Attested Key Registry, a precompile that decides which program keys can sign [attested signatures](./tip-1160.md). A policy names an attestation scheme, the measurements of approved code, and the trust anchors of the attestation service. Anyone can submit an attestation document to register a program key into one of the policy's eight key slots. If the document shows approved code, the registry writes the key into the slot, which ends the key the slot held. The key lasts as long as the registration requests, up to a key lifetime cap that the policy sets, at most 7 days.

Approvals, and increases of the key lifetime cap, wait for a delay that the policy chooses, up to 30 days. A guardian can cancel approvals, remove code, and revoke every key at once. Anyone can create a policy, and a policy affects only the addresses that name it.

## Motivation

An attested signature is safe only if its program key belongs to approved code. The node cannot contact an attestation service during validation, and attestation documents are too large to carry in every signature. The registry checks a document once, when a signer program boots or renews its key, and stores the result for signature verification to read.

Policies let each wallet choose its code, its attestation service, and its governance, instead of validators choosing. The delay makes every approval visible before it can sign. The guardian gives incident response a role that can stop signing but cannot approve code.

### Alternatives considered

- **Validators choosing approved code:** makes the network decide which wallets' code is trusted.
- **Storing the measurement in each key slot and checking it at signature time:** adds a storage read to every signature. Instead, each key slot stores the policy's epoch, a revocation counter, and a removal increments it. The epoch revokes every key on a removal, and approved code registers new keys when it observes the removal.
- **A new storage record for each program key:** each registration would pay [TIP-1000](./tip-1000.md)'s 250,000-gas new-slot cost, and an old key would stay current until it expired. With fixed key slots, a rotation overwrites a slot that already holds a key, and the overwrite ends the old key at once.
- **A fixed protocol-wide key lifetime:** every policy would rotate on the protocol's schedule. An operator that checks every attested signature against its own approval log detects a leaked program key without the lifetime ([TIP-1159](./tip-1159.md#observability)), so the policy chooses the lifetime. The cap bounds what the operator can choose, and an increase waits for the delay.
- **Revoking keys on anchor removal:** anchors rotate routinely, and each rotation would interrupt signing. `revokeKeys` covers a compromised anchor.
- **A fixed protocol-wide delay:** different wallets hold different value. The delay is per policy and visible to wallets.

## Assumptions

- At least one attestation scheme is active ([TIP-1162](./tip-1162.md), [TIP-1163](./tip-1163.md)).
- Policy owners propose only measurements that independent rebuilds reproduce ([TIP-1159](./tip-1159.md#signer-program-requirements)).
- A creator serving several Tempo chains controls the same address on each.

---

# Specification

## Overview

This TIP owns steps 1 and 2 of the [attested signer lifecycle](./tip-1159.md#lifecycle), approval and registration. Its inputs are policy changes from a policy's owner and guardian, and attestation documents that signer programs submit. Its outcome is the set of program keys that [TIP-1160](./tip-1160.md#verification) reads as current. The terms come from [TIP-1159](./tip-1159.md#terms).

A policy holds four kinds of state:

| State | Who changes it | When a change takes effect |
|---|---|---|
| Approved measurements | Owner adds, owner or guardian removes | Additions after the delay. Removals at once, and they revoke every program key of the policy |
| Trust anchors | Owner adds, owner or guardian removes | Additions after the delay. Removals at once, for new registrations |
| Program keys | Anyone registers into a key slot with a valid document. Owner or guardian revokes all | Registration at once, for at most the key lifetime cap. It ends the key that the slot held at once. Revocation at once |
| Settings: paused flag, delay, key lifetime cap, guardian, owner | Owner. The guardian can also pause | At once, except a shorter delay and a longer key lifetime cap, which wait for the current delay |

The lifecycle of a program key:

1. The owner proposes a measurement. After the delay, it is active.
2. A signer program with that measurement boots, generates a program key, and obtains an attestation document that commits to the key's [binding](#binding).
3. Anyone calls `registerKey` for one of the policy's key slots. The scheme verifies the document, and the registry checks the measurement and freshness, then writes the key into the slot.
4. Attested signatures by the key are valid until the slot's registration expires, until another registration overwrites the slot, or until the owner or guardian revokes keys or pauses the policy.

Each policy has `KEY_SLOTS` key slots, and each slot holds at most one program key. An attested signature names its slot, so verification reads one slot. A signer program rotates its key by registering a new key into a slot, which ends the slot's earlier key. To keep a current key during a rotation, it alternates between two slots, so the new key is current before the old one ends ([TIP-1159](./tip-1159.md#program-key-registration)).

The authority boundary is step 3. The registry writes a key only for a document that the policy's scheme verifies, that commits to the key, and whose measurement is active in the policy.

Anyone can create a policy, and a policy's state affects only attested signatures that name it. Each policy uses one attestation scheme, which a [scheme TIP](#scheme-interface) defines.

## Constants

| Name | Value | Notes |
|---|---|---|
| `KEY_SLOTS` | `8` | Key slots per policy. Two slots let one signer program overlap its keys during a rotation. Eight let a policy run up to four signer programs side by side, such as in several regions, and keep the slot index a one-byte field |
| `MAX_KEY_LIFETIME` | `604,800` seconds | 7 days. Largest key lifetime cap a policy can choose. It bounds how long a leaked program key signs if no one revokes it |
| `MAX_FUTURE_SKEW` | `60` seconds | Allowance for attestation service clocks running ahead |
| `MAX_ATTESTATION_AGE` | `300` seconds | Oldest document accepted, measured from its issue time |
| `MAX_DELAY` | `2,592,000` seconds | 30 days |
| `MAX_MEASUREMENTS` | `16` | Active and pending, per policy |
| `MAX_ANCHORS` | `16` | Active and pending, per policy |

## Policy IDs

```text
policy_id = keccak256(abi.encode(creator, salt))
```

`creator` is the caller of `createPolicy`, and `salt` is a `bytes32` it chooses. Only `creator` can create a given ID on any chain, so a wallet serving several chains creates the same ID on each. Attested signatures and registrations that name a policy that does not exist on a chain are rejected there.

## Policy changes

Every change below reverts with `Unauthorized` if the caller lacks the role, and with `PolicyNotFound` if the policy does not exist. A reverted call changes no state.

### Creation

`createPolicy` stores the scheme, owner, guardian, delay, key lifetime cap, initial measurements, and initial trust anchors. Initial entries are active at once, because no attested signature can name the policy before it exists. It reverts with:

- `PolicyExists` if the ID exists;
- `ZeroAddress` if the owner is zero (a zero guardian is allowed and means no guardian);
- `UnknownScheme` if the scheme is not active;
- `DelayTooLong` if the delay exceeds `MAX_DELAY`;
- `InvalidKeyLifetime` if the key lifetime cap is zero or exceeds `MAX_KEY_LIFETIME`;
- `TooManyEntries` if either list exceeds its maximum;
- `AlreadyListed` if either list contains an entry twice;
- `InvalidAnchor` if the scheme rejects an anchor's format, or accepts no anchors and the list is not empty.

### Approvals

`proposeMeasurement` and `proposeAnchor` record the entry with an activation time of `block.timestamp + delay`. Only the owner calls them. Each reverts with `AlreadyListed` if the entry is active or pending, and with `TooManyEntries` at the maximum. `proposeAnchor` also reverts with `InvalidAnchor` as for creation.

`cancelProposal` deletes a pending entry. The owner or guardian calls it, and it reverts with `NotPending` unless the entry's activation time is in the future.

An entry is active when its activation time is non-zero and at most `block.timestamp`.

### Removals and revocation

The owner or guardian calls these, and each takes effect for every later transaction, including later in the same block.

| Call | Effect |
|---|---|
| `removeMeasurement` | Deletes an active or pending measurement and increments the policy's epoch, which revokes every program key of the policy. Reverts with `NotListed` if absent |
| `removeAnchor` | Deletes an active or pending anchor. New registrations through it fail. Registered keys stay valid until they expire. Reverts with `NotListed` if absent |
| `revokeKeys` | Increments the policy's epoch, which revokes every program key of the policy |
| `pause` | Sets the paused flag. Registrations fail and attested signatures for the policy are rejected |

A revocation ends the keys in every slot. Honest signer programs register new keys when they observe `KeysRevoked`, `PolicyPaused`, or `MeasurementRemoved` for their policy, so a revocation stops signing until approved code registers again.

### Settings

- `unpause` clears the paused flag. Only the owner calls it.
- `setGuardian` replaces the guardian, and `transferOwnership` replaces the owner. Only the owner calls them. `transferOwnership` reverts with `ZeroAddress` for a zero owner.
- `setDelay` changes the delay. Only the owner calls it, and it reverts with `DelayTooLong` above `MAX_DELAY`. An increase takes effect at once. A decrease takes effect at `block.timestamp + current delay`, and replaces any pending decrease. A later increase cancels a pending decrease.
- `setMaxKeyLifetime` changes the key lifetime cap. Only the owner calls it, and it reverts with `InvalidKeyLifetime` for zero or above `MAX_KEY_LIFETIME`. A value at most the current cap takes effect at once, and cancels any pending increase. A larger value takes effect at `block.timestamp + current delay`, and replaces any pending increase.

`setMaxKeyLifetime` waits in the opposite direction from `setDelay`. A longer key lifetime widens what a leaked program key can sign, and a longer delay narrows what the owner can do. A new cap applies to later registrations. Keys already registered keep their expiry, and `revokeKeys` ends them sooner.

Proposals already pending keep their activation times when the delay changes.

## Binding

An attestation document must commit to the program key, the chain, the registry, the policy, the key slot, and the requested lifetime:

```text
binding = keccak256(
    "tempo:attested-key" ||
    uint256(block.chainid) || ATTESTED_KEY_REGISTRY_ADDRESS || policy_id ||
    uint8(slot) || program_key_x || program_key_y || uint64(lifetime)
)
```

`slot` is one byte, and `lifetime` is eight bytes, big-endian. Each scheme defines where the document carries `binding`. A document for one policy, chain, slot, key, or lifetime cannot register another. A copy of a registration therefore cannot move its key into another slot or shorten its lifetime.

## Registration

`registerKey(policyId, slot, document, programKeyX, programKeyY, lifetime)` registers a program key into a key slot. Anyone can call it, because the document authenticates itself. A copy of a pending call registers the same key into the same slot for the same lifetime, so front-running has no effect beyond paying the fee.

The registry runs these checks in order. The first failure reverts the call with the named error, and no state changes. As for any reverted transaction, the caller pays for the gas used and the nonce advances.

1. The policy exists (`PolicyNotFound`) and is not paused (`PolicyIsPaused`).
2. The policy's scheme is active at this block (`UnknownScheme`).
3. `slot` is less than `KEY_SLOTS` (`InvalidSlot`).
4. The program key is an uncompressed P256 point on the curve, not the point at infinity (`InvalidProgramKey`).
5. The registry computes `binding` and calls the scheme's [`verify`](#scheme-interface). The scheme charges its gas and returns a result, or fails (`InvalidAttestation`).
6. If the result names a trust anchor, the anchor is active in the policy (`AnchorNotActive`).
7. The result's measurement is active in the policy (`MeasurementNotActive`).
8. `issued_at <= block.timestamp + MAX_FUTURE_SKEW` and `block.timestamp <= issued_at + MAX_ATTESTATION_AGE` (`StaleAttestation`).
9. `valid_until = min(block.timestamp + effective_lifetime, expires_at)`, and `valid_until > block.timestamp` (`StaleAttestation`). `effective_lifetime` is the key lifetime cap in effect at this block if `lifetime` is zero, and otherwise the smaller of `lifetime` and that cap.

On success, the registry writes `{key_hash, valid_until, epoch}` into the slot, with the policy's current epoch and `key_hash = bytes16(keccak256(program_key_x || program_key_y))`. The write replaces the slot's earlier key, which stops being current at once, including later in the same block. The registry emits `KeyRegistered` with `key_id = keccak256(policy_id || program_key_x || program_key_y)` and the slot.

`key_hash` keeps the 16 high-order bytes of the hash, so the slot fits one storage word. Another P256 key with the same `key_hash` takes a 2^128 second-preimage search, the security level of P256 itself.

## Scheme interface

Each scheme TIP defines one function, run by the node:

```text
verify(document, binding, policy, now) -> (measurement, issued_at, expires_at, anchor) or failure
```

| Output | Meaning |
|---|---|
| `measurement` | `bytes32` identifying the attested code. Its derivation MUST NOT change while the scheme is active |
| `issued_at` | Seconds, when the attestation service produced the document |
| `expires_at` | Seconds, when the document stops being valid, or `type(uint64).max` if the scheme defines no expiry |
| `anchor` | The trust anchor the document relied on, or zero if the scheme uses no policy anchors |

Each scheme TIP MUST also define:

- its scheme ID, maximum document size, and anchor format, or that it accepts no anchors;
- a gas charge, deducted before each expensive step from bounded properties of the document;
- that `verify` fails unless the document commits to exactly `binding`;
- that `verify` reads no state other than the policy's anchors.

| Scheme | Attestation service | TIP |
|---|---|---|
| `0x01` | AWS Nitro Enclaves | [TIP-1162](./tip-1162.md) |
| `0x02` | Google Cloud Confidential Space | [TIP-1163](./tip-1163.md) |
| `0xFE` | Development roots that the policy lists as anchors. Not active on Tempo mainnet | [TIP-1162](./tip-1162.md#development-scheme-0xfe) |

New schemes are added by hardfork.

## Node reads

[TIP-1160](./tip-1160.md#verification) checks a program key with:

```text
program_key_current(policy_id, slot, program_key) =
    policy.exists && !policy.paused && slot < KEY_SLOTS &&
    key_slot.key_hash == bytes16(keccak256(program_key)) &&
    key_slot.valid_until >= block.timestamp && key_slot.epoch == policy.epoch
```

`policy` is slot 0 of the policy, and `key_slot` is the named key slot ([Storage layout](#storage-layout)). Both are read at the transaction's position in the block. They are two cold storage reads.

## Gas

`registerKey` charges the scheme's gas plus storage for one key slot. A first write to an empty slot pays [TIP-1000](./tip-1000.md)'s new-slot cost. A rotation overwrites a slot that holds a key, and pays the cost of changing an existing slot, about 250,000 gas less ([TIP-1159](./tip-1159.md#costs)). Policy changes charge storage costs as other Tempo precompiles do. The view functions charge one cold storage read for each slot they read.

## Interfaces

### Precompile address

```solidity
address constant ATTESTED_KEY_REGISTRY_ADDRESS = 0x1161000000000000000000000000000000000000;
```

The address follows the convention of using the TIP number.

### Storage layout

```solidity
struct Policy {               // slot 0 of a policy, packed
    address owner;            // zero if the policy does not exist
    uint8 scheme;
    bool paused;
    uint64 epoch;
}

struct PolicySettings {       // packed
    uint64 delay;
    uint64 pendingDelay;
    uint64 pendingDelayAt;    // zero if no decrease is pending
    uint8 measurementCount;
    uint8 anchorCount;
}

struct KeyLifetime {          // packed
    uint64 max;
    uint64 pendingMax;
    uint64 pendingMaxAt;      // zero if no increase is pending
}

struct KeySlot {              // packed into one storage slot
    bytes16 keyHash;          // bytes16(keccak256(programKeyX || programKeyY))
    uint64 validUntil;
    uint64 epoch;
}

mapping(bytes32 => Policy) policies;                                   // slot 0
mapping(bytes32 => address) guardians;                                 // slot 1
mapping(bytes32 => PolicySettings) settings;                           // slot 2
mapping(bytes32 => mapping(bytes32 => uint64)) measurementActiveAt;    // slot 3
mapping(bytes32 => mapping(bytes32 => uint64)) anchorActiveAt;         // slot 4
mapping(bytes32 => mapping(uint8 => KeySlot)) keySlots;               // slot 5, slot index below KEY_SLOTS
mapping(bytes32 => KeyLifetime) keyLifetimes;                          // slot 6
```

### Contract interface

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IAttestedKeyRegistry {
    /// @notice Emitted when a policy is created.
    event PolicyCreated(bytes32 indexed policyId, address indexed creator, uint8 scheme, address owner, address guardian, uint64 delay, uint64 maxKeyLifetime);

    /// @notice Emitted when a measurement is proposed or listed at creation.
    event MeasurementProposed(bytes32 indexed policyId, bytes32 indexed measurement, uint64 activeAt);

    /// @notice Emitted when a measurement is removed. The policy's epoch increments in the same call.
    event MeasurementRemoved(bytes32 indexed policyId, bytes32 indexed measurement, uint64 epoch);

    /// @notice Emitted when a trust anchor is proposed or listed at creation.
    event AnchorProposed(bytes32 indexed policyId, bytes32 indexed anchor, uint64 activeAt);

    /// @notice Emitted when a trust anchor is removed.
    event AnchorRemoved(bytes32 indexed policyId, bytes32 indexed anchor);

    /// @notice Emitted when a pending measurement or anchor is cancelled.
    event ProposalCancelled(bytes32 indexed policyId, bytes32 indexed entry, bool isAnchor);

    /// @notice Emitted when every program key of a policy is revoked.
    event KeysRevoked(bytes32 indexed policyId, uint64 epoch);

    /// @notice Emitted when a policy is paused or unpaused.
    event PolicyPaused(bytes32 indexed policyId, bool paused);

    /// @notice Emitted when a delay change is made or scheduled.
    event DelayChanged(bytes32 indexed policyId, uint64 newDelay, uint64 effectiveAt);

    /// @notice Emitted when a key lifetime cap change is made or scheduled.
    event MaxKeyLifetimeChanged(bytes32 indexed policyId, uint64 newMaxKeyLifetime, uint64 effectiveAt);

    /// @notice Emitted when the guardian changes.
    event GuardianChanged(bytes32 indexed policyId, address indexed previousGuardian, address indexed newGuardian);

    /// @notice Emitted when the owner changes.
    event OwnershipTransferred(bytes32 indexed policyId, address indexed previousOwner, address indexed newOwner);

    /// @notice Emitted when a program key is registered into a slot, ending the key the slot held.
    event KeyRegistered(bytes32 indexed policyId, bytes32 indexed keyId, bytes32 indexed measurement, uint8 slot, uint64 validUntil);

    /// @notice The caller lacks the role the call requires.
    error Unauthorized();
    /// @notice The policy ID already exists.
    error PolicyExists();
    /// @notice The policy does not exist.
    error PolicyNotFound();
    /// @notice The policy is paused.
    error PolicyIsPaused();
    /// @notice The scheme is unknown or not active.
    error UnknownScheme();
    /// @notice An owner argument is the zero address.
    error ZeroAddress();
    /// @notice The delay exceeds MAX_DELAY.
    error DelayTooLong();
    /// @notice A list would exceed its maximum.
    error TooManyEntries();
    /// @notice The scheme rejects the anchor, or accepts no anchors.
    error InvalidAnchor();
    /// @notice The entry is already active or pending.
    error AlreadyListed();
    /// @notice The entry is not listed.
    error NotListed();
    /// @notice The entry is not pending.
    error NotPending();
    /// @notice The program key is not a valid uncompressed P256 point.
    error InvalidProgramKey();
    /// @notice The scheme rejected the attestation document.
    error InvalidAttestation();
    /// @notice The document relied on a trust anchor that is not active.
    error AnchorNotActive();
    /// @notice The document's measurement is not active.
    error MeasurementNotActive();
    /// @notice The document is too old, too far in the future, or expired.
    error StaleAttestation();
    /// @notice The slot is not below KEY_SLOTS.
    error InvalidSlot();
    /// @notice The key lifetime cap is zero or exceeds MAX_KEY_LIFETIME.
    error InvalidKeyLifetime();

    /// @notice Creates a policy. Initial measurements and anchors are active at once.
    /// @param salt Chosen by the caller. The ID is keccak256(abi.encode(msg.sender, salt)).
    /// @param scheme The attestation scheme.
    /// @param owner Proposes entries and changes settings. Must be non-zero.
    /// @param guardian Pauses, removes, cancels, and revokes. Zero for none.
    /// @param delay Seconds before a proposal becomes active, at most MAX_DELAY.
    /// @param maxKeyLifetime Key lifetime cap, in seconds, in [1, MAX_KEY_LIFETIME].
    /// @param measurements Initial approved measurements.
    /// @param anchors Initial trust anchors, in the scheme's format.
    /// @return policyId The new policy's ID.
    function createPolicy(
        bytes32 salt,
        uint8 scheme,
        address owner,
        address guardian,
        uint64 delay,
        uint64 maxKeyLifetime,
        bytes32[] calldata measurements,
        bytes32[] calldata anchors
    ) external returns (bytes32 policyId);

    /// @notice Returns the policy ID for a creator and salt.
    function computePolicyId(address creator, bytes32 salt) external pure returns (bytes32);

    /// @notice Proposes a measurement, active after the delay. Owner only.
    function proposeMeasurement(bytes32 policyId, bytes32 measurement) external;

    /// @notice Proposes a trust anchor, active after the delay. Owner only.
    function proposeAnchor(bytes32 policyId, bytes32 anchor) external;

    /// @notice Cancels a pending measurement or anchor. Owner or guardian.
    function cancelProposal(bytes32 policyId, bytes32 entry, bool isAnchor) external;

    /// @notice Removes a measurement and revokes every program key. Owner or guardian.
    function removeMeasurement(bytes32 policyId, bytes32 measurement) external;

    /// @notice Removes a trust anchor. Owner or guardian.
    function removeAnchor(bytes32 policyId, bytes32 anchor) external;

    /// @notice Revokes every program key of the policy. Owner or guardian.
    function revokeKeys(bytes32 policyId) external;

    /// @notice Pauses the policy. Owner or guardian.
    function pause(bytes32 policyId) external;

    /// @notice Unpauses the policy. Owner only.
    function unpause(bytes32 policyId) external;

    /// @notice Changes the delay. Increases apply at once, decreases after the current delay. Owner only.
    function setDelay(bytes32 policyId, uint64 newDelay) external;

    /// @notice Changes the key lifetime cap. Decreases apply at once, increases after the current delay. Owner only.
    function setMaxKeyLifetime(bytes32 policyId, uint64 newMaxKeyLifetime) external;

    /// @notice Replaces the guardian. Owner only.
    function setGuardian(bytes32 policyId, address newGuardian) external;

    /// @notice Replaces the owner. Owner only.
    function transferOwnership(bytes32 policyId, address newOwner) external;

    /// @notice Registers a program key into a key slot from an attestation document, ending the key the slot held. Anyone can call.
    /// @param policyId The policy.
    /// @param slot The key slot, below KEY_SLOTS. The binding names it.
    /// @param document The attestation document, in the scheme's format.
    /// @param programKeyX The program key's x coordinate.
    /// @param programKeyY The program key's y coordinate.
    /// @param lifetime Requested seconds until expiry, capped by the key lifetime cap. Zero for the cap. The binding names it.
    /// @return validUntil When the registration expires, in seconds.
    function registerKey(bytes32 policyId, uint8 slot, bytes calldata document, bytes32 programKeyX, bytes32 programKeyY, uint64 lifetime)
        external
        returns (uint64 validUntil);

    /// @notice Returns a policy's slot 0, settings, and key lifetime cap. The owner is zero if the policy does not exist.
    function policy(bytes32 policyId)
        external
        view
        returns (address owner, address guardian, uint8 scheme, bool paused, uint64 epoch, uint64 delay, uint64 pendingDelay, uint64 pendingDelayAt, uint64 maxKeyLifetime, uint64 pendingMaxKeyLifetime, uint64 pendingMaxKeyLifetimeAt);

    /// @notice Returns a measurement's activation time, or zero if absent.
    function measurementActiveAt(bytes32 policyId, bytes32 measurement) external view returns (uint64);

    /// @notice Returns an anchor's activation time, or zero if absent.
    function anchorActiveAt(bytes32 policyId, bytes32 anchor) external view returns (uint64);

    /// @notice Returns when the key slot that holds a program key as current expires, or zero if no slot does. Reads every slot.
    function keyValidUntil(bytes32 policyId, bytes32 programKeyX, bytes32 programKeyY) external view returns (uint64);
}
```

# Compatibility

| Integration | What changes |
|---|---|
| Wallet operators | Create a policy with a key lifetime cap, then register program keys from each running signer program into its own pair of key slots, before each key expires |
| Policy owners and guardians | Use a multisig owner and a separate guardian, and set the delay in proportion to the value the policy's addresses hold |
| SDKs | Expose this interface and helpers for `policy_id`, `binding`, and `key_id` |
| Wallets | Show which policy an address names, with its delay, guardian, and key lifetime cap |
| Monitors | Rebuild each proposed measurement during its delay ([TIP-1159](./tip-1159.md#observability)) |

# Observability

Every state change emits an event from [Interfaces](#interfaces). `KeyRegistered` names the measurement and the slot, so the code behind every program key is public. A policy whose signer programs are healthy shows a `KeyRegistered` event for each slot in use before that slot's key expires. Nodes SHOULD export registrations and their failures by error.

# Threat Model

The local authority boundary is [Registration](#registration). [TIP-1159](./tip-1159.md#threat-model) owns the shared actors.

| Actor | Capability and trust | Boundary check and failure consequence |
|---|---|---|
| Owner | Proposes code and anchors, changes settings | Proposals wait for the delay, and a shorter delay waits for the current delay. A compromised owner can approve malicious code after the delay, which then signs for every address of the policy |
| Guardian | Pauses, removes, cancels, revokes | Cannot approve. A compromised guardian can stop signing until the owner replaces it |
| Registration caller | Submits any document and key | The scheme verifies the document and its binding, and the registry checks the measurement and freshness. A replayed document registers only its own key for its own policy |
| Holder of an old document | Replays it | Rejected after `MAX_ATTESTATION_AGE`, or at once if its measurement was removed. Within `MAX_ATTESTATION_AGE`, the replay writes its own key back into its own slot, for its own lifetime |
| Attestation service | Fully trusted by its scheme's policies | A compromised service lets an attacker register keys for any measurement. Removing its anchors stops new registrations, and `revokeKeys` ends current keys |

Residual risks:

- A policy with a zero delay, or no guardian, trusts its owner fully. Wallets SHOULD show the delay and guardian.
- `removeAnchor` does not end keys already registered through that anchor. They expire within the key lifetime cap unless `revokeKeys` ends them sooner.
- Within `MAX_ATTESTATION_AGE` of a registration, a copy of the slot's previous document can write the previous key back into the slot, which ends the newer key. A signer program that writes each slot at most once every `MAX_ATTESTATION_AGE + MAX_FUTURE_SKEW` seconds leaves no such document. A program that restarts sooner checks its slots after registering.
- A lower key lifetime cap does not shorten keys already registered. `revokeKeys` ends them.

# Invariants

- A program key is current only if its document's measurement was active in its policy when it was registered.
- A proposed entry is never active before `block.timestamp + delay` at proposal time.
- A delay decrease never takes effect before the earlier delay elapses.
- After `removeMeasurement`, `revokeKeys`, or `pause`, no earlier program key of the policy is current, including later in the same block.
- A program key is current only while it occupies its key slot and is unexpired. A registration into a slot ends the slot's earlier key at once.
- A registration never lasts more than the key lifetime cap in effect at registration, `MAX_KEY_LIFETIME`, or past the document's expiry.
- A key lifetime cap increase never takes effect before the delay in effect at the change elapses.
- A policy's state affects only attested signatures that name it.

## Test Cases

- Creation with each error, and a second creation of the same ID.
- A proposal used one second before and at its activation time, and a cancelled proposal.
- A delay decrease followed by a proposal before it takes effect.
- Registration with each error, in order, including a document bound to another key, policy, chain, registry, slot, and lifetime.
- Re-registration of the same key, and registration after `revokeKeys`.
- A rotation that overwrites a slot, after which the earlier key fails in the same block, and two slots whose keys are current together.
- A requested lifetime above, at, and below the cap, and zero.
- A cap increase used one second before and at its effective time, and a decrease that cancels a pending increase.
- Removing a measurement and pausing earlier in the same block as an attested signature.

# Open Questions

- Should anchor additions use a shorter delay than measurement additions, so attestation service key rotations do not require planning a full delay ahead?
- Confirm the precompile storage pricing for policy changes and key slots.
