Skip to content

Commit 92665b7

Browse files
committed
tested example, made 02 capable of local test w/ anvil and launching multiple nodes
1 parent 29dab51 commit 92665b7

10 files changed

Lines changed: 896 additions & 25 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Deploy a CVM replica using existing appId via direct API calls.
4+
This bypasses the CLI's limitation of always deploying a new AppAuth contract.
5+
"""
6+
7+
import os
8+
import json
9+
import platform
10+
import hashlib
11+
import requests
12+
from pathlib import Path
13+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
14+
15+
CLOUD_API = "https://cloud-api.phala.network/api/v1"
16+
17+
def get_machine_key():
18+
"""Generate machine-specific key like the CLI does"""
19+
import subprocess
20+
hostname = platform.node()
21+
plat = platform.system().lower()
22+
# Node.js os.arch() returns 'x64' not 'x86_64'
23+
arch = platform.machine()
24+
if arch == "x86_64":
25+
arch = "x64"
26+
try:
27+
cpu_model = subprocess.check_output("cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d: -f2", shell=True).decode().strip()
28+
except:
29+
cpu_model = ""
30+
username = os.environ.get("USER", "")
31+
32+
parts = f"{hostname}|{plat}|{arch}|{cpu_model}|{username}"
33+
return hashlib.sha256(parts.encode()).digest()
34+
35+
def decrypt_api_key():
36+
"""Decrypt the stored API key"""
37+
key_file = Path.home() / ".phala-cloud" / "api-key"
38+
if not key_file.exists():
39+
return None
40+
41+
encrypted = key_file.read_text().strip()
42+
parts = encrypted.split(":")
43+
if len(parts) != 2:
44+
return None
45+
46+
iv = bytes.fromhex(parts[0])
47+
ciphertext = bytes.fromhex(parts[1])
48+
key = get_machine_key()[:32]
49+
50+
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
51+
decryptor = cipher.decryptor()
52+
padded = decryptor.update(ciphertext) + decryptor.finalize()
53+
54+
# Remove PKCS7 padding
55+
pad_len = padded[-1]
56+
return padded[:-pad_len].decode()
57+
58+
API_KEY = os.environ.get("PHALA_CLOUD_API_KEY") or decrypt_api_key()
59+
60+
# First CVM's info - UPDATE THIS with your appId from the first deployment
61+
EXISTING_APP_ID = "c96d55b03ede924c89154348be9dcffd52304af0"
62+
EXISTING_APP_AUTH_ADDRESS = "0x" + EXISTING_APP_ID # For on-chain KMS, appId IS the contract address
63+
64+
# Target node for replica
65+
TARGET_NODE_ID = 18 # prod9
66+
67+
# Replica name - change this for each replica
68+
REPLICA_NAME = "tee-oracle-option2-replica"
69+
70+
def get_headers():
71+
return {
72+
"X-API-Key": API_KEY,
73+
"Content-Type": "application/json"
74+
}
75+
76+
def read_compose_file():
77+
with open("docker-compose.yaml", "r") as f:
78+
return f.read()
79+
80+
def provision_cvm(name: str, compose_content: str, node_id: int, kms_id: str):
81+
"""Step 1: Provision CVM resources"""
82+
payload = {
83+
"name": name,
84+
"image": "dstack-0.5.4",
85+
"vcpu": 1,
86+
"memory": 2048,
87+
"disk_size": 20,
88+
"teepod_id": node_id,
89+
"kms_id": kms_id,
90+
"compose_file": {
91+
"docker_compose_file": compose_content,
92+
"allowed_envs": [],
93+
"features": ["kms"],
94+
"kms_enabled": True,
95+
"manifest_version": 2,
96+
"name": name,
97+
"public_logs": True,
98+
"public_sysinfo": True,
99+
"tproxy_enabled": False
100+
},
101+
"env_keys": [],
102+
"listed": True,
103+
"instance_type": "tdx.small"
104+
}
105+
106+
resp = requests.post(f"{CLOUD_API}/cvms/provision", headers=get_headers(), json=payload)
107+
resp.raise_for_status()
108+
return resp.json()
109+
110+
def create_cvm_with_existing_app(app_id: str, compose_hash: str, app_auth_address: str, deployer_address: str):
111+
"""Step 2: Create CVM using existing appId (skip contract deployment)"""
112+
payload = {
113+
"app_id": app_id,
114+
"compose_hash": compose_hash,
115+
"encrypted_env": "",
116+
"app_auth_contract_address": app_auth_address,
117+
"deployer_address": deployer_address
118+
}
119+
120+
resp = requests.post(f"{CLOUD_API}/cvms", headers=get_headers(), json=payload)
121+
resp.raise_for_status()
122+
return resp.json()
123+
124+
def main():
125+
if not API_KEY:
126+
print("Set PHALA_CLOUD_API_KEY environment variable")
127+
return
128+
129+
print("=" * 60)
130+
print("Deploying CVM Replica with Existing App ID")
131+
print("=" * 60)
132+
print(f"Existing App ID: {EXISTING_APP_ID}")
133+
print(f"Target Node: prod9 (id={TARGET_NODE_ID})")
134+
print()
135+
136+
# Step 1: Provision
137+
print("Step 1: Provisioning CVM resources...")
138+
compose_content = read_compose_file()
139+
provision_result = provision_cvm(
140+
name=REPLICA_NAME,
141+
compose_content=compose_content,
142+
node_id=TARGET_NODE_ID,
143+
kms_id="kms-base-prod9"
144+
)
145+
146+
print(f" Compose Hash: {provision_result.get('compose_hash', 'N/A')}")
147+
print(f" Device ID: {provision_result.get('device_id', 'N/A')}")
148+
149+
# Step 2: Create CVM with existing app_id
150+
print("\nStep 2: Creating CVM with existing App ID...")
151+
print(" (Skipping contract deployment - using existing AppAuth)")
152+
153+
# Get deployer address from first CVM or use a known one
154+
# For allowAnyDevice=true, the deployer doesn't matter for auth
155+
deployer = "0x0000000000000000000000000000000000000000" # placeholder
156+
157+
try:
158+
create_result = create_cvm_with_existing_app(
159+
app_id=EXISTING_APP_ID,
160+
compose_hash=provision_result["compose_hash"],
161+
app_auth_address=EXISTING_APP_AUTH_ADDRESS,
162+
deployer_address=deployer
163+
)
164+
165+
print("\nCVM Created!")
166+
print(json.dumps(create_result, indent=2))
167+
168+
except requests.exceptions.HTTPError as e:
169+
print(f"\nError: {e}")
170+
print(f"Response: {e.response.text}")
171+
print("\nThis might fail if:")
172+
print(" 1. The AppAuth contract wasn't deployed with allowAnyDevice=true")
173+
print(" 2. The compose_hash isn't registered in the contract")
174+
print(" 3. The device_id for prod9 isn't whitelisted")
175+
176+
if __name__ == "__main__":
177+
main()
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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

Comments
 (0)