# Private Key Extension

> Build and deploy a TEE extension that stores a private key and signs messages on-chain.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

Source: https://dev.flare.network/fcc/guides/sign-extension

Build and deploy a **Trusted Execution Environment (TEE) extension** that securely stores a private key and signs arbitrary messages. This guide walks through every step — from writing the smart contract and extension handler to deploying on Coston2 and running an end-to-end test. The code for this example is available on [GitHub](https://github.com/flare-foundation/fce-sign).

New to FCC extensions?

Start with [Build Your First Extension](/fcc/guides/getting-started) for the Hello World scaffold, OPType/OPCommand customization, and the Coston2 deploy flow. How signing works at the protocol layer is split in [TEE public and private keys](/fcc/tee-keys) and [Data providers and cosigners](/fcc/data-providers).

Demonstration only

This example is for learning purposes. Storing encrypted secrets on-chain is not advisable in production — on-chain data is public and encryption can be broken over time. A production extension should use offchain channels for secret delivery.

New to Flare TEE?

A TEE extension is an offchain program that runs inside a Trusted Execution Environment. It receives **instructions** from on-chain transactions, processes them in a secure enclave, and writes the results back on-chain. The TEE framework handles attestation, key management, and message routing — you only write the business logic.

## Overview[​](#overview "Direct link to Overview")

The Private Key Manager extension demonstrates the core TEE workflow:

1.  A user sends an [Elliptic Curve Integrated Encryption Scheme (ECIES)](https://en.wikipedia.org/wiki/Integrated_Encryption_Scheme) encrypted private key on-chain via the `InstructionSender` contract.
2.  The TEE extension decrypts and stores the key inside the secure enclave.
3.  A user sends a `sign` instruction with an arbitrary message.
4.  The TEE extension signs the message with the stored key and returns the signature on-chain.

We will build this in three parts:

1.  **on-chain contract** that sends instructions;
2.  **off-chain handler** that processes them;
3.  **deployment tooling** that ties everything together.

## Architecture[​](#architecture "Direct link to Architecture")

The extension stack consists of three components running as Docker services:

-   **`extension-tee`:** Your extension code (Go, Python, or TypeScript). Receives decoded instructions from the proxy and returns results.
-   **`ext-proxy`:** The TEE extension proxy. Watches the chain for new instructions targeting your extension, forwards them to your handler, and submits results back on-chain.
-   **`redis`:** In-memory store used by the proxy for internal state.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

Before you begin, make sure you have the following installed:

-   [Docker Desktop](https://docs.docker.com/get-docker/).
-   [Foundry](https://book.getfoundry.sh/getting-started/installation) for contract compilation.
-   [Go](https://go.dev/dl/) — for deploy and registration tools in `go/tools/`.
-   [ngrok](https://ngrok.com/) — to expose your local proxy to the internet (this guide uses ngrok; any HTTPS tunnel to port 6674 works, such as Cloudflare Tunnel).
-   A funded Coston2 wallet with C2FLR for gas and TEE registration fees — use the [Coston2 faucet](https://faucet.flare.network/coston2).

## Onchain Contract[​](#onchain-contract "Direct link to Onchain Contract")

The `InstructionSender` contract is the onchain entry point. It interacts with two Flare system contracts:

-   **`TeeExtensionRegistry`:** Registers extensions and routes instructions to TEE machines.
-   **`TeeMachineRegistry`:** Tracks registered TEE machines and provides random selection.

### Contract Code[​](#contract-code "Direct link to Contract Code")

contracts/InstructionSender.sol

```
// SPDX-License-Identifier: MITpragma solidity ^0.8.27;// TODO: Replace local interfaces with imports from flare-smart-contracts-v2 once published as a package.import { ITeeExtensionRegistry } from "./interfaces/ITeeExtensionRegistry.sol";import { ITeeMachineRegistry } from "./interfaces/ITeeMachineRegistry.sol";/// @title InstructionSender (sign extension)/// @author Acex/// @notice On-chain entry point for sending instructions to the sign-extension TEE.////// DO NOT MODIFY: constructor, setExtensionId(), _getExtensionId()contract InstructionSender {    /// @notice Operation type for key-related actions (UPDATE, SIGN).    // forge-lint: disable-next-line(unsafe-typecast)    bytes32 public constant OP_TYPE_KEY = bytes32("KEY");    /// @notice Command for the UPDATE_KEY action (stores an encrypted private key).    // forge-lint: disable-next-line(unsafe-typecast)    bytes32 public constant OP_COMMAND_UPDATE = bytes32("UPDATE");    /// @notice Command for the SIGN action (signs a message with the stored key).    // forge-lint: disable-next-line(unsafe-typecast)    bytes32 public constant OP_COMMAND_SIGN = bytes32("SIGN");    /// @notice Reference to the TEE extension registry contract.    ITeeExtensionRegistry public immutable TEE_EXTENSION_REGISTRY;    /// @notice Reference to the TEE machine registry contract.    ITeeMachineRegistry public immutable TEE_MACHINE_REGISTRY;    /// @notice First public extension ID. The registry reserves IDs below this    /// for system/reserved extensions; public extensions are assigned from here up.    uint256 private constant FIRST_PUBLIC_EXTENSION_ID = 0x10000; // 65536    uint256 private _extensionId;    /// @notice Initializes the contract with registry addresses.    /// @param _teeExtensionRegistry Address of the TEE extension registry.    /// @param _teeMachineRegistry Address of the TEE machine registry.    constructor(        ITeeExtensionRegistry _teeExtensionRegistry,        ITeeMachineRegistry _teeMachineRegistry    ) {        require(address(_teeExtensionRegistry) != address(0), "TeeExtensionRegistry cannot be zero address");        require(address(_teeMachineRegistry) != address(0), "TeeMachineRegistry cannot be zero address");        require(address(_teeExtensionRegistry).code.length > 0, "TeeExtensionRegistry has no code");        require(address(_teeMachineRegistry).code.length > 0, "TeeMachineRegistry has no code");        TEE_EXTENSION_REGISTRY = _teeExtensionRegistry;        TEE_MACHINE_REGISTRY = _teeMachineRegistry;    }    /// @notice Finds and sets this contract's extension id. Can only be set once.    /// DO NOT MODIFY this function.    function setExtensionId() external {        require(_extensionId == 0, "Extension ID already set.");        uint256 c = TEE_EXTENSION_REGISTRY.nextPublicExtensionId();        for (uint256 i = FIRST_PUBLIC_EXTENSION_ID; i < c; ++i) {            if (TEE_EXTENSION_REGISTRY.getTeeExtensionInstructionsSender(i) == address(this)) {                _extensionId = i;                return;            }        }        revert("Extension ID not found.");    }    /// @notice Update the stored private key by sending an encrypted key payload to the TEE.    /// @param _encryptedKey ECIES-encrypted (or otherwise sealed) private-key bytes that the    ///        TEE node will decrypt before storing the inner secp256k1 private key.    function updateKey(bytes calldata _encryptedKey) external payable {        address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);        address[] memory cosigners = new address[](0);        ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({            opType: OP_TYPE_KEY,            opCommand: OP_COMMAND_UPDATE,            message: _encryptedKey,            cosigners: cosigners,            cosignersThreshold: 0,            claimBackAddress: msg.sender        });        TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(            teeIds,            params        );    }    /// @notice Request the TEE to sign a message with the stored private key.    /// @param _message Raw bytes to sign. The TEE returns the signature in the action result.    function sign(bytes calldata _message) external payable {        address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);        address[] memory cosigners = new address[](0);        ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({            opType: OP_TYPE_KEY,            opCommand: OP_COMMAND_SIGN,            message: _message,            cosigners: cosigners,            cosignersThreshold: 0,            claimBackAddress: msg.sender        });        TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(            teeIds,            params        );    }    /// @notice Returns the cached extension ID, reverting if not yet set.    /// @return The extension ID assigned to this contract.    function _getExtensionId() internal view returns (uint256) {        require(_extensionId != 0, "Extension ID is not set.");        return _extensionId;    }}
```

note

The constructor takes the addresses of the two Flare system contracts. These are already deployed on Coston2 — the deployment tooling reads their addresses from `config/coston2/deployed-addresses.json`. This is a temporary solution because the Flare confidential compute is still in development. On release, both addresses will be available through the [`FlareContractRegistry`](/network/guides/flare-contracts-registry) contract.

### Extension ID Discovery[​](#extension-id-discovery "Direct link to Extension ID Discovery")

After the extension is registered on-chain, call `setExtensionId()` once to discover and cache the extension ID:

contracts/InstructionSender.sol

```
uint256 private constant FIRST_PUBLIC_EXTENSION_ID = 0x10000; // 65536function setExtensionId() external {    require(_extensionId == 0, "Extension ID already set.");    uint256 c = TEE_EXTENSION_REGISTRY.nextPublicExtensionId();    for (uint256 i = FIRST_PUBLIC_EXTENSION_ID; i < c; ++i) {        if (TEE_EXTENSION_REGISTRY.getTeeExtensionInstructionsSender(i) == address(this)) {            _extensionId = i;            return;        }    }    revert("Extension ID not found.");}
```

Public extension IDs start at `0x10000` (65536). IDs below are reserved for system extensions. The end-to-end test calls `setExtensionId()` automatically before sending instructions.

### Instruction Parameters[​](#instruction-parameters "Direct link to Instruction Parameters")

Every instruction sent to a TEE extension uses the `TeeInstructionParams` struct:

```
struct TeeInstructionParams {    bytes32 opType;    bytes32 opCommand;    bytes message;    address[] cosigners;    uint64 cosignersThreshold;    address claimBackAddress;}
```

-   **`opType` and `opCommand`:** Route the instruction to the correct handler in your extension. This example uses `opType = "KEY"` with two commands: `"UPDATE"` and `"SIGN"`.
-   **`message`:** Arbitrary payload. For `updateKey`, this is the ECIES-encrypted private key. For `sign`, this is the message to sign.
-   **`cosigners` and `cosignersThreshold`:** Optional multi-party signing (not used in this example).
-   **`claimBackAddress`:** Set to `msg.sender` so unused fees can be reclaimed.

### Sending Instructions[​](#sending-instructions "Direct link to Sending Instructions")

The `updateKey` function sends an encrypted private key to the TEE:

contracts/InstructionSender.sol

```
function updateKey(bytes calldata _encryptedKey) external payable {    address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);    ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({        opType: OP_TYPE_KEY,        opCommand: OP_COMMAND_UPDATE,        message: _encryptedKey,        cosigners: new address[](0),        cosignersThreshold: 0,        claimBackAddress: msg.sender    });    TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(teeIds, params);}
```

The flow is:

1.  Call `getRandomTeeIds` to select a random TEE machine registered for this extension.
2.  Build the instruction parameters with the appropriate `opType` and `opCommand`.
3.  Call `sendInstructions` on the `TeeExtensionRegistry`, forwarding the fee as `msg.value`.

The `sign` function follows the same pattern with `opCommand = "SIGN"`:

contracts/InstructionSender.sol

```
function sign(bytes calldata _message) external payable {    address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);    ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({        opType: OP_TYPE_KEY,        opCommand: OP_COMMAND_SIGN,        message: _message,        cosigners: new address[](0),        cosignersThreshold: 0,        claimBackAddress: msg.sender    });    TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(teeIds, params);}
```

Customizing the contract

When building your own extension, change the `opType` and `opCommand` constants to match your use case. The same constants must appear in both the Solidity contract and your offchain handler code. `opType` strings beginning with `F_` are reserved for Flare system operations, so do not use that prefix.

## Offchain Handler[​](#offchain-handler "Direct link to Offchain Handler")

The offchain handler is where your extension's business logic lives. This example is available in three languages, but the Go implementation uses a different architecture from Python and TypeScript.

Language

Directory

Handler file

Config file

Go

`go/internal/extension/`

`extension.go`

`go/internal/config/config.go`

Python

`python/app/`

`handlers.py`

`config.py`

TypeScript

`typescript/src/app/`

`handlers.ts`

`config.ts`

### Go Implementation[​](#go-implementation "Direct link to Go Implementation")

The Go extension exposes an HTTP server with two endpoints:

-   **`GET /state`** — reports whether a key is stored (never exposes key material).
-   **`POST /action`** — receives TEE actions and routes them by `opType` / `opCommand`.

Constants match the Solidity contract:

go/internal/config/config.go

```
const (    OPTypeKey       = "KEY"    OPCommandUpdate = "UPDATE"    OPCommandSign   = "SIGN")
```

The `processKeyUpdate` handler decrypts the instruction payload via the TEE node's `/decrypt` endpoint and stores the `secp256k1` private key:

go/internal/extension/extension.go

```
func (e *Extension) processKeyUpdate(action teetypes.Action, df *instruction.DataFixed) teetypes.ActionResult {    keyBytes, err := decryptViaNode(e.signPort, df.OriginalMessage)    if err != nil {        return buildResult(action, df, nil, 0, fmt.Errorf("decryption failed: %v", err))    }    privKey, err := parseSecp256k1PrivateKey(keyBytes)    if err != nil {        return buildResult(action, df, nil, 0, fmt.Errorf("invalid private key: %v", err))    }    e.mu.Lock()    e.privateKey = privKey    e.mu.Unlock()    return buildResult(action, df, nil, 1, nil)}
```

The `processKeySign` handler signs `OriginalMessage` bytes directly and returns ABI-encoded `(bytes, bytes)`:

go/internal/extension/extension.go

```
func (e *Extension) processKeySign(action teetypes.Action, df *instruction.DataFixed) teetypes.ActionResult {    sig, err := signECDSA(key, df.OriginalMessage)    if err != nil {        return buildResult(action, df, nil, 0, fmt.Errorf("signing failed: %v", err))    }    encoded, err := abiEncodeTwo(df.OriginalMessage, sig)    if err != nil {        return buildResult(action, df, nil, 0, fmt.Errorf("ABI encoding failed: %v", err))    }    return buildResult(action, df, encoded, 1, nil)}
```

The decryption uses the TEE node's built-in `/decrypt` endpoint on `SIGN_PORT` (default 7701). The caller ECIES-encrypts the private key using the TEE's public key (fetched from the proxy's `/info` endpoint) before sending it on-chain.

### Python and TypeScript implementations[​](#python-and-typescript-implementations "Direct link to Python and TypeScript implementations")

Python and TypeScript use a `Framework` that registers handlers by `opType`/`opCommand` pair:

python/app/handlers.py

```
def register(framework: Framework) -> None:    framework.handle(OP_TYPE_KEY, OP_COMMAND_UPDATE, handle_key_update)    framework.handle(OP_TYPE_KEY, OP_COMMAND_SIGN, handle_key_sign)
```

Each handler receives a hex-encoded `originalMessage` and returns `(data, status, error)`:

-   **`data`:** Hex-encoded return data written back on-chain, or `None` if no data;
-   **`status`:** `0` = error, `1` = success, `>=2` = pending;
-   **`error`:** Error message string if the handler failed.

The `handle_key_update` handler decrypts the instruction payload via the TEE node and stores the `secp256k1` private key; `handle_key_sign` signs the message and ABI-encodes the result — the same logic as the Go implementation, adapted to the Framework API.

### Framework Utilities (Python and TypeScript)[​](#framework-utilities-python-and-typescript "Direct link to Framework Utilities (Python and TypeScript)")

The `base/` package in Python and TypeScript provides common utilities:

Utility

Description

`hex_to_bytes` / `bytes_to_hex`

Hex encoding and decoding

`Framework`

Handler registration and HTTP server

warning

The files in `base/` are Framework infrastructure. You should not need to modify them when building your own extension.

## Project Structure[​](#project-structure "Direct link to Project Structure")

```
sign/├── contracts/                   # Solidity contracts (shared across implementations)│   ├── InstructionSender.sol│   └── interfaces/│       ├── ITeeExtensionRegistry.sol│       └── ITeeMachineRegistry.sol├── config/│   ├── extension.env            # Generated by pre-build.sh (EXTENSION_ID, INSTRUCTION_SENDER)│   ├── coston2/                 # Deployed contract addresses│   │   └── deployed-addresses.json│   └── proxy/                   # Chain-specific ext-proxy TOML configs├── scripts/                     # Primary deploy interface│   ├── use-chain.sh│   ├── pre-build.sh│   ├── start-services.sh│   ├── post-build.sh│   └── test.sh├── go/│   ├── internal/extension/      # Go business logic (modify these)│   └── tools/cmd/               # All deploy CLIs (Go-only)├── python/                      # Python implementation│   ├── app/                     # Your business logic (modify these)│   └── base/                    # Framework infrastructure (do not modify)├── typescript/                  # TypeScript implementation│   ├── src/app/                 # Your business logic (modify these)│   └── src/base/                # Framework infrastructure (do not modify)├── proxy/                       # Extension proxy (Docker build context)├── docker-compose.yaml├── .env.example├── .env.local.coston2           # Local simulated Coston2 template└── README.md
```

## Deploying and Testing on Coston2[​](#deploying-and-testing-on-coston2 "Direct link to Deploying and Testing on Coston2")

This walkthrough deploys a **local simulated TEE** against the real Coston2 chain using Docker and an ngrok tunnel. For production deployment on a GCP Confidential Space VM, see `DEPLOYMENT_STEPS.md` in the repository.

Quick start

After completing steps 0-5 (chain selection, deployer keys, ngrok tunnel, contract deploy, and indexer TOML), run:

```
./scripts/start-services.sh./scripts/post-build.sh./scripts/test.sh
```

### Step 0: Activate local simulated mode[​](#step-0-activate-local-simulated-mode "Direct link to Step 0: Activate local simulated mode")

Select the local simulated Coston2 environment and extension language:

```
./scripts/use-chain.sh local coston2 go
```

This copies `.env.local.coston2` to `.env`, setting `SIMULATED_TEE=true` and `LOCAL_MODE=false` (you are on the real Coston2 chain; only the TEE attestation is simulated).

Use `python` or `typescript` instead of `go` to run a different extension image. Run `./scripts/use-chain.sh --list` to see available chains and languages.

### Step 1: Configure deployer keys[​](#step-1-configure-deployer-keys "Direct link to Step 1: Configure deployer keys")

Edit `.env.local.coston2` and set your funded Coston2 credentials:

.env.local.coston2

```
DEPLOYMENT_PRIVATE_KEY="<your-funded-coston2-private-key-hex-no-0x>"INITIAL_OWNER="0x<your-address>"
```

Optional TEE governance overrides (if unset, `post-build` defaults to the deployer as the sole signer with threshold 1):

.env.local.coston2

```
# GOVERNANCE_SIGNERS="0xAbc...,0xDef..."# GOVERNANCE_THRESHOLD=2
```

These must match the values passed to the TEE node container, or registration fails with `InvalidGovernanceHash`.

Re-activate after editing so `.env` picks up the changes:

```
./scripts/use-chain.sh local coston2 go
```

### Step 2: Reserve a public proxy URL[​](#step-2-reserve-a-public-proxy-url "Direct link to Step 2: Reserve a public proxy URL")

`post-build.sh`, `start-services.sh`, and `test.sh` all read `EXT_PROXY_URL` from `.env`. Set it **before** deploying the contract or starting Docker services.

Security — read before exposing port 6674

The `ngrok http 6674` command makes your local **ext-proxy** public. Port **6674** exposes the proxy HTTP API, and anyone with the URL can call it.

Use ngrok **only for Coston2 testnet**, stop the tunnel when finished.

In a separate terminal, start the tunnel to the proxy's public port (the proxy is not running yet — ngrok will forward traffic once Step 5 starts `ext-proxy`):

```
ngrok http 6674
```

Copy the HTTPS URL from ngrok's **Forwarding** line and set it in `.env.local.coston2`:

.env.local.coston2

```
EXT_PROXY_URL="https://<your-ngrok-domain>"
```

Reactivate the environment:

```
./scripts/use-chain.sh local coston2 go
```

info

The `ngrok` free tier provides one reserved domain, so this URL normally stays stable across restarts.  
Only update `EXT_PROXY_URL` if the domain changes.

### Step 3: Deploy contract and register extension[​](#step-3-deploy-contract-and-register-extension "Direct link to Step 3: Deploy contract and register extension")

```
./scripts/pre-build.sh
```

This compiles Solidity, deploys `InstructionSender`, and registers the extension on-chain. On success, it writes `EXTENSION_ID` and `INSTRUCTION_SENDER` to `config/extension.env`.

warning

Once `config/extension.env` exists, the pre-build step reuses it and skips minting a new extension. Use `./scripts/pre-build.sh --force` only when you intentionally want a new extension. Forcing a new pre-build deploys a new `InstructionSender` and registers a new extension ID on-chain. The TEE machine you already registered stays on-chain under the previous extension ID, but your local config now points to the new one. That mismatch causes the end-to-end test to fail with `MachineManager.TooMany()`.

### Step 4: Configure the indexer database[​](#step-4-configure-the-indexer-database "Direct link to Step 4: Configure the indexer database")

The local `ext-proxy` queries Flare's C-chain indexer to find TEE events. Create the Coston2 proxy config from the bundled example:

```
cp config/proxy/extension_proxy.coston2.docker.toml.example \ config/proxy/extension_proxy.coston2.docker.toml
```

Edit the `[db]` block in `config/proxy/extension_proxy.coston2.docker.toml` with the Coston2 indexer host and the read-only credentials provided to you (not published in this guide):

config/proxy/extension\_proxy.coston2.docker.toml

```
[db]host = "34.38.42.208"port = 3306database = "indexer"username = "<your-indexer-db-username>"password = "<your-indexer-db-password>"log_queries = false
```

Flare Indexer Access

To get the indexer credentials, please get in touch with us via [support](https://flare.network/resources/technical-support) or [X](https://x.com/FlareDevs) and share what you are building.

### Step 5: Start the extension stack[​](#step-5-start-the-extension-stack "Direct link to Step 5: Start the extension stack")

```
./scripts/start-services.sh
```

This builds the extension image for your `LANGUAGE`, then starts redis, ext-proxy, and extension-tee. The `start-services.sh` script waits for `EXT_PROXY_URL/info` — with ngrok already running from Step 2, that check goes through your public tunnel to the local proxy.

Wait for the proxy to become healthy locally:

```
until curl -sf http://localhost:6674/info >/dev/null 2>&1; do sleep 2; doneecho "Extension proxy is ready"
```

Confirm only the extension proxy is listening on port 6674:

```
lsof -i :6674curl -sf http://localhost:6674/info | jq .
```

Switching languages

Change `LANGUAGE` in `.env.local.coston2`, re-run `use-chain.sh`, then re-run `start-services.sh` to rebuild the extension image.

### Step 6: Verify the proxy[​](#step-6-verify-the-proxy "Direct link to Step 6: Verify the proxy")

Confirm the simulated TEE is reporting the correct extension ID:

```
curl -s "$EXT_PROXY_URL/info" | jq '.machineData'
```

For a simulated TEE, expect:

Field

Expected

`codeHash`

Simulated hash (`0x194844cf…`)

`extensionId`

Matches `EXTENSION_ID` in `config/extension.env`

`initialOwner`

Matches your `INITIAL_OWNER`

### Step 7: Register the TEE machine[​](#step-7-register-the-tee-machine "Direct link to Step 7: Register the TEE machine")

```
./scripts/post-build.sh
```

This runs three onchain steps:

1.  **`allow-tee-version`:** allows the code hash and platform for your extension.
2.  **`set-governance`:** registers the TEE governance signer set and threshold for the extension (defaults to `INITIAL_OWNER` with threshold 1 unless you set `GOVERNANCE_SIGNERS` / `GOVERNANCE_THRESHOLD`).
3.  **`register-tee -command rRap`:** pre-registers the TEE, requests fresh attestation, runs the FTDC availability check, and promotes to production.

The capital `R` in `rRap` issues a fresh attestation challenge on re-runs, avoiding `Verification.ChallengeExpired`. Set `SIMULATED_TEE=true` in your `.env` — it pairs with `MODE=1` injected by Docker Compose for simulated attestation.

### Step 8: Run the end-to-end test[​](#step-8-run-the-end-to-end-test "Direct link to Step 8: Run the end-to-end test")

```
./scripts/test.sh
```

The test performs the following sequence:

1.  Calls `setExtensionId()` on the `InstructionSender` contract to discover and store the extension ID.
2.  Fetches the TEE's public key from the proxy's `/info` endpoint.
3.  ECIES encrypts a test private key using the TEE's public key.
4.  Sends an `updateKey` instruction on-chain with the encrypted key.
5.  Waits for the TEE to process the instruction and store the key.
6.  Sends a `sign` instruction on-chain with a test message.
7.  Verifies the returned ECDSA signature matches the test private key.

If the test passes, your extension is fully operational.

## Port Reference[​](#port-reference "Direct link to Port Reference")

Service

Container port

Host port

ext-proxy internal

6663

6673

ext-proxy external

6664

6674

redis

6379

6382

The `ngrok` tunnel exposes host port **6674** (ext-proxy external) to the internet. The internal port (6673) is used for communication between the extension container and the proxy within the Docker network.

Only bind ext-proxy to 6674 during this walkthrough — do not run other services on that port while the tunnel is active.

## Building your own Extension[​](#building-your-own-extension "Direct link to Building your own Extension")

To create your own TEE extension using this template:

1.  **Clone the repository** and pick a language (`go`, `python`, or `typescript`).
2.  **Define your instruction types** — Choose `opType` and `opCommand` constants that describe your extension's operations.
3.  **Modify `contracts/InstructionSender.sol`** — Update the contract functions to use your new constants and accept the appropriate parameters.
4.  **Write your handlers** — Implement business logic in `go/internal/extension/` (Go) or `app/` (Python/TypeScript).
5.  **Deploy and test** — Follow the steps in this guide; deploy tooling stays in `go/tools/` regardless of language.

What to change vs. what to keep

Modify your handler directory and `contracts/InstructionSender.sol`. The `base/` packages (Python/TypeScript) and `go/tools/` deploy CLIs are framework infrastructure and should not need changes.

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

**Proxy won't start or DB sync error**

The proxy needs access to the C-chain indexer database. Check the proxy logs and verify the DB credentials in `config/proxy/extension_proxy.coston2.docker.toml`:

```
docker compose logs ext-proxy
```

**Transaction reverts**

Ensure your wallet has enough C2FLR for gas and fees. Fund it with the [Coston2 faucet](https://faucet.flare.network/coston2).

**`MachineManager.TooMany()` during test**

The extension ID in `config/extension.env` does not match the on-chain TEE record — usually because `pre-build.sh --force` registered a new extension. In contrast, an older TEE machine is still registered under the previous extension ID. Run a [full reset](#cleanup) and start again from step 0, or keep the existing `config/extension.env` and re-run only `post-build.sh` and `test.sh`.

**`Verification.ChallengeExpired`**

Re-run `post-build.sh`.

**`InvalidGovernanceHash`**

The governance signer set used by `set-governance` does not match the `governanceHash` signed by the TEE node. Leave `GOVERNANCE_SIGNERS` / `GOVERNANCE_THRESHOLD` unset for the default deployer-only setup, or ensure both `.env` and the node container receive the same values, then re-run `post-build.sh`.

**`code hashes do not match`**

The `SIMULATED_TEE` variable and the container `MODE` environment variable disagree. For local simulated deployment, use `SIMULATED_TEE=true` with `MODE=1` (injected by Docker Compose).

**TEE registration times out**

Try restarting the proxy — it may have missed a signing policy round:

```
docker compose restart ext-proxy
```

If that doesn't help, the FDC attestation flow requires active relay providers on Coston2.

**`ngrok` URL changed**

1.  Update `EXT_PROXY_URL` in `.env.local.coston2` and re-run `use-chain.sh`.
2.  Restart the ngrok tunnel if needed (`ngrok http 6674`).
3.  Restart the Docker stack: `./scripts/stop-services.sh && ./scripts/start-services.sh`.
4.  Re-run `post-build.sh` and `test.sh`.

## Cleanup[​](#cleanup "Direct link to Cleanup")

**Stop the Docker stack**

```
./scripts/stop-services.sh
```

This stops and removes all containers (redis, ext-proxy, extension-tee).

**Full reset**

To completely reset local state and start from scratch:

```
./scripts/stop-services.shdocker compose down --rmi localrm -f .env config/extension.env config/proxy/extension_proxy.coston2.docker.toml
```

After a full reset, start again from [Step 0](#step-0-activate-local-simulated-mode).

To mint a new extension after a diamond redeploy without a full reset:

```
./scripts/pre-build.sh --force
```

info

On-chain state (deployed contracts, registered extensions, registered TEEs) cannot be reset. Each fresh `pre-build.sh` deploys a new `InstructionSender` contract and registers a new extension. This is fine for testing — Coston2 is a testnet.

What's next?

Read the [Flare Confidential Computing (FCC) overview](/fcc/overview) for more information on how to build and deploy TEE extensions on Flare.
