Skip to content

Commit a83dfe8

Browse files
committed
worked demo with multiple phala machines
1 parent 133ac59 commit a83dfe8

11 files changed

Lines changed: 1474 additions & 0 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Implementation Notes: Self-Join Oracle
2+
3+
Notes from implementing multi-node oracle deployment with shared signing keys.
4+
5+
## Challenges Encountered
6+
7+
### 1. CLI `--custom-app-id` Flag is Disabled
8+
9+
The `--custom-app-id` flag exists in the CLI's help text but **the implementation is commented out** in the phala-cloud-cli source (`src/commands/deploy/index.ts` lines 126-162):
10+
11+
```typescript
12+
// TODO: remove customAppId for now
13+
// if (customAppId) { ... } // ALL COMMENTED OUT
14+
```
15+
16+
Every CLI deployment creates a new AppAuth contract, making self-join impossible via CLI alone. This is why we use direct API calls in `deploy_with_contract.py` and `deploy_replica.py`.
17+
18+
### 2. API Key Decryption
19+
20+
The CLI encrypts stored API keys (`~/.phala-cloud/api-key`) with AES-256-CBC using a machine-specific key:
21+
22+
```python
23+
parts = f"{hostname}|{platform}|{arch}|{cpu_model}|{username}"
24+
key = hashlib.sha256(parts.encode()).digest()
25+
```
26+
27+
**Gotcha**: Python `platform.machine()` returns `x86_64`, but Node.js `os.arch()` returns `x64`. The scripts include this conversion.
28+
29+
### 3. allowAnyDevice Defaults to False
30+
31+
First replica failed at "requesting app keys" because the AppAuth contract only allowed the original device (prod5). Required redeploying with `allowAnyDevice=true` via direct contract call.
32+
33+
### 4. compose_hash Includes CVM Name
34+
35+
Each CVM name produces a different compose_hash. Replicas fail with "Compose hash not allowed" until you call `addComposeHash()` on the AppAuth contract for each replica's hash.
36+
37+
**Workaround**: After deploying a replica, get its compose_hash and register it:
38+
39+
```python
40+
from web3 import Web3
41+
w3 = Web3(Web3.HTTPProvider("https://mainnet.base.org"))
42+
app_auth = w3.eth.contract(address=APP_AUTH_ADDRESS, abi=APP_AUTH_ABI)
43+
tx = app_auth.functions.addComposeHash(compose_hash_bytes).build_transaction(...)
44+
```
45+
46+
### 5. Image Version Matters
47+
48+
Base KMS clusters showed "No available resources" with `v0.5.4-dev`. Using `dstack-0.5.4` (non-dev image) worked.
49+
50+
## Future CLI Improvements Needed
51+
52+
1. Re-enable `--custom-app-id` flag
53+
2. Add `--allow-any-device` flag for AppAuth deployment
54+
3. Add command to register additional compose hashes (`phala app add-compose-hash`)
55+
56+
## Successful Deployment
57+
58+
After resolving these issues, both oracles (prod5 and prod9) share identical:
59+
- appId: `5a367973f645a11328d5b80fc226e3cb7436f78e`
60+
- signerAddress: `0x7B83657880051cD6782E1D7fFFf3e6bd54f06853`
61+
- derivedPubkey: `0x0323c9da9e831c9a677a92597a95c40bd7e7fe723d215763ef70874ae5dd660404`
62+
63+
Both produce identical signatures for the same input, enabling oracle redundancy.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Tutorial 05: On-Chain Verifiable Oracle
2+
3+
Build an oracle that signs price data with TEE-derived keys, verifiable on-chain. This tutorial demonstrates the **AppAuth contract** architecture that controls which TEEs can get keys from KMS.
4+
5+
## Understanding AppAuth
6+
7+
Every dstack app has an **AppAuth contract** on Base. When a TEE requests keys, KMS calls your AppAuth's `isAppAllowed()` to decide.
8+
9+
```
10+
┌─────────────────────────────────────────────────────────────┐
11+
│ DstackKms Contract (Base) │
12+
│ │
13+
│ registerApp(address) ← registers your AppAuth │
14+
│ isAppAllowed(bootInfo) → delegates to your contract │
15+
└─────────────────────────────────────────────────────────────┘
16+
17+
│ calls IAppAuth(appId).isAppAllowed(bootInfo)
18+
19+
┌─────────────────────────────────────────────────────────────┐
20+
│ Your AppAuth Contract (DstackApp) │
21+
│ │
22+
│ owner: 0x... ← can add devices/hashes │
23+
│ allowedDeviceIds[...] │
24+
│ allowedComposeHashes[...] │
25+
│ allowAnyDevice: true/false │
26+
│ │
27+
│ isAppAllowed(bootInfo) → checks whitelist │
28+
└─────────────────────────────────────────────────────────────┘
29+
```
30+
31+
**Key insight:** The private key you deploy with becomes the **owner** of the AppAuth contract. The owner can add devices and compose hashes to the whitelist.
32+
33+
## Four Deployment Options
34+
35+
### Option 1: Single Device (CLI Default)
36+
37+
```bash
38+
phala deploy -n my-oracle -c docker-compose.yaml \
39+
--kms-id kms-base-prod5 \
40+
--private-key "$PRIVATE_KEY"
41+
```
42+
43+
Creates AppAuth with:
44+
- `owner` = address derived from PRIVATE_KEY
45+
- `allowAnyDevice = false`
46+
- `allowedDeviceIds[thisDevice] = true`
47+
- `allowedComposeHashes[thisHash] = true`
48+
49+
**Result:** Only this device can get keys. Simple single-node deployment.
50+
51+
### Option 2: Owner Adds Devices (Recommended for Multi-Node)
52+
53+
Deploy first node, then the **owner** explicitly whitelists additional devices:
54+
55+
```bash
56+
# Step 1: Deploy first node (owner = your address)
57+
phala deploy -n my-oracle -c docker-compose.yaml \
58+
--kms-id kms-base-prod5 \
59+
--private-key "$PRIVATE_KEY"
60+
61+
# Step 2: Get device ID of second node (from Phala Cloud API)
62+
# Step 3: Owner calls addDevice() on the AppAuth contract
63+
cast send $APP_AUTH_ADDRESS "addDevice(bytes32)" $DEVICE_ID_2 \
64+
--private-key "$PRIVATE_KEY" --rpc-url https://mainnet.base.org
65+
66+
# Step 4: Deploy second node with same appId
67+
python3 deploy_replica.py
68+
```
69+
70+
**Result:** Controlled multi-node. Owner explicitly approves each device.
71+
72+
### Option 3: allowAnyDevice (Permissive)
73+
74+
Deploy with `allowAnyDevice=true` to let any TEE device get keys:
75+
76+
```bash
77+
python3 deploy_with_contract.py # Sets allowAnyDevice=true
78+
```
79+
80+
The contract checks:
81+
```solidity
82+
function isAppAllowed(AppBootInfo calldata bootInfo) {
83+
if (!allowedComposeHashes[bootInfo.composeHash])
84+
return (false, "Compose hash not allowed");
85+
if (!allowAnyDevice && !allowedDeviceIds[bootInfo.deviceId])
86+
return (false, "Device not allowed");
87+
return (true, "");
88+
}
89+
```
90+
91+
**Result:** Any device with a whitelisted composeHash can join. Less secure but simpler.
92+
93+
### Option 4: Custom AppAuth Contract
94+
95+
Deploy your own contract implementing `IAppAuth`:
96+
97+
```solidity
98+
interface IAppAuth {
99+
function isAppAllowed(AppBootInfo calldata bootInfo)
100+
external view returns (bool isAllowed, string memory reason);
101+
}
102+
```
103+
104+
Then register it:
105+
```solidity
106+
DstackKms(KMS_ADDRESS).registerApp(yourContract); // Public function!
107+
```
108+
109+
**Examples:**
110+
- NFT-gated: Only NFT holders can run nodes
111+
- DAO-controlled: Compose hashes approved by vote
112+
- Time-locked: Deployments only during certain periods
113+
- Multi-sig: Multiple approvals required
114+
115+
## DstackApp Owner Functions
116+
117+
The standard AppAuth (`DstackApp`) gives the owner these functions:
118+
119+
```solidity
120+
// Whitelist management
121+
function addDevice(bytes32 deviceId) external onlyOwner;
122+
function removeDevice(bytes32 deviceId) external onlyOwner;
123+
function addComposeHash(bytes32 composeHash) external onlyOwner;
124+
function removeComposeHash(bytes32 composeHash) external onlyOwner;
125+
126+
// Mode toggle
127+
function setAllowAnyDevice(bool allow) external onlyOwner;
128+
129+
// Upgrades
130+
function disableUpgrades() external onlyOwner; // Permanent!
131+
```
132+
133+
## Signature Chain
134+
135+
Regardless of which option you use, the oracle proves its keys came from KMS:
136+
137+
```
138+
KMS Root (known on-chain)
139+
140+
│ signs: "dstack-kms-issued:" + appId + appPubkey
141+
142+
App Key (recovered from kmsSignature)
143+
144+
│ signs: "ethereum:" + derivedPubkeyHex
145+
146+
Derived Key → signs oracle messages
147+
```
148+
149+
## Quick Start (Local Simulator)
150+
151+
```bash
152+
pip install -r requirements.txt
153+
phala simulator start
154+
155+
docker compose build
156+
docker compose run --rm -p 8080:8080 \
157+
-v ~/.phala-cloud/simulator/0.5.3/dstack.sock:/var/run/dstack.sock \
158+
app
159+
160+
# In another terminal
161+
python3 test_local.py
162+
```
163+
164+
## Files
165+
166+
```
167+
05-onchain-oracle/
168+
├── docker-compose.yaml # Oracle app
169+
├── TeeOracle.sol # On-chain signature verification
170+
├── test_local.py # Test with simulator
171+
├── test_phalacloud.py # Test on Phala Cloud
172+
├── deploy_with_contract.py # Deploy with allowAnyDevice=true (Option 3)
173+
├── deploy_replica.py # Deploy replica using existing appId
174+
├── add_device.py # Add device to whitelist (Option 2)
175+
├── add_compose_hash.py # Add compose hash to whitelist
176+
├── requirements.txt
177+
├── NOTES.md # Troubleshooting & CLI limitations
178+
└── README.md
179+
```
180+
181+
## Contract Addresses
182+
183+
| Contract | Address |
184+
|----------|---------|
185+
| DstackKms (Base) | `0x2f83172A49584C017F2B256F0FB2Dca14126Ba9C` |
186+
| KMS Root (Simulator) | `0x8f2cF602C9695b23130367ed78d8F557554de7C5` |
187+
188+
## IAppAuth Interface
189+
190+
From [dstack/kms/auth-eth/contracts/IAppAuth.sol](https://github.com/dstack-tee/dstack/blob/main/kms/auth-eth/contracts/IAppAuth.sol):
191+
192+
```solidity
193+
struct AppBootInfo {
194+
address appId;
195+
bytes32 composeHash;
196+
address instanceId;
197+
bytes32 deviceId;
198+
bytes32 mrAggregated;
199+
bytes32 mrSystem;
200+
bytes32 osImageHash;
201+
string tcbStatus;
202+
string[] advisoryIds;
203+
}
204+
205+
function isAppAllowed(AppBootInfo calldata bootInfo)
206+
external view returns (bool isAllowed, string memory reason);
207+
```
208+
209+
## References
210+
211+
- [DstackKms.sol](https://github.com/dstack-tee/dstack/blob/main/kms/auth-eth/contracts/DstackKms.sol)
212+
- [DstackApp.sol](https://github.com/dstack-tee/dstack/blob/main/kms/auth-eth/contracts/DstackApp.sol)
213+
- [NOTES.md](NOTES.md)

0 commit comments

Comments
 (0)