# Archive Signer
This document outlines the design of the Archive Signer — a TEE application for long-term support of legacy HOT Wallet keys.
## Background
HOT Labs operates a threshold MPC network for ECDSA (secp256k1) and EdDSA (ed25519) signing on behalf of HOT Wallet users. HOT Labs wants to retire this network.
To support this, we'll provide a standalone application running inside a Trusted Execution Environment (TEE) that holds the reconstructed full private keys and handles signing requests. HOT will then be able to operate an instance of this application.
### Scope
The Archive Signer is a custom lightweight binary that replaces HOT's MPC network ([`near/hot-mpc`][hot-mpc]). It must:
- **Sign ECDSA (secp256k1) and EdDSA (ed25519)** using the [reconstructed][key-import] full private keys directly with `k256` and `ed25519-dalek` (_not_ threshold signatures).
- **Receive signature requests via an off-chain HTTP API** compatible with [`hot_protocol::MpcClient`][mpc-client]. HOT's existing backend already sends sign requests via HTTP. Current volume is \~25k requests/day (bearish market baseline) with spikes to 25+ TPS during campaigns (airdrops, mints, claims). On-chain requests would add per-request gas costs (\~$0.001/tx → \~$750/month at current low volume, scaling with activity), latency, and require the [_Block Event Subscriber_][block-event-subscriber].
- **Authorize requests** using HOT's existing [`Validation::verify()`][validation-verify] with [`ProofModel`][proof-model] — looks up wallet contract via user's [`Uid`][uid], may delegate to cross-chain auth calls. We refer to [http-signing-api](#http-signing-api) for a detailed overview of the authentication and response mechanism.
- **Submit TEE attestation** on-chain to a dedicated HOT governance contract.
- **Monitor the HOT governance contract** for allowed Docker image hashes and launcher compose hashes.
[key-import]: #key-import-process
[block-event-subscriber]: chain-gateway-design.md#block-event-subscriber
[hot-mpc]: https://github.com/near/hot-mpc
[mpc-client]: https://github.com/near/hot-mpc/blob/bd19508821ceb974e107e701cc106866b1442d6f/node/src/hot_protocol/mpc_client.rs
[validation-verify]: https://github.com/hot-dao/hot-validation-sdk/blob/2c669f97d547d2fc9cfb011ff207282590aa8bc5/core/src/lib.rs#L143
[proof-model]: https://github.com/hot-dao/hot-validation-sdk/blob/2c669f97d547d2fc9cfb011ff207282590aa8bc5/primitives/src/validation.rs#L7-L12
[uid]: https://github.com/hot-dao/hot-validation-sdk/blob/2c669f97d547d2fc9cfb011ff207282590aa8bc5/primitives/src/uid.rs#L11
## Architecture Overview
### Component Diagram
```mermaid
---
title: Archive Signer
---
flowchart TB
subgraph CVM["CVM"]
subgraph DSTACK["Dstack Runtime"]
LAUNCHER["Launcher
(verifies image hash)"]
subgraph HOT_APP["Archive Signer Container"]
SIGNING[
Signing
HTTP Server
Validation SDK
Signing Engine
In-Memory Key Store
]
TEE_GOV[
TEE Governance
TEE Context
Chain Gateway
]
end
end
DISK[("Encrypted Disk
(Gramine Key Provider)")]
end
CLIENT["Authenticated Accounts"]
HOT_CONTRACT["HOT TEE Governance
Contract (NEAR)"]
RPC["Foreign Chain RPCs"]
CLIENT -->|"POST /sign"| SIGNING
SIGNING -->|"cross-chain auth"| RPC
SIGNING ---|"persist / load on boot"| DISK
TEE_GOV -->|"view: allowed hashes, foreign chain policy
call: submit_participant_info"| HOT_CONTRACT
classDef cvm stroke:#1b5e20,stroke-width:4px;
classDef app stroke:#2563eb,stroke-width:4px;
classDef ext stroke:#7c3aed,stroke-width:2px;
class CVM cvm;
class HOT_APP app;
class HOT_CONTRACT ext;
class CLIENT ext;
class RPC ext;
```
### Shared Components
The Archive Signer is built on three reusable layers from this repository. For the shared boot, attestation, governance, and upgrade patterns, see [TEE Lifecycle][tee-lifecycle].
- **[Embedded Indexer Node](#embedded-indexer-node)** — runs a full `near-indexer` (including `neard`) inside the CVM for trustless NEAR chain access.
- **[Chain Gateway][chain-gateway-design]** — sits on top of the embedded indexer node; provides `ContractStateSubscriber` (reads contract state) and `TransactionSender` (submits transactions to the NEAR network).
- **[TEE Context][tee-context]** — sits on top of the Chain Gateway; provides the contract interface for the [attestation lifecycle][tee-lifecycle].
[tee-lifecycle]: tee-lifecycle.md
[chain-gateway-design]: chain-gateway-design.md
### Crate Dependencies
```mermaid
---
title: Archive Signer Dependencies
---
flowchart TB
subgraph SERVICES["Services"]
direction LR
MPC_NODE["MPC Node"]
BACKUP["Backup Service"]
HOT_APP["Archive Signer"]
end
MPC_CTX[
MPC Context
Signature Requests
Key Events
Block Event Subscriber
]
TEE_CTX[
TEE Context
tee-authority
mpc-attestation
]
subgraph CHAIN["Chain Gateway"]
direction LR
CSUB["Contract State Subscriber"]
TSEND["Transaction Sender"]
end
MPC_NODE --> MPC_CTX
BACKUP --> TEE_CTX
HOT_APP --> TEE_CTX
MPC_CTX --> TEE_CTX
MPC_CTX --> CHAIN
TEE_CTX --> CHAIN
classDef new stroke:#d97706,stroke-width:3px;
class HOT_APP new;
```
### Embedded Indexer Node
The Archive Signer embeds a full `near-indexer` (which includes a `neard` node), the same as the MPC node, rather than using a lightweight RPC client. The embedded neard is used for **TEE governance operations**: monitoring the HOT governance contract for allowed Docker image hashes, launcher compose hashes, and foreign chain policy, and submitting TEE attestation transactions.
The Archive Signer depends on the TEE Context interface. The Chain Gateway (`ContractStateSubscriber`, [`TransactionSender`][tx-sender]) is an internal dependency of the TEE Context — the Archive Signer does not use it directly.
[tx-sender]: https://github.com/near/mpc/blob/ce53324f472aa89fdf702d7482211bbdb6a44967/crates/node/src/indexer/tx_sender.rs#L22
[hot-validation-core]: https://github.com/hot-dao/hot-validation-sdk/tree/2c669f97d547d2fc9cfb011ff207282590aa8bc5/core
## Key Import Process
### Overview
Key import is a one-time operation performed at initial deployment. HOT MPC network operators export their threshold keyshares via the [`ExportKeyshare`][export-keyshare] CLI command, which are then encrypted to the CVM's ephemeral public key and delivered via an HTTP endpoint (see [Phase 2][phase-2]). The application decrypts the keyshares, reconstructs the full private keys, and verifies them against the expected public keys.
[phase-2]: #phase-2-encryption-and-delivery
### Phase 1: Export (HOT Operators)
Each HOT MPC node operator runs the `ExportKeyshareCmd` CLI command (from [`hot-mpc/node/src/cli.rs`][export-keyshare]):
```rust
#[derive(Parser, Debug)]
pub struct ExportKeyshareCmd {
/// Path to home directory
#[arg(long, env("MPC_HOME_DIR"))]
pub home_dir: String,
/// Hex-encoded 16 byte AES key for local storage encryption
#[arg(help = "Hex-encoded 16 byte AES key for local storage encryption")]
pub local_encryption_key_hex: String,
}
```
This currently **only exports ECDSA keyshares** — it calls `legacy_ecdsa_key_from_keyshares()` (see [`keyshare/compat.rs`][keyshare-compat]) which only handles `KeyshareData::Secp256k1`. It must be extended to also export EdDSA keyshares (this work is on the HOT codebase side).
[export-keyshare]: https://github.com/near/hot-mpc/blob/bd19508821ceb974e107e701cc106866b1442d6f/node/src/cli.rs#L411-L459
[keyshare-compat]: https://github.com/near/hot-mpc/blob/bd19508821ceb974e107e701cc106866b1442d6f/node/src/keyshare/compat.rs
### Phase 2: Encryption and Delivery
Keyshares are encrypted **to the CVM's public key** so that only the TEE can decrypt them. This follows the same principle as [#2094][issue-2094] (secure provisioning of AES migration key for MPC nodes): the decryption key never exists outside the TEE, protecting against a compromised host intercepting keyshare material.
[issue-2094]: https://github.com/near/mpc/issues/2094
The key import is a two-phase process:
#### Phase 2a: CVM Boot and Encryption Key Publication
1. The CVM boots and the Archive Signer starts.
2. On first boot (no keys on encrypted disk), the app generates an **ephemeral encryption key pair** inside the TEE.
3. The app exposes the ephemeral public key via a local HTTP endpoint (`GET /import/public_key`). This endpoint is only available during the initial import phase and is disabled after key reconstruction succeeds.
4. The app generates a TDX attestation quote binding the ephemeral public key in `report_data`, so operators can verify the public key belongs to a genuine TEE running the approved image.
#### Phase 2b: Operator Encrypts and Delivers Keyshares
1. Each operator fetches the CVM's ephemeral public key from `GET /import/public_key` and verifies the accompanying TDX attestation to confirm it originates from a genuine TEE.
2. Each operator encrypts their exported keyshare to the CVM's ephemeral public key.
3. The encrypted keyshares are delivered to the app via `POST /import/keyshares`.
A CLI tool will be provided for these steps (fetch, verify, encrypt, deliver).
An attacker who controls the host could substitute a fake CVM with their own encryption key. The TDX attestation quote binding the ephemeral public key defends against this — operators must verify the attestation before encrypting. An attacker would need to compromise **both** the host and forge a valid TDX attestation.
### Phase 3: Reconstruction (Inside TEE)
The TEE application decrypts the keyshares using its ephemeral private key and reconstructs the full private keys.
### Phase 4: Verification
After reconstruction, the application derives the ECDSA public key from the reconstructed ECDSA private key and the EdDSA public key from the reconstructed EdDSA private key, then compares each against the known HOT wallet root public key for that domain. This detects corrupted keyshares, tampered ciphertext, or shares from different key generation epochs.
If verification succeeds, the keys are written to the CVM's encrypted disk for subsequent boots (see [Redundancy & Recovery][redundancy-recovery]).
[redundancy-recovery]: #redundancy-and-recovery
If verification fails, the application logs the error and exits without starting the HTTP server.
## TEE Context
The TEE Context is described in the [TEE Context design doc][tee-context]. It is a shared crate managing the attestation lifecycle — polling allowed hashes, periodic attestation submission, attestation removal monitoring, and polling foreign chain policy. The Archive Signer uses it directly, configured to talk to the HOT governance contract. The foreign chain policy task provides the active `ForeignChainPolicy` to the validation SDK so it knows which RPC providers to trust for each chain.
[tee-context]: tee-context-design.md
## HTTP Signing API
The HTTP API is designed to be compatible with HOT's existing interface so their backend can switch to the Archive Signer without client-side changes.
### Request and Response Types
```rust
/// Signature request — compatible with HOT's OffchainSignatureRequest.
///
/// The `uid` field is a HOT-specific user identifier from the
/// `hot-validation-primitives` crate. The tweak is derived from it
/// via `uid.to_tweak()`, which produces a 32-byte value used for
/// child key derivation.
#[derive(Debug, Clone, Deserialize)]
pub struct SignRequest {
/// HOT user identifier — tweak is derived from this.
pub uid: Uid,
/// The message to sign.
/// ECDSA: exactly 32 bytes (pre-hashed).
/// EdDSA: 1-1232 bytes (raw message).
pub message: Vec,
/// Which signature scheme to use.
pub key_type: KeyType,
/// HOT validation proof (authentication).
pub proof: ProofModel,
}
/// Signature response — compatible with HOT's OffchainSignatureResponse
/// (see hot-mpc/node/src/hot_protocol/types.rs:121-133).
///
/// ECDSA: uses identical k256 types — serialization is compatible.
/// EdDSA: uses ed25519-dalek instead of cait_sith's frost_ed25519 — must validate compatibility.
#[derive(Debug, Clone, Serialize)]
pub enum SignResponse {
Ecdsa {
big_r: k256::AffinePoint,
signature: k256::Scalar,
public_key: k256::AffinePoint,
},
Eddsa {
signature: ed25519_dalek::Signature,
public_key: ed25519_dalek::VerifyingKey,
},
}
```
**Note on response format compatibility:** The current HOT MPC network returns `OffchainSignatureResponse` (defined in [`hot-mpc/node/src/hot_protocol/types.rs:121-133`][offchain-sig-response]). For **ECDSA**, HOT already uses `k256` types (`k256::AffinePoint`, `k256::Scalar`) — identical to our `SignResponse`, so JSON serialization is compatible out of the box. For **EdDSA**, HOT uses cait_sith's `frost_ed25519::Signature` and `frost_ed25519::VerifyingKey`, while the Archive Signer uses `ed25519_dalek::Signature` and `ed25519_dalek::VerifyingKey`. These must serialize identically in JSON. If they do not, we may need to use cait_sith's types in the EdDSA response or write custom serialization. This must be validated during implementation.
[offchain-sig-response]: https://github.com/near/hot-mpc/blob/kuksag/hot-protocol/node/src/hot_protocol/types.rs
### Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/sign` | Sign a message |
| `GET` | `/import/public_key` | Get CVM's ephemeral public key + TDX attestation (first boot only) |
| `POST` | `/import/keyshares` | Deliver encrypted keyshares (first boot only) |
### Request Authorization
The Archive Signer reuses HOT's existing authorization model via [`hot-validation-sdk`][hot-validation-sdk] (`hot-validation-core` and `hot-validation-primitives` crates). Every sign request includes a caller-constructed `ProofModel`, and the Archive Signer verifies it before signing. The entire signing flow (request → validation → signature → response) is **off-chain** — no on-chain transactions are involved.
1. **Caller** (HOT's backend) sends a `SignRequest` containing `uid`, `message`, `key_type`, and `proof: ProofModel`.
2. **Archive Signer** derives `wallet_id = SHA256(uid)` and calls [`Validation::verify(wallet_id, message, proof)`][validation-verify].
3. **Validation SDK** makes RPC calls to look up the user's wallet contract on NEAR (`mpc.hot.tg`) and calls `hot_verify()` on it, passing the proof.
4. The wallet contract either returns a **bool** directly, or returns a `HotVerifyAuthCall` — parameters for a **cross-chain auth call** (target chain, contract address, method, input data).
5. If a cross-chain auth call is needed, the SDK makes an RPC call to the target chain (EVM, Cosmos, Stellar, TON, Solana) using chain-specific verifiers with threshold voting (multiple RPCs must agree). All cross-chain orchestration is internal to [`hot-validation-core`][hot-validation-core].
6. If verification passes, the Archive Signer proceeds to sign. If not, it returns `401 UNAUTHORIZED`.
This is the same authorization flow the HOT MPC network uses today. The key difference: in the MPC network, **every node** independently validates each request. In the Archive Signer, there is only one node, so validation happens once.
`ProofModel` is constructed off-chain by the caller:
```rust
pub struct ProofModel {
pub message_body: String,
pub user_payloads: Vec,
}
```
**Cross-chain RPC configuration:** The `Validation` struct requires RPC endpoints for every chain HOT wallets may reference in auth calls. Which providers are trusted for each chain is governed on-chain via the HOT governance contract using the same [`ForeignChainPolicy`][foreign-chain-policy] mechanism as the MPC signer contract. Governors vote on a `ForeignChainPolicy` (chains → RPC providers); the Archive Signer reads the active policy from the contract via the TEE Context's [foreign chain policy polling task][tee-context]. The validation SDK makes HTTP RPC calls to the governor-approved providers for all chain calls; the embedded neard is **not** used for validation — it is strictly for TEE governance. The `hot-validation-sdk` currently accepts RPC configuration only at initialization (in the existing HOT MPC network, providers are set per-deployment via local config); it will need to be extended to support dynamic provider updates from the governance contract.
[hot-validation-sdk]: https://github.com/hot-dao/hot-validation-sdk
[foreign-chain-policy]: https://github.com/near/mpc/blob/ce53324f472aa89fdf702d7482211bbdb6a44967/crates/contract-interface/src/types/foreign_chain.rs#L570
### Signing Endpoint
```rust
async fn handle_sign(
State(state): State>,
Json(request): Json,
) -> Result, StatusCode> {
// 1. Validate the request using HOT's validation SDK
state.validation.verify(
request.uid.to_wallet_id(),
request.message.clone(),
request.proof.clone(),
).await.map_err(|_| StatusCode::UNAUTHORIZED)?;
// 2. Derive tweak from uid
let tweak_bytes: [u8; 32] = request.uid.to_tweak();
// 3. Sign based on key type
match request.key_type {
KeyType::Ecdsa => sign_ecdsa(state, tweak_bytes, &request.message),
KeyType::Eddsa => sign_eddsa(state, tweak_bytes, &request.message),
}
.map(Json)
.map_err(|e| {
tracing::error!("Signing failed: {e:?}");
StatusCode::INTERNAL_SERVER_ERROR
})
}
```
## On-Chain Contract (HOT TEE Governance)
### Overview
A dedicated NEAR smart contract manages TEE governance for the Archive Signer. This is **separate** from the MPC signer contract (`v1.signer`). The contract is structurally similar to the TEE-related subset of the MPC contract (see [`crates/contract/src/tee/`][tee-dir]).
[tee-dir]: https://github.com/near/mpc/tree/ce53324f472aa89fdf702d7482211bbdb6a44967/crates/contract/src/tee
### State
```rust
pub struct HotTeeContract {
/// Set of accounts that can vote on image hashes.
/// Multiple entities.
governors: BTreeSet,
/// Number of governor votes required to approve an action.
vote_threshold: u32,
/// TEE state: allowed image hashes, attestations.
/// Reuses the existing TeeState structure from mpc-contract.
tee_state: TeeState,
/// Trusted RPC providers per chain for cross-chain authorization.
/// Reuses the existing ForeignChainPolicy from contract-interface.
foreign_chain_policy: ForeignChainPolicy,
foreign_chain_policy_votes: ForeignChainPolicyVotes,
}
```
The [`TeeState`][tee-state] struct is reused from the MPC contract:
[tee-state]: https://github.com/near/mpc/blob/ce53324f472aa89fdf702d7482211bbdb6a44967/crates/contract/src/tee/tee_state.rs#L92
```rust
pub struct TeeState {
pub(crate) allowed_docker_image_hashes: AllowedDockerImageHashes,
pub(crate) allowed_launcher_images: AllowedLauncherImages,
pub(crate) votes: CodeHashesVotes,
pub(crate) launcher_votes: LauncherHashVotes,
pub(crate) stored_attestations: BTreeMap,
}
```
### Governance
Although the Archive Signer is a single node (not a multi-node network), the voting mechanism is still relevant because it governs trust-critical parameters, not node coordination. The governance body consists of multiple actors with equal voting weight, ensuring no single party can unilaterally change the system. Governors vote on: (1) **allowed code** — which Docker images may touch the private keys, (2) **trusted RPC providers** — the [`ForeignChainPolicy`][foreign-chain-policy] governing which external data sources the TEE relies on for cross-chain authorization decisions, and (3) **the governor set itself**. This follows the same multi-entity voting pattern as the MPC contract.
Note: the MPC contract's [`vote_new_parameters`][vote-new-params] method does not have a direct equivalent here. In the MPC contract, `vote_new_parameters` changes the **participant set and threshold for the threshold signing protocol** (via [`ThresholdParameters`][threshold-params]), triggering a resharing. The Archive Signer is a single node doing direct signing — there is no threshold protocol, no resharing, and no signing participant set to manage. Instead, the HOT governance contract needs methods for managing its own **governor set** (see below).
[vote-new-params]: https://github.com/near/mpc/blob/ce53324f472aa89fdf702d7482211bbdb6a44967/crates/contract/src/lib.rs#L922
[threshold-params]: https://github.com/near/mpc/blob/ce53324f472aa89fdf702d7482211bbdb6a44967/crates/contract/src/primitives/thresholds.rs#L33
### Governor Management
The initial governor set and vote threshold are configured at contract deployment. After deployment, governors can vote to change the governor set and/or threshold via `vote_update_governors`. This requires `vote_threshold` votes on the same proposal to take effect — ensuring that governor changes go through the same multi-stakeholder approval as code hash changes.
### Contract Methods
| Method | Type | Caller | Description |
|--------|------|--------|-------------|
| `vote_code_hash(code_hash)` | Call | Governor | Vote for a new Docker image hash |
| `vote_remove_code_hash(code_hash)` | Call | Governor | Vote to remove a Docker image hash before natural expiry |
| `vote_add_launcher_hash(launcher_hash)` | Call | Governor | Vote for a new launcher image hash (threshold) |
| `vote_remove_launcher_hash(launcher_hash)` | Call | Governor | Vote to remove a launcher image hash (unanimity) |
| `vote_update_governors(governors, threshold)` | Call | Governor | Vote to change the governor set and/or vote threshold |
| `vote_foreign_chain_policy(policy)` | Call | Governor | Vote on trusted RPC providers per chain |
| `submit_participant_info(attestation, tls_public_key)` | Call | Archive Signer | Submit TEE attestation |
| `verify_tee()` | Call | Anyone | Re-validate all stored attestations |
| `allowed_docker_image_hashes()` | View | Archive Signer | Query approved image hashes |
| `allowed_launcher_image_hashes()` | View | Archive Signer | Query approved launcher image hashes |
| `allowed_launcher_compose_hashes()` | View | Archive Signer | Query approved launcher compose hashes |
| `get_tee_accounts()` | View | Anyone | Query nodes with valid attestations |
| `get_supported_foreign_chains()` | View | Archive Signer | Query active foreign chains (opt-in) |
### Launcher Compose Hash Derivation
Launcher compose hash derivation follows the standard mechanism described in [TEE Lifecycle: Launcher Compose Hash Derivation][tee-launcher-hash]. The HOT TEE governance contract uses its own launcher compose template, since the Archive Signer has a different Docker Compose configuration than the MPC node.
[tee-launcher-hash]: tee-lifecycle.md#voting-methods
## Attestation Flow
### Boot Flow
The generic CVM boot sequence is described in [TEE Lifecycle: CVM Boot Sequence][tee-boot-sequence]. The diagram below extends it with Archive Signer-specific key import steps.
[tee-boot-sequence]: tee-lifecycle.md#boot
```mermaid
sequenceDiagram
participant OP as HOT Operator
participant DS as Dstack
participant LA as Launcher
participant APP as Archive Signer
participant HC as HOT TEE Contract
OP ->> DS: Start CVM
DS ->> DS: Boot, extend RTMR3 with docker_compose
DS ->> LA: Start Launcher
LA ->> LA: Verify app image hash (from disk or DEFAULT_IMAGE_DIGEST)
LA ->> LA: Extend RTMR3 with app image hash
LA ->> APP: Start Archive Signer container
alt Keys found on encrypted disk (subsequent boot)
APP ->> APP: Load keys from encrypted disk
APP ->> APP: Verify keys are valid
else First boot (no keys on disk)
APP ->> APP: Generate ephemeral encryption key pair
APP ->> APP: Generate TDX attestation binding ephemeral public key
APP ->> OP: Expose GET /import/public_key (pubkey + attestation)
OP ->> OP: Verify TDX attestation
OP ->> OP: Encrypt keyshares to CVM's ephemeral public key
OP ->> APP: POST /import/keyshares (encrypted payloads)
APP ->> APP: Decrypt keyshares (asymmetric decryption)
APP ->> APP: Reconstruct ECDSA + EdDSA private keys
APP ->> APP: Verify public keys match expected values
Note right of APP: Key import happens before image hash check.
This is safe: operators already verified the
TDX attestation (including image hash in RTMR3)
before encrypting their keyshares.
end
APP ->> HC: Query allowed_docker_image_hashes()
alt Image hash is approved
APP ->> APP: Generate TDX attestation quote
APP ->> HC: submit_participant_info(attestation, tls_pk)
APP ->> APP: Start HTTP server
else Not approved
APP ->> HC: Fetch latest approved hash
APP ->> APP: Store hash to disk, exit
Note over OP: Operator restarts CVM with correct image
end
loop Every 7 days
APP ->> APP: Generate fresh attestation quote
APP ->> HC: submit_participant_info(attestation, tls_pk)
end
```
### Attestation Generation and On-Chain Verification
Attestation generation and on-chain verification follow the standard TEE lifecycle. See [TEE Lifecycle: Attestation][tee-attestation] for the full details.
[tee-attestation]: tee-lifecycle.md#attestation
## Upgrade Path
Application upgrades follow the standard [Launcher pattern][tee-upgrade]: governors vote for a new Docker image hash, the running app detects the update, the operator restarts the CVM, and the new app reattests. See [TEE Lifecycle: Application Upgrade][tee-upgrade] for the full flow.
[tee-upgrade]: tee-lifecycle.md#upgrade
## Redundancy and Recovery
Since the Archive Signer is a single node holding the full private key, redundancy is critical. A hardware failure or misconfiguration must not result in permanent loss of user funds.
### Primary: Encrypted Disk Persistence
Keys are held in memory for signing and persisted on the CVM's encrypted disk for restart resilience. The CVM's encrypted disk (key derived from RTMR measurements) provides safe at-rest storage.
After successful key reconstruction and verification on first boot, the private keys are stored on the CVM's encrypted filesystem. [Gramine Key Provider][gramine-key-provider] derives the encryption key from TDX measurements, so only the same TEE image can decrypt this data.
On subsequent boots, the app first attempts to load keys from the encrypted disk. If found and valid, it skips the import flow entirely (the `/import/*` endpoints are never exposed).
[gramine-key-provider]: https://github.com/MoeMahhouk/gramine-sealing-key-provider
### Recovery via Confidential Key Derivation (CKD)
Encrypted disk persistence protects against restarts, but not against total loss of the CVM instance — for example, if the TEE image is updated (changing RTMR measurements and invalidating the disk encryption key) or all CVM instances are destroyed. Without an additional recovery mechanism, this would be unrecoverable after the HOT MPC network is retired, since keyshares can no longer be re-imported.
To address this, we use [Confidential Key Derivation][ckd] (CKD) via the NEAR MPC network to create a hardware-independent encrypted backup:
1. **Derive a wrapping key.** After key reconstruction, the Archive Signer calls the NEAR MPC network's CKD endpoint with its `app_id`. CKD returns a deterministic, confidential key that only this application can obtain.
2. **Encrypt and store a backup.** The reconstructed HOT private keys are encrypted with the CKD-derived wrapping key. The resulting ciphertext is stored in external storage.
3. **Recover from any new instance.** A new instance re-derives the same CKD key — since CKD is deterministic on `app_id`, not on hardware measurements — and decrypts the backup.
This decouples recovery from any specific CVM instance or disk encryption key, requiring only that the NEAR MPC network remains operational.
For additional high availability, a hot standby instance (a second CVM holding the same keys) can be added in the future if needed.
[ckd]: ../crates/threshold-signatures/docs/confidential_key_derivation/confidential-key-derivation.md
## Open Questions
1. **EdDSA keyshare export:** The HOT `ExportKeyshareCmd` currently only exports ECDSA shares. It must be extended to also export EdDSA shares. This work is on the HOT codebase side.
2. **Response format byte-level compatibility:** Do `k256::AffinePoint` and `k256::Scalar` serialize to the same JSON as `cait_sith::frost_secp256k1::VerifyingKey` and `cait_sith::ecdsa::sign::FullSignature`? This must be validated. If not, we may need to depend on `cait_sith` types in the response or write adapter serialization.
3. **Dynamic RPC provider updates in validation SDK:** `hot-validation-sdk` currently accepts RPC configuration only at initialization. It needs to be extended to support dynamic updates from the governance contract's `ForeignChainPolicy`, so that governor votes to change trusted RPC providers take effect without restarting the Archive Signer.
## Related Issues
- [#2062][issue-2062] -- TEE application for long-term support of legacy keys
- [#2018][issue-2018] -- HOT keyshare import (original)
- [#2021][pr-2021] -- Design: HOT migration (closed)
- [#1891][issue-1891] -- TEE backup service
- [#2103][pr-2103] -- Indexer design proposal
[issue-2062]: https://github.com/near/mpc/issues/2062
[issue-2018]: https://github.com/near/mpc/issues/2018
[issue-1891]: https://github.com/near/mpc/issues/1891
[pr-2021]: https://github.com/near/mpc/pull/2021
[pr-2103]: https://github.com/near/mpc/pull/2103