sealed-precision-oracle 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/bin/cli.js +17 -0
- package/contracts/IERC7857.sol +86 -0
- package/contracts/OracleDataVerifier.sol +72 -0
- package/contracts/PrecisionOracleID.sol +328 -0
- package/dist/hardhat.config.d.ts +6 -0
- package/dist/hardhat.config.js +29 -0
- package/dist/hardhat.config.js.map +1 -0
- package/dist/scripts/deploy.d.ts +1 -0
- package/dist/scripts/deploy.js +47 -0
- package/dist/scripts/deploy.js.map +1 -0
- package/dist/scripts/registerTee.d.ts +1 -0
- package/dist/scripts/registerTee.js +16 -0
- package/dist/scripts/registerTee.js.map +1 -0
- package/dist/src/fetchMarketData.d.ts +15 -0
- package/dist/src/fetchMarketData.js +160 -0
- package/dist/src/fetchMarketData.js.map +1 -0
- package/dist/src/oracle.d.ts +1 -0
- package/dist/src/oracle.js +263 -0
- package/dist/src/oracle.js.map +1 -0
- package/dist/src/teeInference.d.ts +43 -0
- package/dist/src/teeInference.js +200 -0
- package/dist/src/teeInference.js.map +1 -0
- package/dist/src/uploadReceipt.d.ts +12 -0
- package/dist/src/uploadReceipt.js +111 -0
- package/dist/src/uploadReceipt.js.map +1 -0
- package/package.json +44 -0
- package/skills/sealed-precision-oracle/SKILL.md +71 -0
- package/src/fetchMarketData.ts +187 -0
- package/src/oracle.ts +308 -0
- package/src/teeInference.ts +272 -0
- package/src/uploadReceipt.ts +92 -0
package/src/oracle.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import { uploadReceipt } from "./uploadReceipt";
|
|
3
|
+
import { fetchMarketData, formatMarketDataForPrompt, type MarketData } from "./fetchMarketData";
|
|
4
|
+
import { teeInference, discoverProviders, type TeeInferenceResult } from "./teeInference";
|
|
5
|
+
import "dotenv/config";
|
|
6
|
+
|
|
7
|
+
// ─── Configuration ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const OG_RPC_URL = process.env.OG_RPC_URL || "https://evmrpc.0g.ai";
|
|
10
|
+
|
|
11
|
+
// Contract ABI (only the functions we need)
|
|
12
|
+
const ORACLE_ABI = [
|
|
13
|
+
"function recordResolution(uint256 tokenId, bytes32 storageRoot, string calldata signedText, bytes calldata teeSignature, address teeSigner) external",
|
|
14
|
+
"function resolutionCount(uint256 tokenId) external view returns (uint256)",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// ─── Oracle Prompt ──────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function buildSystemPrompt(marketData: MarketData, dataHash: string): string {
|
|
20
|
+
const dataBlock = formatMarketDataForPrompt(marketData);
|
|
21
|
+
return `You are the Sealed Precision Oracle, a tamper-proof AI judge for precision prediction markets.
|
|
22
|
+
|
|
23
|
+
REAL-TIME DATA PROVIDED (verified from live sources):
|
|
24
|
+
${dataBlock}
|
|
25
|
+
Data integrity hash: ${dataHash}
|
|
26
|
+
|
|
27
|
+
Your role:
|
|
28
|
+
1. Use the REAL DATA above (do NOT hallucinate values).
|
|
29
|
+
2. Evaluate the market question against this data.
|
|
30
|
+
3. Provide a clear YES or NO resolution.
|
|
31
|
+
4. State your confidence level (0-100%).
|
|
32
|
+
5. Explain your reasoning step by step, referencing the actual data.
|
|
33
|
+
|
|
34
|
+
Respond in this exact JSON format:
|
|
35
|
+
{
|
|
36
|
+
"market_question": "<the question>",
|
|
37
|
+
"resolution": "YES" or "NO",
|
|
38
|
+
"confidence": <0-100>,
|
|
39
|
+
"reasoning": "<step-by-step reasoning citing the real data>",
|
|
40
|
+
"data_integrity_hash": "${dataHash}",
|
|
41
|
+
"data_used": ${JSON.stringify(marketData.dataPoints)},
|
|
42
|
+
"timestamp": "<current UTC timestamp>"
|
|
43
|
+
}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const marketQuestion =
|
|
50
|
+
process.argv[2] ||
|
|
51
|
+
"Will the average Ethereum mainnet gas price exceed 50 gwei right now?";
|
|
52
|
+
const contractAddress = process.env.ORACLE_CONTRACT_ADDRESS;
|
|
53
|
+
const tokenId = parseInt(process.env.ORACLE_TOKEN_ID || "1", 10);
|
|
54
|
+
|
|
55
|
+
console.log("=== Sealed Precision Oracle v3.0 ===\n");
|
|
56
|
+
|
|
57
|
+
const PRIVATE_KEY = process.env.PRIVATE_KEY;
|
|
58
|
+
const REMOTE_API_URL = process.env.REMOTE_API_URL || "http://localhost:3001";
|
|
59
|
+
|
|
60
|
+
if (!PRIVATE_KEY) {
|
|
61
|
+
console.log("No PRIVATE_KEY found in .env. Attempting to use Managed API Bridge...");
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(`${REMOTE_API_URL}/api/resolve`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ marketQuestion }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const result: any = await response.json();
|
|
70
|
+
if (!result.success) throw new Error(result.error);
|
|
71
|
+
|
|
72
|
+
console.log(`AI Resolution:\n \`\`\`json\n${JSON.stringify({
|
|
73
|
+
market_question: marketQuestion,
|
|
74
|
+
resolution: result.resolution,
|
|
75
|
+
confidence: result.confidence,
|
|
76
|
+
reasoning: result.reasoning,
|
|
77
|
+
data_used: result.data_used.dataPoints,
|
|
78
|
+
timestamp: new Date().toISOString()
|
|
79
|
+
}, null, 2)}\n\`\`\``);
|
|
80
|
+
|
|
81
|
+
console.log("\n" + "=".repeat(56));
|
|
82
|
+
console.log(" SEALED PRECISION ORACLE — RESOLUTION COMPLETE (via API)");
|
|
83
|
+
console.log("=".repeat(56) + "\n");
|
|
84
|
+
console.log("Market Question:", marketQuestion);
|
|
85
|
+
console.log("TEE Attestation: VERIFIED (via Remote TEE Signer)");
|
|
86
|
+
console.log(" Signer: ", result.proofs.teeSigner);
|
|
87
|
+
console.log("\n0G Storage Root:", result.proofs.storageRoot);
|
|
88
|
+
if (result.proofs.onChainTx !== "0x") {
|
|
89
|
+
console.log("0G Chain TX: ", result.proofs.onChainTx);
|
|
90
|
+
console.log("Explorer: https://chainscan.0g.ai/tx/" + result.proofs.onChainTx);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
} catch (err: any) {
|
|
94
|
+
console.error("Managed API fallback failed:", err.message);
|
|
95
|
+
console.log("Please set PRIVATE_KEY in .env to run locally.");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log("Contract Address:", contractAddress);
|
|
101
|
+
console.log("Market Question:", marketQuestion);
|
|
102
|
+
|
|
103
|
+
// ── Step 1: Fetch real market data ───────────────────────────────────
|
|
104
|
+
console.log("\n[1/5] Fetching real-time market data...\n");
|
|
105
|
+
|
|
106
|
+
const marketData = await fetchMarketData(marketQuestion);
|
|
107
|
+
console.log(" Type:", marketData.marketType);
|
|
108
|
+
for (const [k, v] of Object.entries(marketData.dataPoints)) {
|
|
109
|
+
if (v !== null) console.log(` ${k}: ${v}`);
|
|
110
|
+
}
|
|
111
|
+
console.log(" Source:", marketData.source);
|
|
112
|
+
|
|
113
|
+
// Compute data integrity hash from data points only (excludes timestamp for stability)
|
|
114
|
+
const stableData = JSON.stringify(marketData.dataPoints);
|
|
115
|
+
const dataHash = ethers.keccak256(ethers.toUtf8Bytes(stableData));
|
|
116
|
+
console.log(" Data hash:", dataHash);
|
|
117
|
+
|
|
118
|
+
// ── Step 2: Run TEE-attested inference ──────────────────────────────
|
|
119
|
+
console.log("\n[2/5] Running TEE-attested inference (direct provider)...\n");
|
|
120
|
+
|
|
121
|
+
const systemPrompt = buildSystemPrompt(marketData, dataHash);
|
|
122
|
+
const userPrompt = `Resolve this market: ${marketQuestion}`;
|
|
123
|
+
|
|
124
|
+
let inferenceResult: TeeInferenceResult;
|
|
125
|
+
try {
|
|
126
|
+
inferenceResult = await teeInference(systemPrompt, userPrompt);
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
console.warn("\n ╔══════════════════════════════════════════════════════════╗");
|
|
129
|
+
console.warn(" ║ WARNING: FALLING BACK TO ROUTER API ║");
|
|
130
|
+
console.warn(" ║ No per-inference TEE attestation will be available. ║");
|
|
131
|
+
console.warn(" ║ The AI response will NOT be cryptographically signed. ║");
|
|
132
|
+
console.warn(" ╚══════════════════════════════════════════════════════════╝\n");
|
|
133
|
+
console.warn("[TEE] Reason:", err.message);
|
|
134
|
+
|
|
135
|
+
// Fallback to Router if no direct providers available
|
|
136
|
+
const { OpenAI } = await import("openai");
|
|
137
|
+
const client = new OpenAI({
|
|
138
|
+
baseURL: "https://router-api.0g.ai/v1",
|
|
139
|
+
apiKey: process.env.ZG_API_KEY || "not-needed-for-testnet",
|
|
140
|
+
});
|
|
141
|
+
const completion = await client.chat.completions.create({
|
|
142
|
+
model: "Qwen/Qwen2.5-7B-Instruct",
|
|
143
|
+
messages: [
|
|
144
|
+
{ role: "system", content: systemPrompt },
|
|
145
|
+
{ role: "user", content: userPrompt },
|
|
146
|
+
],
|
|
147
|
+
temperature: 0.1,
|
|
148
|
+
max_tokens: 1024,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
inferenceResult = {
|
|
152
|
+
content: completion.choices[0]?.message?.content || "",
|
|
153
|
+
chatId: completion.id || "unknown",
|
|
154
|
+
model: "Qwen/Qwen2.5-7B-Instruct",
|
|
155
|
+
providerAddress: "router-api (no direct provider)",
|
|
156
|
+
providerUrl: "https://router-api.0g.ai/v1",
|
|
157
|
+
attestation: null,
|
|
158
|
+
attestationNote:
|
|
159
|
+
"FALLBACK: Used Router API. No per-inference TEE attestation available. " +
|
|
160
|
+
"Direct provider access failed: " + err.message,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!inferenceResult.content) throw new Error("No response from inference");
|
|
165
|
+
console.log("\nAI Resolution:\n", inferenceResult.content);
|
|
166
|
+
|
|
167
|
+
// ── Step 3: Build receipt with cryptographic binding ────────────────
|
|
168
|
+
console.log("\n[3/5] Building tamper-evident receipt...\n");
|
|
169
|
+
|
|
170
|
+
// Hash the AI response for integrity binding
|
|
171
|
+
const responseHash = ethers.keccak256(
|
|
172
|
+
ethers.toUtf8Bytes(inferenceResult.content)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const receipt = {
|
|
176
|
+
oracle: "Sealed Precision Oracle v3.0",
|
|
177
|
+
model: inferenceResult.model,
|
|
178
|
+
provider: {
|
|
179
|
+
address: inferenceResult.providerAddress,
|
|
180
|
+
url: inferenceResult.providerUrl,
|
|
181
|
+
},
|
|
182
|
+
tee_attestation: inferenceResult.attestation
|
|
183
|
+
? {
|
|
184
|
+
verified: inferenceResult.attestation.verified,
|
|
185
|
+
signed_text: inferenceResult.attestation.signedText,
|
|
186
|
+
signature: inferenceResult.attestation.signature,
|
|
187
|
+
tee_signer: inferenceResult.attestation.teeSigner,
|
|
188
|
+
recovered_address: inferenceResult.attestation.recoveredAddress,
|
|
189
|
+
chat_id: inferenceResult.chatId,
|
|
190
|
+
note: inferenceResult.attestationNote,
|
|
191
|
+
}
|
|
192
|
+
: {
|
|
193
|
+
verified: false,
|
|
194
|
+
note: inferenceResult.attestationNote,
|
|
195
|
+
},
|
|
196
|
+
integrity: {
|
|
197
|
+
data_hash: dataHash,
|
|
198
|
+
response_hash: responseHash,
|
|
199
|
+
note:
|
|
200
|
+
"data_hash = keccak256(market_data_json). response_hash = keccak256(ai_response). " +
|
|
201
|
+
"data_hash is embedded in the AI prompt so the TEE-signed response implicitly commits to the data.",
|
|
202
|
+
},
|
|
203
|
+
market_data: {
|
|
204
|
+
type: marketData.marketType,
|
|
205
|
+
data_points: marketData.dataPoints,
|
|
206
|
+
source: marketData.source,
|
|
207
|
+
fetched_at: marketData.fetchedAt,
|
|
208
|
+
},
|
|
209
|
+
market_question: marketQuestion,
|
|
210
|
+
ai_response: inferenceResult.content,
|
|
211
|
+
created_at: new Date().toISOString(),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// ── Step 4: Upload to 0G Storage ───────────────────────────────────
|
|
215
|
+
console.log("[4/5] Uploading receipt to 0G Storage...\n");
|
|
216
|
+
|
|
217
|
+
const receiptJSON = JSON.stringify(receipt, null, 2);
|
|
218
|
+
const { rootHash, txHash } = await uploadReceipt(receiptJSON);
|
|
219
|
+
|
|
220
|
+
// ── Step 5: Record on-chain with TEE signature ─────────────────────
|
|
221
|
+
let chainTxHash: string | null = null;
|
|
222
|
+
|
|
223
|
+
if (contractAddress) {
|
|
224
|
+
console.log("\n[5/5] Recording resolution on-chain...\n");
|
|
225
|
+
|
|
226
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
227
|
+
if (!privateKey) throw new Error("PRIVATE_KEY required for on-chain recording");
|
|
228
|
+
|
|
229
|
+
const provider = new ethers.JsonRpcProvider(OG_RPC_URL);
|
|
230
|
+
const signer = new ethers.Wallet(privateKey, provider);
|
|
231
|
+
const contract = new ethers.Contract(contractAddress, ORACLE_ABI, signer);
|
|
232
|
+
|
|
233
|
+
// rootHash is 0x-prefixed hex — convert to bytes32
|
|
234
|
+
const rootHashBytes32 = ethers.zeroPadValue(rootHash, 32);
|
|
235
|
+
|
|
236
|
+
// Pass TEE signature and signer for on-chain verification
|
|
237
|
+
const signedText = inferenceResult.attestation?.signedText || "";
|
|
238
|
+
const teeSignature = inferenceResult.attestation?.signature
|
|
239
|
+
? inferenceResult.attestation.signature
|
|
240
|
+
: "0x";
|
|
241
|
+
const teeSigner = inferenceResult.attestation?.teeSigner
|
|
242
|
+
? inferenceResult.attestation.teeSigner
|
|
243
|
+
: ethers.ZeroAddress;
|
|
244
|
+
|
|
245
|
+
const tx = await contract.recordResolution(
|
|
246
|
+
tokenId,
|
|
247
|
+
rootHashBytes32,
|
|
248
|
+
signedText,
|
|
249
|
+
teeSignature,
|
|
250
|
+
teeSigner
|
|
251
|
+
);
|
|
252
|
+
const receiptTx = await tx.wait();
|
|
253
|
+
chainTxHash = receiptTx?.hash || tx.hash;
|
|
254
|
+
|
|
255
|
+
const count = await contract.resolutionCount(tokenId);
|
|
256
|
+
const chainId = (await provider.getNetwork()).chainId;
|
|
257
|
+
const explorerBase = chainId === 16661n
|
|
258
|
+
? "https://chainscan.0g.ai/tx/"
|
|
259
|
+
: "https://chainscan-galileo.0g.ai/tx/";
|
|
260
|
+
|
|
261
|
+
console.log(" On-chain TX:", chainTxHash);
|
|
262
|
+
console.log(" Total resolutions for token", tokenId + ":", count.toString());
|
|
263
|
+
} else {
|
|
264
|
+
console.log("\n[5/5] Skipped on-chain recording (ORACLE_CONTRACT_ADDRESS not set)");
|
|
265
|
+
console.log(" Set ORACLE_CONTRACT_ADDRESS in .env after deploying the contract.");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Final Summary ────────────────────────────────────────────────────
|
|
269
|
+
console.log("\n" + "=".repeat(56));
|
|
270
|
+
console.log(" SEALED PRECISION ORACLE — RESOLUTION COMPLETE");
|
|
271
|
+
console.log("=".repeat(56) + "\n");
|
|
272
|
+
console.log("Market Question:", marketQuestion);
|
|
273
|
+
const dataPreview = Object.entries(marketData.dataPoints)
|
|
274
|
+
.filter(([_, v]) => v !== null)
|
|
275
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
276
|
+
.join(", ");
|
|
277
|
+
console.log("Real Data: ", dataPreview);
|
|
278
|
+
console.log("Data Hash: ", dataHash.slice(0, 18) + "...");
|
|
279
|
+
console.log("Response Hash: ", responseHash.slice(0, 18) + "...");
|
|
280
|
+
|
|
281
|
+
if (inferenceResult.attestation?.verified) {
|
|
282
|
+
console.log("TEE Attestation: VERIFIED (per-inference signature)");
|
|
283
|
+
console.log(" Signer: ", inferenceResult.attestation.teeSigner);
|
|
284
|
+
} else {
|
|
285
|
+
console.log("TEE Attestation:", inferenceResult.attestationNote.slice(0, 80));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log("\n0G Storage Root:", rootHash);
|
|
289
|
+
console.log("0G Storage TX: ", txHash);
|
|
290
|
+
if (chainTxHash) {
|
|
291
|
+
const chainId = (await new ethers.JsonRpcProvider(OG_RPC_URL).getNetwork()).chainId;
|
|
292
|
+
const explorerBase = chainId === 16661n ? "https://chainscan.0g.ai/tx/" : "https://chainscan-galileo.0g.ai/tx/";
|
|
293
|
+
console.log("0G Chain TX: ", chainTxHash);
|
|
294
|
+
console.log("Explorer: " + explorerBase + chainTxHash);
|
|
295
|
+
}
|
|
296
|
+
console.log(
|
|
297
|
+
"\nVerification: Download the receipt from 0G Storage using the root hash."
|
|
298
|
+
);
|
|
299
|
+
console.log(
|
|
300
|
+
"The receipt contains the TEE signature — verify it against the on-chain TEE signer."
|
|
301
|
+
);
|
|
302
|
+
console.log();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
main().catch((err) => {
|
|
306
|
+
console.error("Oracle execution failed:", err);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct TEE-attested inference via the 0G Compute SDK.
|
|
3
|
+
*
|
|
4
|
+
* Bypasses the Router API entirely. Instead:
|
|
5
|
+
* 1. Discovers providers on-chain from the InferenceServing contract
|
|
6
|
+
* 2. Calls inference directly to the provider's broker URL
|
|
7
|
+
* 3. Fetches the per-inference TEE signature from the provider
|
|
8
|
+
* 4. Verifies the signature against the on-chain TEE signer address
|
|
9
|
+
*
|
|
10
|
+
* This provides REAL per-inference attestation, not just metadata.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ethers } from "ethers";
|
|
14
|
+
import {
|
|
15
|
+
createZGComputeNetworkBroker,
|
|
16
|
+
createZGComputeNetworkReadOnlyBroker,
|
|
17
|
+
} from "@0gfoundation/0g-compute-ts-sdk";
|
|
18
|
+
import { OpenAI } from "openai";
|
|
19
|
+
import "dotenv/config";
|
|
20
|
+
|
|
21
|
+
const OG_RPC_URL = process.env.OG_RPC_URL || "https://evmrpc.0g.ai";
|
|
22
|
+
|
|
23
|
+
export interface TeeAttestation {
|
|
24
|
+
signedText: string;
|
|
25
|
+
signature: string;
|
|
26
|
+
teeSigner: string;
|
|
27
|
+
recoveredAddress: string;
|
|
28
|
+
verified: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TeeInferenceResult {
|
|
32
|
+
content: string;
|
|
33
|
+
chatId: string;
|
|
34
|
+
model: string;
|
|
35
|
+
providerAddress: string;
|
|
36
|
+
providerUrl: string;
|
|
37
|
+
attestation: TeeAttestation | null;
|
|
38
|
+
attestationNote: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProviderInfo {
|
|
42
|
+
provider: string;
|
|
43
|
+
url: string;
|
|
44
|
+
model: string;
|
|
45
|
+
verifiability: string;
|
|
46
|
+
teeSignerAddress: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Discover TEE-attested providers for a given model from the on-chain registry.
|
|
51
|
+
*/
|
|
52
|
+
export async function discoverProviders(
|
|
53
|
+
modelQuery?: string
|
|
54
|
+
): Promise<ProviderInfo[]> {
|
|
55
|
+
const broker = await createZGComputeNetworkReadOnlyBroker(OG_RPC_URL);
|
|
56
|
+
const services = await broker.inference.listService();
|
|
57
|
+
|
|
58
|
+
return services
|
|
59
|
+
.filter((s: any) => {
|
|
60
|
+
const isTee =
|
|
61
|
+
s.verifiability === "TeeML" || s.verifiability === "TeeTLS";
|
|
62
|
+
const modelMatch = modelQuery
|
|
63
|
+
? s.model?.toLowerCase().includes(modelQuery.toLowerCase())
|
|
64
|
+
: true;
|
|
65
|
+
return isTee && modelMatch;
|
|
66
|
+
})
|
|
67
|
+
.map((s: any) => ({
|
|
68
|
+
provider: s.provider,
|
|
69
|
+
url: s.url,
|
|
70
|
+
model: s.model,
|
|
71
|
+
verifiability: s.verifiability,
|
|
72
|
+
teeSignerAddress: s.teeSignerAddress,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run inference directly on a TEE-attested provider and verify the response signature.
|
|
78
|
+
*/
|
|
79
|
+
export async function teeInference(
|
|
80
|
+
systemPrompt: string,
|
|
81
|
+
userPrompt: string,
|
|
82
|
+
modelQuery?: string
|
|
83
|
+
): Promise<TeeInferenceResult> {
|
|
84
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
85
|
+
if (!privateKey) throw new Error("PRIVATE_KEY not set in .env");
|
|
86
|
+
|
|
87
|
+
const provider = new ethers.JsonRpcProvider(OG_RPC_URL);
|
|
88
|
+
const wallet = new ethers.Wallet(privateKey, provider);
|
|
89
|
+
|
|
90
|
+
// 1. Create broker with wallet (needed for auth headers + fees)
|
|
91
|
+
console.log("[TEE] Creating compute broker...");
|
|
92
|
+
const broker = await createZGComputeNetworkBroker(wallet);
|
|
93
|
+
|
|
94
|
+
// 2. Discover providers on-chain
|
|
95
|
+
console.log("[TEE] Discovering providers from on-chain registry...");
|
|
96
|
+
const services = await broker.inference.listService();
|
|
97
|
+
const service = services.find((s: any) => {
|
|
98
|
+
const isTee =
|
|
99
|
+
s.verifiability === "TeeML" || s.verifiability === "TeeTLS";
|
|
100
|
+
const modelMatch = modelQuery
|
|
101
|
+
? s.model?.toLowerCase().includes(modelQuery.toLowerCase())
|
|
102
|
+
: true;
|
|
103
|
+
return isTee && modelMatch;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!service) {
|
|
107
|
+
// Fallback: list what's available
|
|
108
|
+
const available = services.map((s: any) => `${s.model} (${s.verifiability})`);
|
|
109
|
+
throw new Error(
|
|
110
|
+
`No TEE provider found for model query "${modelQuery}". ` +
|
|
111
|
+
`Available: ${available.join(", ") || "none"}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const providerAddress = service.provider;
|
|
116
|
+
const providerUrl = service.url;
|
|
117
|
+
const model = service.model;
|
|
118
|
+
const teeSigner = service.teeSignerAddress;
|
|
119
|
+
|
|
120
|
+
console.log(`[TEE] Found provider: ${providerAddress}`);
|
|
121
|
+
console.log(`[TEE] Model: ${model}`);
|
|
122
|
+
console.log(`[TEE] URL: ${providerUrl}`);
|
|
123
|
+
console.log(`[TEE] TEE signer: ${teeSigner}`);
|
|
124
|
+
console.log(`[TEE] Verifiability: ${service.verifiability}`);
|
|
125
|
+
|
|
126
|
+
// 3. Setup: deposit funds and acknowledge provider
|
|
127
|
+
try {
|
|
128
|
+
await broker.ledger.depositFund(0.001); // Small deposit for testnet
|
|
129
|
+
console.log("[TEE] Deposited funds to ledger");
|
|
130
|
+
} catch (err: any) {
|
|
131
|
+
// May fail if already deposited — that's OK
|
|
132
|
+
console.log("[TEE] Deposit skipped (may already have funds):", err.message?.slice(0, 80));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await broker.inference.acknowledgeProviderSigner(providerAddress);
|
|
137
|
+
console.log("[TEE] Acknowledged provider TEE signer");
|
|
138
|
+
} catch (err: any) {
|
|
139
|
+
console.log("[TEE] Acknowledge skipped (may already be done):", err.message?.slice(0, 80));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 4. Get endpoint and auth headers
|
|
143
|
+
const metadata = await broker.inference.getServiceMetadata(providerAddress);
|
|
144
|
+
const headers = await broker.inference.getRequestHeaders(providerAddress);
|
|
145
|
+
|
|
146
|
+
console.log("[TEE] Calling inference directly on provider...");
|
|
147
|
+
|
|
148
|
+
// 5. Direct inference call to provider (no Router)
|
|
149
|
+
const openai = new OpenAI({ baseURL: metadata.endpoint, apiKey: "not-needed" });
|
|
150
|
+
const { data: completion, response: httpResponse } =
|
|
151
|
+
await openai.chat.completions
|
|
152
|
+
.create(
|
|
153
|
+
{
|
|
154
|
+
model: metadata.model,
|
|
155
|
+
messages: [
|
|
156
|
+
{ role: "system", content: systemPrompt },
|
|
157
|
+
{ role: "user", content: userPrompt },
|
|
158
|
+
],
|
|
159
|
+
temperature: 0.1,
|
|
160
|
+
max_tokens: 1024,
|
|
161
|
+
},
|
|
162
|
+
{ headers: { ...headers } }
|
|
163
|
+
)
|
|
164
|
+
.withResponse();
|
|
165
|
+
|
|
166
|
+
const content = completion.choices[0]?.message?.content;
|
|
167
|
+
if (!content) throw new Error("No response from provider");
|
|
168
|
+
|
|
169
|
+
// Get chat ID from response header (for signature lookup)
|
|
170
|
+
const chatId =
|
|
171
|
+
httpResponse.headers.get("ZG-Res-Key") || completion.id || "unknown";
|
|
172
|
+
|
|
173
|
+
console.log("[TEE] Got response, chat ID:", chatId);
|
|
174
|
+
|
|
175
|
+
// 6. Process response (fees + signature verification via SDK)
|
|
176
|
+
let sdkVerified: boolean | null = null;
|
|
177
|
+
try {
|
|
178
|
+
sdkVerified = await broker.inference.processResponse(
|
|
179
|
+
providerAddress,
|
|
180
|
+
chatId,
|
|
181
|
+
JSON.stringify(completion.usage || {})
|
|
182
|
+
);
|
|
183
|
+
console.log("[TEE] SDK processResponse result:", sdkVerified);
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
console.log("[TEE] SDK processResponse failed:", err.message?.slice(0, 100));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 7. Manual signature verification for full transparency
|
|
189
|
+
let attestation: TeeAttestation | null = null;
|
|
190
|
+
let attestationNote = "";
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const sigUrl = `${providerUrl}/v1/proxy/signature/${chatId}?model=${model}`;
|
|
194
|
+
console.log("[TEE] Fetching per-inference signature...");
|
|
195
|
+
const sigRes = await fetch(sigUrl, { signal: AbortSignal.timeout(10_000) });
|
|
196
|
+
|
|
197
|
+
if (sigRes.ok) {
|
|
198
|
+
const { text: signedText, signature } = (await sigRes.json()) as {
|
|
199
|
+
text: string;
|
|
200
|
+
signature: string;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Determine the expected signing address
|
|
204
|
+
let expectedSigner = teeSigner;
|
|
205
|
+
try {
|
|
206
|
+
const additionalInfo = JSON.parse(
|
|
207
|
+
(service as any).additionalInfo || "{}"
|
|
208
|
+
);
|
|
209
|
+
if (
|
|
210
|
+
additionalInfo.TargetSeparated &&
|
|
211
|
+
additionalInfo.ProviderType !== "centralized" &&
|
|
212
|
+
additionalInfo.TargetTeeAddress
|
|
213
|
+
) {
|
|
214
|
+
expectedSigner = additionalInfo.TargetTeeAddress;
|
|
215
|
+
}
|
|
216
|
+
} catch {}
|
|
217
|
+
|
|
218
|
+
// Verify ECDSA signature
|
|
219
|
+
const messageHash = ethers.hashMessage(signedText);
|
|
220
|
+
const recoveredAddress = ethers.recoverAddress(messageHash, signature);
|
|
221
|
+
const verified =
|
|
222
|
+
recoveredAddress.toLowerCase() === expectedSigner.toLowerCase();
|
|
223
|
+
|
|
224
|
+
attestation = {
|
|
225
|
+
signedText,
|
|
226
|
+
signature,
|
|
227
|
+
teeSigner: expectedSigner,
|
|
228
|
+
recoveredAddress,
|
|
229
|
+
verified,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
attestationNote = verified
|
|
233
|
+
? `PER-INFERENCE TEE attestation VERIFIED. ` +
|
|
234
|
+
`Response signed by ${recoveredAddress}, matches on-chain TEE signer ${expectedSigner}.`
|
|
235
|
+
: `PER-INFERENCE TEE signature recovered ${recoveredAddress} but expected ${expectedSigner}. MISMATCH.`;
|
|
236
|
+
|
|
237
|
+
console.log("[TEE]", attestationNote);
|
|
238
|
+
} else {
|
|
239
|
+
attestationNote = `Signature endpoint returned ${sigRes.status}. Per-inference attestation unavailable.`;
|
|
240
|
+
console.log("[TEE]", attestationNote);
|
|
241
|
+
}
|
|
242
|
+
} catch (err: any) {
|
|
243
|
+
attestationNote = `Signature fetch failed: ${err.message}. Per-inference attestation unavailable.`;
|
|
244
|
+
console.log("[TEE]", attestationNote);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
content,
|
|
249
|
+
chatId,
|
|
250
|
+
model,
|
|
251
|
+
providerAddress,
|
|
252
|
+
providerUrl,
|
|
253
|
+
attestation,
|
|
254
|
+
attestationNote,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Allow direct execution for testing
|
|
259
|
+
if (require.main === module) {
|
|
260
|
+
(async () => {
|
|
261
|
+
console.log("=== TEE Provider Discovery ===\n");
|
|
262
|
+
try {
|
|
263
|
+
const providers = await discoverProviders();
|
|
264
|
+
console.log(`Found ${providers.length} TEE providers:\n`);
|
|
265
|
+
for (const p of providers) {
|
|
266
|
+
console.log(` ${p.model} | ${p.verifiability} | ${p.provider.slice(0, 10)}...`);
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error("Discovery failed:", err);
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
272
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ZgFile, Indexer } from "@0gfoundation/0g-ts-sdk";
|
|
2
|
+
import { ethers } from "ethers";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import "dotenv/config";
|
|
7
|
+
|
|
8
|
+
const OG_RPC_URL = process.env.OG_RPC_URL || "https://evmrpc.0g.ai";
|
|
9
|
+
const OG_INDEXER_RPC =
|
|
10
|
+
process.env.OG_INDEXER_RPC ||
|
|
11
|
+
"https://indexer-storage-mainnet-turbo.0g.ai";
|
|
12
|
+
|
|
13
|
+
export interface UploadResult {
|
|
14
|
+
rootHash: string;
|
|
15
|
+
txHash: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Upload an AI resolution receipt to 0G Storage.
|
|
20
|
+
*
|
|
21
|
+
* Writes the content to a temp file, computes its Merkle tree root,
|
|
22
|
+
* uploads via the 0G Indexer, and returns the root hash + tx hash.
|
|
23
|
+
*/
|
|
24
|
+
export async function uploadReceipt(content: string): Promise<UploadResult> {
|
|
25
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
26
|
+
if (!privateKey) throw new Error("PRIVATE_KEY not set in .env");
|
|
27
|
+
|
|
28
|
+
// Write content to a temp file (ZgFile works with file paths)
|
|
29
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "oracle-"));
|
|
30
|
+
const tmpFile = path.join(tmpDir, "resolution.json");
|
|
31
|
+
fs.writeFileSync(tmpFile, content, { encoding: "utf-8", mode: 0o600 });
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Create ZgFile and compute Merkle tree
|
|
35
|
+
const file = await ZgFile.fromFilePath(tmpFile);
|
|
36
|
+
const [tree, treeErr] = await file.merkleTree();
|
|
37
|
+
if (treeErr !== null || tree === null) {
|
|
38
|
+
throw new Error(`Merkle tree error: ${treeErr}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rootHash = tree.rootHash() as string;
|
|
42
|
+
if (!rootHash) throw new Error("Failed to compute Merkle root hash");
|
|
43
|
+
console.log("[0G Storage] Merkle root hash:", rootHash);
|
|
44
|
+
|
|
45
|
+
// Connect signer
|
|
46
|
+
const provider = new ethers.JsonRpcProvider(OG_RPC_URL);
|
|
47
|
+
const signer = new ethers.Wallet(privateKey, provider);
|
|
48
|
+
|
|
49
|
+
// Upload via Indexer
|
|
50
|
+
const indexer = new Indexer(OG_INDEXER_RPC);
|
|
51
|
+
console.log("[0G Storage] Uploading receipt...");
|
|
52
|
+
|
|
53
|
+
const [tx, uploadErr] = await indexer.upload(file, OG_RPC_URL, signer);
|
|
54
|
+
await file.close();
|
|
55
|
+
|
|
56
|
+
if (uploadErr !== null) {
|
|
57
|
+
throw new Error(`Upload error: ${uploadErr}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const txHash =
|
|
61
|
+
tx && "txHash" in tx ? (tx as any).txHash : "unknown";
|
|
62
|
+
|
|
63
|
+
console.log("[0G Storage] Upload complete!");
|
|
64
|
+
console.log("[0G Storage] TX hash:", txHash);
|
|
65
|
+
|
|
66
|
+
return { rootHash, txHash };
|
|
67
|
+
} finally {
|
|
68
|
+
// Clean up temp files
|
|
69
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Allow running directly: ts-node src/uploadReceipt.ts "your content here"
|
|
74
|
+
if (require.main === module) {
|
|
75
|
+
const content = process.argv[2] || JSON.stringify({
|
|
76
|
+
market: "ETH Gas > 50 gwei",
|
|
77
|
+
resolution: "NO",
|
|
78
|
+
reasoning: "Sample test resolution",
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
uploadReceipt(content)
|
|
83
|
+
.then((result) => {
|
|
84
|
+
console.log("\n--- Upload Result ---");
|
|
85
|
+
console.log("Root Hash:", result.rootHash);
|
|
86
|
+
console.log("TX Hash:", result.txHash);
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
console.error("Upload failed:", err);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
}
|