|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Deploy CVM with allowAnyDevice=true for self-join support. |
| 4 | +
|
| 5 | +This script: |
| 6 | +1. Provisions a CVM |
| 7 | +2. Deploys AppAuth contract with allowAnyDevice=true |
| 8 | +3. Creates the CVM |
| 9 | +
|
| 10 | +For replicas, use deploy_replica.py with the appId from this deployment. |
| 11 | +""" |
| 12 | + |
| 13 | +import os |
| 14 | +import json |
| 15 | +import platform |
| 16 | +import hashlib |
| 17 | +import requests |
| 18 | +from pathlib import Path |
| 19 | +from web3 import Web3 |
| 20 | +from eth_account import Account |
| 21 | + |
| 22 | +# Decrypt API key (same as deploy_replica.py) |
| 23 | +def get_machine_key(): |
| 24 | + import subprocess |
| 25 | + hostname = platform.node() |
| 26 | + plat = platform.system().lower() |
| 27 | + arch = platform.machine() |
| 28 | + if arch == "x86_64": |
| 29 | + arch = "x64" |
| 30 | + try: |
| 31 | + cpu_model = subprocess.check_output("cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d: -f2", shell=True).decode().strip() |
| 32 | + except: |
| 33 | + cpu_model = "" |
| 34 | + username = os.environ.get("USER", "") |
| 35 | + parts = f"{hostname}|{plat}|{arch}|{cpu_model}|{username}" |
| 36 | + return hashlib.sha256(parts.encode()).digest() |
| 37 | + |
| 38 | +def decrypt_api_key(): |
| 39 | + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
| 40 | + key_file = Path.home() / ".phala-cloud" / "api-key" |
| 41 | + if not key_file.exists(): |
| 42 | + return None |
| 43 | + encrypted = key_file.read_text().strip() |
| 44 | + parts = encrypted.split(":") |
| 45 | + if len(parts) != 2: |
| 46 | + return None |
| 47 | + iv = bytes.fromhex(parts[0]) |
| 48 | + ciphertext = bytes.fromhex(parts[1]) |
| 49 | + key = get_machine_key()[:32] |
| 50 | + cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) |
| 51 | + decryptor = cipher.decryptor() |
| 52 | + padded = decryptor.update(ciphertext) + decryptor.finalize() |
| 53 | + pad_len = padded[-1] |
| 54 | + return padded[:-pad_len].decode() |
| 55 | + |
| 56 | +CLOUD_API = "https://cloud-api.phala.network/api/v1" |
| 57 | +API_KEY = os.environ.get("PHALA_CLOUD_API_KEY") or decrypt_api_key() |
| 58 | +PRIVATE_KEY = os.environ.get("PRIVATE_KEY") |
| 59 | + |
| 60 | +# Base mainnet |
| 61 | +BASE_RPC = "https://mainnet.base.org" |
| 62 | +KMS_CONTRACT = "0x2f83172A49584C017F2B256F0FB2Dca14126Ba9C" |
| 63 | + |
| 64 | +# KMS Factory ABI for deploying AppAuth with allowAnyDevice |
| 65 | +KMS_FACTORY_ABI = [{ |
| 66 | + "inputs": [ |
| 67 | + {"name": "deployer", "type": "address"}, |
| 68 | + {"name": "disableUpgrades", "type": "bool"}, |
| 69 | + {"name": "allowAnyDevice", "type": "bool"}, |
| 70 | + {"name": "deviceId", "type": "bytes32"}, |
| 71 | + {"name": "composeHash", "type": "bytes32"} |
| 72 | + ], |
| 73 | + "name": "deployAndRegisterApp", |
| 74 | + "outputs": [{"name": "", "type": "address"}], |
| 75 | + "stateMutability": "nonpayable", |
| 76 | + "type": "function" |
| 77 | +}, { |
| 78 | + "inputs": [ |
| 79 | + {"name": "appId", "type": "address", "indexed": True}, |
| 80 | + {"name": "deployer", "type": "address", "indexed": True} |
| 81 | + ], |
| 82 | + "name": "AppDeployedViaFactory", |
| 83 | + "type": "event" |
| 84 | +}] |
| 85 | + |
| 86 | +def get_headers(): |
| 87 | + return {"X-API-Key": API_KEY, "Content-Type": "application/json"} |
| 88 | + |
| 89 | +def read_compose_file(): |
| 90 | + with open("docker-compose.yaml", "r") as f: |
| 91 | + return f.read() |
| 92 | + |
| 93 | +def provision_cvm(name: str, compose_content: str, node_id: int, kms_id: str): |
| 94 | + payload = { |
| 95 | + "name": name, |
| 96 | + "image": "dstack-0.5.4", |
| 97 | + "vcpu": 1, |
| 98 | + "memory": 2048, |
| 99 | + "disk_size": 20, |
| 100 | + "teepod_id": node_id, |
| 101 | + "kms_id": kms_id, |
| 102 | + "compose_file": { |
| 103 | + "docker_compose_file": compose_content, |
| 104 | + "allowed_envs": [], |
| 105 | + "features": ["kms"], |
| 106 | + "kms_enabled": True, |
| 107 | + "manifest_version": 2, |
| 108 | + "name": name, |
| 109 | + "public_logs": True, |
| 110 | + "public_sysinfo": True, |
| 111 | + "tproxy_enabled": False |
| 112 | + }, |
| 113 | + "env_keys": [], |
| 114 | + "listed": True, |
| 115 | + "instance_type": "tdx.small" |
| 116 | + } |
| 117 | + resp = requests.post(f"{CLOUD_API}/cvms/provision", headers=get_headers(), json=payload) |
| 118 | + resp.raise_for_status() |
| 119 | + return resp.json() |
| 120 | + |
| 121 | +def deploy_app_auth_any_device(compose_hash: str): |
| 122 | + """Deploy AppAuth contract with allowAnyDevice=true""" |
| 123 | + w3 = Web3(Web3.HTTPProvider(BASE_RPC)) |
| 124 | + account = Account.from_key(PRIVATE_KEY) |
| 125 | + |
| 126 | + contract = w3.eth.contract(address=KMS_CONTRACT, abi=KMS_FACTORY_ABI) |
| 127 | + |
| 128 | + # Zero device ID + allowAnyDevice=true |
| 129 | + device_id = bytes(32) |
| 130 | + compose_hash_bytes = bytes.fromhex(compose_hash.replace("0x", "")) |
| 131 | + |
| 132 | + tx = contract.functions.deployAndRegisterApp( |
| 133 | + account.address, # deployer |
| 134 | + False, # disableUpgrades |
| 135 | + True, # allowAnyDevice = TRUE! |
| 136 | + device_id, # zero device ID |
| 137 | + compose_hash_bytes # compose hash |
| 138 | + ).build_transaction({ |
| 139 | + 'from': account.address, |
| 140 | + 'nonce': w3.eth.get_transaction_count(account.address), |
| 141 | + 'gas': 500000, |
| 142 | + 'gasPrice': w3.eth.gas_price |
| 143 | + }) |
| 144 | + |
| 145 | + signed = account.sign_transaction(tx) |
| 146 | + tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) |
| 147 | + print(f" Transaction: {tx_hash.hex()}") |
| 148 | + |
| 149 | + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) |
| 150 | + |
| 151 | + # Parse AppDeployedViaFactory event to get appId |
| 152 | + logs = contract.events.AppDeployedViaFactory().process_receipt(receipt) |
| 153 | + if not logs: |
| 154 | + raise Exception("No AppDeployedViaFactory event found") |
| 155 | + |
| 156 | + app_id = logs[0]['args']['appId'] |
| 157 | + return app_id, account.address |
| 158 | + |
| 159 | +def create_cvm(app_id: str, compose_hash: str, app_auth_address: str, deployer: str): |
| 160 | + payload = { |
| 161 | + "app_id": app_id.lower().replace("0x", ""), |
| 162 | + "compose_hash": compose_hash, |
| 163 | + "encrypted_env": "", |
| 164 | + "app_auth_contract_address": app_auth_address, |
| 165 | + "deployer_address": deployer |
| 166 | + } |
| 167 | + resp = requests.post(f"{CLOUD_API}/cvms", headers=get_headers(), json=payload) |
| 168 | + resp.raise_for_status() |
| 169 | + return resp.json() |
| 170 | + |
| 171 | +def main(): |
| 172 | + if not API_KEY: |
| 173 | + print("Set PHALA_CLOUD_API_KEY or have ~/.phala-cloud/api-key") |
| 174 | + return |
| 175 | + if not PRIVATE_KEY: |
| 176 | + print("Set PRIVATE_KEY environment variable (for Base contract deployment)") |
| 177 | + return |
| 178 | + |
| 179 | + print("=" * 60) |
| 180 | + print("Deploying CVM with allowAnyDevice=true") |
| 181 | + print("=" * 60) |
| 182 | + |
| 183 | + # Step 1: Provision |
| 184 | + print("\nStep 1: Provisioning CVM resources on prod5...") |
| 185 | + compose_content = read_compose_file() |
| 186 | + provision = provision_cvm("tee-oracle-any", compose_content, 26, "kms-base-prod5") |
| 187 | + compose_hash = provision["compose_hash"] |
| 188 | + print(f" Compose Hash: {compose_hash}") |
| 189 | + |
| 190 | + # Step 2: Deploy AppAuth with allowAnyDevice=true |
| 191 | + print("\nStep 2: Deploying AppAuth contract with allowAnyDevice=true...") |
| 192 | + app_id, deployer = deploy_app_auth_any_device(compose_hash) |
| 193 | + print(f" App ID: {app_id}") |
| 194 | + print(f" Deployer: {deployer}") |
| 195 | + |
| 196 | + # Step 3: Create CVM |
| 197 | + print("\nStep 3: Creating CVM...") |
| 198 | + result = create_cvm(app_id, compose_hash, app_id, deployer) |
| 199 | + print(f" CVM ID: {result.get('id')}") |
| 200 | + print(f" Status: {result.get('status')}") |
| 201 | + |
| 202 | + print("\n" + "=" * 60) |
| 203 | + print("SUCCESS! Save this for deploying replicas:") |
| 204 | + print(f" APP_ID={app_id}") |
| 205 | + print(f" COMPOSE_HASH={compose_hash}") |
| 206 | + print("=" * 60) |
| 207 | + |
| 208 | +if __name__ == "__main__": |
| 209 | + main() |
0 commit comments