# MPC Testnet with TEE Support This guide describes how to set up a testnet MPC cluster, where each MPC node runs inside a dstack TDX CVM. The flow uses the Rust launcher (TOML config) — the older Python launcher and its `.conf` config format have been removed (#2951, #2952). ## Prerequisites 1. One TDX-enabled machine (with 2 external IPs), or two such machines. 2. dstack installed and configured (`v0.5.5+` for reserved port forwarding). 3. MPC repository cloned. 4. NEAR CLI installed (`near-cli-rs`). ## Two flows This guide walks the **manual two-node** (`frodo` + `sam`) flow end-to-end — useful when learning the system or debugging individual steps. For deploying **N nodes at once** (or scaling up existing networks), use `localnet/tee/scripts/rust-launcher/deploy-tee-cluster.sh` with `MODE=testnet NEAR_NETWORK_CONFIG=testnet`. It automates account creation, contract deployment, key generation, attestation, and voting in a resumable script. See [`how-to-run-deploy-tee-cluster.md`](../../localnet/tee/scripts/rust-launcher/how-to-run-deploy-tee-cluster.md) for the full reference. ## High-Level Steps 1. Create a NEAR account that will host the MPC contract. 2. Deploy the MPC contract. 3. Start two TDX CVMs: Running MPC node **frodo** and running MPC node **sam** on the same machine. 4. Get account and TLS keys from each node. 5. Initialize the contract with node parameters (keys, accounts, IPs). 6. Workaround for port override issue. 7. Vote for MPC code hash on the contract. 8. Vote for launcher image hash on the contract. 9. Vote for OS measurements on the contract. 10. MPC nodes attestation submission. 11. Add a domain to the contract. 12. Submit a signing request to the MPC cluster. --- **Note:** These operations assume you start from the MPC repo root. **Note:** Both CVMs run on the same host below (one machine, two static IPs). To split the cluster across two TDX hosts instead, run the deploy command for each node on its own host and adjust the IPs. ## Step 1: Create MPC Account First, set a unique network name for the MPC cluster: ```bash # Pick a globally unique name within the team. # Include your username to avoid collisions. export MPC_NETWORK_NAME=yourusername-test ``` Define the NEAR accounts to be used: ```bash export ROOT_ACCOUNT=${MPC_NETWORK_NAME}.testnet export MPC_CONTRACT_ACCOUNT=${MPC_NETWORK_NAME}_mpc.testnet export FRODO_ACCOUNT=${MPC_NETWORK_NAME}_frodo.testnet export SAM_ACCOUNT=${MPC_NETWORK_NAME}_sam.testnet ``` Since the faucet gives only **10 NEAR per account** (and the MPC contract storage costs **15 NEAR**), we need to create two accounts and then transfer funds between them. ```bash near account create-account sponsor-by-faucet-service $ROOT_ACCOUNT \ autogenerate-new-keypair save-to-legacy-keychain network-config testnet create near account create-account sponsor-by-faucet-service $MPC_CONTRACT_ACCOUNT \ autogenerate-new-keypair save-to-legacy-keychain network-config testnet create ``` ```bash export ACCOUNT_ID=$ROOT_ACCOUNT export RECEIVER_ID=$MPC_CONTRACT_ACCOUNT near tokens $ACCOUNT_ID send-near $RECEIVER_ID '9 NEAR' network-config testnet sign-with-keychain send ``` After this, **$MPC_CONTRACT_ACCOUNT ** has **19 NEAR**, enough to deploy the MPC contract. you can review the account balance: ```bash near account view-account-summary $MPC_CONTRACT_ACCOUNT network-config testnet now ``` --- ## Step 2: Deploy MPC Contract Build the MPC contract wasm: ```bash cargo near build non-reproducible-wasm --features abi --manifest-path crates/contract/Cargo.toml --locked ``` Set the path to the built contract: ```bash export MPC_CONTRACT_PATH="$(pwd)/target/near/mpc_contract/mpc_contract.wasm" ``` Deploy the MPC contract: ```bash near contract deploy $MPC_CONTRACT_ACCOUNT use-file "$MPC_CONTRACT_PATH" without-init-call network-config testnet sign-with-keychain send ``` Inspect the deployed contract: ```bash near contract inspect $MPC_CONTRACT_ACCOUNT network-config testnet now ``` --- ## Step 3: Start the CVMs with MPC Nodes ### Create two accounts for the MPC nodes: ```bash near account create-account sponsor-by-faucet-service $FRODO_ACCOUNT autogenerate-new-keypair save-to-legacy-keychain network-config testnet create near account create-account sponsor-by-faucet-service $SAM_ACCOUNT autogenerate-new-keypair save-to-legacy-keychain network-config testnet create ``` ### Update Bootnodes and IPs > See [Running multiple MPC nodes on one host](../running-multiple-mpc-nodes-on-one-host.md) for the general framing of why each node needs its own host IP. Run this command to get the current testnet bootnodes. **Important:** Boot nodes must not contain duplicate addresses or peer IDs, as duplicates will cause the node to crash on startup. The command below deduplicates automatically: ```bash export BOOTNODES=$(curl -s -X POST https://rpc.testnet.near.org \ -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "method": "network_info", "params": [], "id": "dontcare"}' | jq -r '.result.active_peers | unique_by(.addr) | unique_by(.id) | map("\(.id)@\(.addr)") | .[]' | paste -sd',' -) ``` Define 2 external IPs available on the server that will be used by each MPC node: ```bash export SERVER_IP_1=${IP 1} # e.g., On Alice Server you can use 51.68.219.1 export SERVER_IP_2=${IP 2} # e.g., On Alice Server you can use 51.68.219.2 ``` --- ### Replace config placeholders inside the config files ```bash envsubst < deployment/testnet/frodo.toml > "/tmp/$USER/frodo.toml" ``` ```bash envsubst < deployment/testnet/sam.toml > "/tmp/$USER/sam.toml" ``` ```bash envsubst < deployment/testnet/frodo.env > "/tmp/$USER/frodo.env" ``` ```bash envsubst < deployment/testnet/sam.env > "/tmp/$USER/sam.env" ``` ### deploy the CVMs using script #### preliminry setup: Allow reserved port forwarding (e.g 80) for the CVMs. ```bash sudo setcap 'cap_net_bind_service=+ep' $(which qemu-system-x86_64) ``` #### 1. Move into the `deployment/cvm-deployment` directory ```bash cd deployment/cvm-deployment ``` #### 2. Ensure the script is executable ```bash chmod +x deploy-launcher.sh ``` #### 3. Set environment variables Set your `BASE_PATH` to the dstack directory that contains the `vmm` folder. Example: `$BASE_PATH/vmm/src/vmm-cli.py` should exist. ```bash export BASE_PATH="dstack base path" ``` Start the nodes (run both on the same host if it has both static IPs; otherwise run each on the host that owns its IP): **Frodo:** ```bash ./deploy-launcher.sh --env-file /tmp/$USER/frodo.env --base-path $BASE_PATH --python-exec python ``` **Sam:** ```bash ./deploy-launcher.sh --env-file /tmp/$USER/sam.env --base-path $BASE_PATH --python-exec python ``` > The `--python-exec python` flag refers to the Python binary used to invoke > dstack's `vmm-cli.py` — **not** to any MPC launcher. Confusingly named > but required. If successful, each command outputs an **App ID** and confirms creation of a **CVM instance**. --- ## Step 4: Get Account & TLS Keys from Each MPC Node Each node exposes its keys via: - Frodo: `http://$SERVER_IP_1:18081/public_data` - Sam: `http://$SERVER_IP_2:18082/public_data` ```bash export FRODO_PUBKEY=$(curl -s http://$SERVER_IP_1:18081/public_data | jq -r ".near_signer_public_key") export SAM_PUBKEY=$(curl -s http://$SERVER_IP_2:18082/public_data | jq -r ".near_signer_public_key") export FRODO_RESPONDER_KEY=$(curl -s http://$SERVER_IP_1:18081/public_data | jq -r ".near_responder_public_keys[0]") export SAM_RESPONDER_KEY=$(curl -s http://$SERVER_IP_2:18082/public_data | jq -r ".near_responder_public_keys[0]") export FRODO_P2P_KEY=$(curl -s http://$SERVER_IP_1:18081/public_data | jq -r '.near_p2p_public_key') export SAM_P2P_KEY=$(curl -s http://$SERVER_IP_2:18082/public_data | jq -r '.near_p2p_public_key') ``` Add keys to each NEAR account: The keys are granted access to all methods on the MPC contract (empty `--function-names` list) while staying scoped to it via `--contract-account-id`. This avoids the keys breaking whenever a release adds a method the node must call (e.g. `register_foreign_chain_config`). ```bash near account add-key $FRODO_ACCOUNT grant-function-call-access \ --allowance unlimited --contract-account-id $MPC_CONTRACT_ACCOUNT --function-names '' \ use-manually-provided-public-key "$FRODO_PUBKEY" network-config testnet sign-with-keychain send near account add-key $FRODO_ACCOUNT grant-function-call-access \ --allowance unlimited --contract-account-id $MPC_CONTRACT_ACCOUNT --function-names '' \ use-manually-provided-public-key "$FRODO_RESPONDER_KEY" network-config testnet sign-with-keychain send near account add-key $SAM_ACCOUNT grant-function-call-access \ --allowance unlimited --contract-account-id $MPC_CONTRACT_ACCOUNT --function-names '' \ use-manually-provided-public-key "$SAM_PUBKEY" network-config testnet sign-with-keychain send near account add-key $SAM_ACCOUNT grant-function-call-access \ --allowance unlimited --contract-account-id $MPC_CONTRACT_ACCOUNT --function-names '' \ use-manually-provided-public-key "$SAM_RESPONDER_KEY" network-config testnet sign-with-keychain send ``` --- ## Step 5: Initialize Contract with Node Parameters Move to the MPC root folder: ```bash cd .. ``` Prepare arguments for the init call: ```bash mkdir -p "/tmp/$USER" envsubst < docs/testnet/args/init_testnet_tee.json > "/tmp/$USER/init_args.json" ``` **Note:** Ensure `/tmp/$USER/init_args.json` has correct IPs, keys, accounts before proceeding. Example: ```json { "parameters": { "threshold": 2, "participants": { "next_id": 2, "participants": [ [ "barak_tee_test1_frodo.testnet", 0, { "tls_public_key": "ed25519:6CeuXPt6qXtXRHVb5C4USZAyQcg65LXJvebPyCJewaN1", "url": "https://91.134.92.20:13001" } ], [ "barak_tee_test1_sam.testnet", 1, { "tls_public_key": "ed25519:B2pHHn9Kr2GZhDn85VP3vGUccJK7Haekcmy5JRBScUn3", "url": "https://57.129.140.254:13002" } ] ] } } } ``` Initialize the contract: ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT init \ file-args /tmp/$USER/init_args.json prepaid-gas '300.0 Tgas' \ attached-deposit '0 NEAR' sign-as $MPC_CONTRACT_ACCOUNT \ network-config testnet sign-with-keychain send ``` Verify: ```bash near contract call-function as-read-only $MPC_CONTRACT_ACCOUNT state \ json-args '{}' network-config testnet now ``` --- ## Step 6: Workaround for Port Override Issue (optional) **Note:** `port_override = 80` is the default on testnet. The MPC nodes will advertise URL port 80 to the contract regardless of the contract's `url:` field. If you need a different port (e.g. dstack on this host can't forward port 80), change `port_override` post-deploy as follows. > SSH into the CVM is **not supported on dstack ≥ 0.5.6** unless the > launcher's `pre_launch_script` installs a passwordless SSH key (the > dstack-dev images stopped exposing SSH by default). On `dstack-0.5.5` > and earlier dev images you can SSH with `ssh root@localhost -p > ` (`1220` for frodo, `1221` for sam by default). Once on the CVM, stop the MPC container, edit the config, restart: ```bash docker stop mpc-node sed -i 's/port_override: 80/port_override: 2080/' /var/lib/docker/volumes/mpc-data/_data/config.yaml sed -i 's/port_override: 80/port_override: 2080/' /var/lib/docker/volumes/mpc-data/_data/config.json docker start mpc-node ``` --- ## Step 7: Vote MPC Hash on Contract Hash format: 00006c1059cc0219005b21956a4df8238b0cc33ad559a578a63169de4e28c81e (no prefix) ```bash export CODE_HASH= ``` ### Frodo votes ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_code_hash \ json-args "{\"code_hash\": \"$CODE_HASH\"}" prepaid-gas '100.0 Tgas' \ attached-deposit '0 NEAR' sign-as $FRODO_ACCOUNT \ network-config testnet sign-with-keychain send ``` ### Sam votes ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_code_hash \ json-args "{\"code_hash\": \"$CODE_HASH\"}" prepaid-gas '100.0 Tgas' \ attached-deposit '0 NEAR' sign-as $SAM_ACCOUNT \ network-config testnet sign-with-keychain send ``` Check the hash: ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT allowed_docker_image_hashes \ json-args '{}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' \ sign-as $SAM_ACCOUNT network-config testnet sign-with-keychain send ``` --- ## Step 8: Vote Launcher Image Hash on Contract The launcher image hash must also be voted in for compose hashes to be derived and attestation to work. The previous steps had you set `LAUNCHER_MANIFEST_DIGEST` in `deployment/testnet/*.env` (uncommented in step 6 before running `deploy-launcher.sh`). The `.env` file is sourced by `deploy-launcher.sh` but not by your interactive shell, so re-export the value here. Either re-source the env file, or set the variable directly: ```bash # Option A: pull from the env file you already edited set -a; source deployment/testnet/frodo.env; set +a # or sam.env — both should match export LAUNCHER_HASH="${LAUNCHER_MANIFEST_DIGEST#sha256:}" # Option B: paste the same hex you put in the .env export LAUNCHER_HASH= ``` ### Frodo votes ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_add_launcher_hash \ json-args "{\"launcher_hash\": \"$LAUNCHER_HASH\"}" prepaid-gas '100.0 Tgas' \ attached-deposit '0 NEAR' sign-as $FRODO_ACCOUNT \ network-config testnet sign-with-keychain send ``` ### Sam votes ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_add_launcher_hash \ json-args "{\"launcher_hash\": \"$LAUNCHER_HASH\"}" prepaid-gas '100.0 Tgas' \ attached-deposit '0 NEAR' sign-as $SAM_ACCOUNT \ network-config testnet sign-with-keychain send ``` Check the launcher hashes: ```bash near contract call-function as-read-only $MPC_CONTRACT_ACCOUNT allowed_launcher_image_hashes \ json-args '{}' network-config testnet now ``` --- ## Step 9: Vote OS Measurements on Contract OS measurements (MRTD, RTMR0-2, key-provider event digest) identify the CVM environment. Both participants must vote for each measurement set. The values can be found in `crates/mpc-attestation/assets/tcb_info.json` and `tcb_info_dev.json`. Extract the measurement values from a `tcb_info.json` file: ```bash # Extract from tcb_info.json (repeat for tcb_info_dev.json) TCB_FILE=crates/mpc-attestation/assets/tcb_info.json MRTD=$(jq -r '.mrtd' $TCB_FILE) RTMR0=$(jq -r '.rtmr0' $TCB_FILE) RTMR1=$(jq -r '.rtmr1' $TCB_FILE) RTMR2=$(jq -r '.rtmr2' $TCB_FILE) KP_DIGEST=$(jq -r '.event_log[] | select(.event == "key-provider") | .digest' $TCB_FILE) ``` ### Frodo votes ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_add_os_measurement \ json-args "{\"measurement\": {\"mrtd\": \"$MRTD\", \"rtmr0\": \"$RTMR0\", \"rtmr1\": \"$RTMR1\", \"rtmr2\": \"$RTMR2\", \"key_provider_event_digest\": \"$KP_DIGEST\"}}" \ prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' \ sign-as $FRODO_ACCOUNT network-config testnet sign-with-keychain send ``` ### Sam votes ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_add_os_measurement \ json-args "{\"measurement\": {\"mrtd\": \"$MRTD\", \"rtmr0\": \"$RTMR0\", \"rtmr1\": \"$RTMR1\", \"rtmr2\": \"$RTMR2\", \"key_provider_event_digest\": \"$KP_DIGEST\"}}" \ prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' \ sign-as $SAM_ACCOUNT network-config testnet sign-with-keychain send ``` Check the allowed measurements: ```bash near contract call-function as-read-only $MPC_CONTRACT_ACCOUNT allowed_os_measurements \ json-args '{}' network-config testnet now ``` --- ## Step 10: Check Attestation Submission ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT get_tee_accounts \ json-args '{}' prepaid-gas '300 Tgas' attached-deposit '0 NEAR' \ sign-as $SAM_ACCOUNT network-config testnet sign-with-keychain send ``` *Note:* If attestation public key is **null**, the contract uses the default mock attestation. *Note:* After attestation succeeds, both nodes spam logs aggressively (bug to fix). --- ## Step 11: Add Domain to Contract ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_add_domains \ file-args docs/localnet/args/add_domain.json prepaid-gas '300.0 Tgas' \ attached-deposit '0 NEAR' sign-as $FRODO_ACCOUNT network-config testnet sign-with-keychain send near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT vote_add_domains \ file-args docs/localnet/args/add_domain.json prepaid-gas '300.0 Tgas' \ attached-deposit '0 NEAR' sign-as $SAM_ACCOUNT network-config testnet sign-with-keychain send ``` Check status: ```bash near contract call-function as-read-only $MPC_CONTRACT_ACCOUNT state \ json-args '{}' network-config testnet now ``` If the contract is stuck in **Initializing**, this usually means the MPC nodes failed to connect. --- ## Step 12: Submit Signing Request ```bash near contract call-function as-transaction $MPC_CONTRACT_ACCOUNT sign \ file-args docs/localnet/args/sign_ecdsa.json prepaid-gas '300.0 Tgas' \ attached-deposit '100 yoctoNEAR' sign-as $FRODO_ACCOUNT \ network-config testnet sign-with-keychain send ```