TEEBridge enables CVMs running on different dstack KMS ecosystems to verify each other's attestations and share secrets. A smart contract on Base accepts DstackProofs from any whitelisted KMS root, creating a unified membership registry with ECIES-encrypted onboarding.
┌─────────────────┐ ┌──────────────────┐
│ CVM-A │ │ CVM-B │
│ Phala Cloud │ │ hosted.dstack │
│ Base KMS │ │ Sepolia KMS │
│ │ │ │
│ Flask :8080 │ │ Flask :8080 │
│ GET /proof │ │ GET /proof │
│ GET /info │ │ GET /onboarding │
└────────┬────────┘ └────────┬─────────┘
│ │
│ register.py │ register.py
│ (fetches /proof, │ (fetches /proof,
│ submits tx) │ submits tx)
│ │
└──────────┐ ┌────────────┘
▼ ▼
┌───────────────────┐
│ TEEBridge.sol │
│ Base Mainnet │
│ │
│ allowedKmsRoots │
│ allowedCode │
│ members │
│ onboarding msgs │
└───────────────────┘
▲
│
onboard.py
(ECIES encrypt,
submit tx)
The CVM never needs private keys or RPC access for registration. Instead:
- CVM runs an HTTP server exposing
/proof(DstackProof JSON derived from KMS) - External script (
register.py) fetches the proof and submits theregisterDstacktx - External script (
onboard.py) encrypts secrets to a member's pubkey and posts them on-chain - CVM polls the contract for onboarding messages and decrypts with its KMS-derived key
This eliminates the need for encrypted env delivery (which is broken on some dstack deployments).
| File | Purpose |
|---|---|
bridge_agent.py |
CVM agent: Flask server with /proof, /info, /onboarding endpoints + background onboarding poller |
register.py |
External: fetch proof from CVM (or paste from serial logs), register on TEEBridge. Auto-adds KMS root and code to allowlist. |
onboard.py |
External: ECIES-encrypt a secret to a member's pubkey, post onboard tx |
contracts/TEEBridge.sol |
Solidity contract: multi-KMS membership registry with onboarding |
docker-compose-bridge.yaml |
Compose for Phala Cloud deployment |
docker-compose-bridge-hosted.yaml |
Compose for hosted.dstack.info (serial logging, onboarding polling) |
hosted.sh |
Wrapper for vmm-cli.py with hosted.dstack.info credentials |
Phala Cloud:
phala deploy --name my-bridge \
--compose docker-compose-bridge.yaml \
--image dstack-0.5.4 --node-id 26 \
--kms base --private-key $KEY --rpc-url https://mainnet.base.orghosted.dstack.info:
# Generate compose manifest
./hosted.sh compose --name my-bridge \
--docker-compose docker-compose-bridge-hosted.yaml \
--kms --public-logs --output /tmp/compose.json
# Whitelist compose hash on Sepolia DstackApp
cast send $DSTACK_APP "addComposeHash(bytes32)" $HASH \
--rpc-url https://sepolia.drpc.org --private-key $KEY
# Deploy
./hosted.sh deploy --name my-bridge --image dstack-0.5.7 \
--compose /tmp/compose.json \
--kms-url https://kms.hosted.dstack.info:9100 \
--app-id $DSTACK_APP \
--port tcp:0.0.0.0:18080:8080# From HTTP endpoint (if gateway works):
python3 register.py --cvm-url https://APP_ID-8080.gateway.domain \
--bridge 0x254057d9d92FC7F75E3D49F0c6B0be9eE2A334D5 \
--private-key $KEY
# From serial logs (if gateway is broken):
# 1. Find PROOF_JSON=... line in serial/container logs
# 2. Pass it directly:
python3 register.py --proof-json '{"code_id":"0x...","dstack_proof":{...}}' \
--bridge 0x254057d9d92FC7F75E3D49F0c6B0be9eE2A334D5 \
--private-key $KEYpython3 onboard.py \
--from-member 0x5682abc2... \
--to-member 0xc8af48f2... \
--secret "my secret data" \
--bridge 0x254057d9d92FC7F75E3D49F0c6B0be9eE2A334D5 \
--private-key $KEYIf BRIDGE_CONTRACT env is set, CVM-B polls every 60s and prints decrypted messages:
ONBOARDING from=5682abc2... payload=my secret data
Or hit the HTTP endpoint:
GET /onboarding?bridge=0x254057d9d92FC7F75E3D49F0c6B0be9eE2A334D5
| CVM-A | CVM-B | |
|---|---|---|
| Platform | Phala Cloud | hosted.dstack.info |
| KMS | Base (0x52d3CF51...) |
Sepolia (0x9d456Bb7...) |
| Image | dstack-0.5.4 | dstack-0.5.7 |
| TEEBridge | 0x254057d9d92FC7F75E3D49F0c6B0be9eE2A334D5 (Base) |
same |
Problem: vmm-cli.py deploy --env-file requires the CLI to fetch an encrypt pubkey from KMS. On hosted.dstack.info, KMS port 9100 is firewalled from the internet, and the VMM's own kms_url points to localhost (wrong — KMS is in a separate CVM). Both paths to get the pubkey fail.
Workaround: The HTTP Proof Pattern eliminates the need for env vars entirely. The CVM derives all keys from the KMS socket at runtime. Registration and onboarding happen externally.
Problem: VMM config has gateway_urls = ["http://127.0.0.1:8082"], but the gateway runs in a separate CVM. CVMs use QEMU user-mode networking where 127.0.0.1 is the CVM's own localhost. Gateway registration fails silently.
Workaround: Deploy without --gateway. Use --port tcp:0.0.0.0:HOST_PORT:CVM_PORT for direct port mapping (only reachable from the host, firewalled from internet). Print critical data to serial logs via tee /dev/ttyS0.
Problem: vmm-cli.py logs only shows the serial console. Docker container stdout is invisible. The guest agent's log endpoint requires gateway access.
Workaround: Use CMD ["sh", "-c", "python -u agent.py 2>&1 | tee /dev/ttyS0"] with privileged: true to pipe container output to the serial console. /dev/ttyS0 is the serial port — not /dev/console, not /dev/kmsg.
Problem: hosted.dstack.info uses Sepolia DstackApp contracts for KMS auth. Every compose change produces a new hash that must be whitelisted via addComposeHash(bytes32) — a Sepolia tx per iteration.
Impact: 10+ Sepolia txs during a debugging sprint, each taking 15-30s.
Mitigation: Get the compose right before deploying. The HTTP Proof Pattern reduces iteration cycles since the CVM code is stable and external scripts handle the changing logic.
Problem: Neither Phala CLI nor vmm-cli upload Docker build context. COPY . . fails.
Workaround: Inline all code via RUN cat > file.py <<'PYEOF' ... PYEOF in dockerfile_inline.
Problem: Using the wrong image version for a node causes silent failures. dstack-0.5.4 nodes need --image dstack-0.5.4 --node-id 26.
The PyPI package is eciespy (not ecies), but the Python import is from ecies import ....
Problem: Gateway access (the APP_ID-PORT.gateway.domain URL pattern) sometimes returns connection errors (TLS EOF, exit code 56) even when the container is running and healthy.
Workaround: Use phala ssh (requires --dev-os) or fall back to serial log output with PROOF_JSON= pattern. The --proof-json flag on register.py accepts pasted proof from logs.