@sip-protocol/sdk 0.1.9 → 0.2.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.
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Oracle Attestation Verification
3
+ *
4
+ * Verification of oracle signatures on attestation messages.
5
+ *
6
+ * @see docs/specs/ORACLE-ATTESTATION.md Section 4
7
+ */
8
+
9
+ import { ed25519 } from '@noble/curves/ed25519'
10
+ import { sha256 } from '@noble/hashes/sha256'
11
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
12
+ import type { HexString } from '@sip-protocol/types'
13
+ import type {
14
+ OracleId,
15
+ OracleInfo,
16
+ OracleRegistry,
17
+ OracleRegistryConfig,
18
+ OracleSignature,
19
+ SignedOracleAttestation,
20
+ VerificationResult,
21
+ } from './types'
22
+ import { DEFAULT_THRESHOLD, DEFAULT_TOTAL_ORACLES } from './types'
23
+ import { computeAttestationHash } from './serialization'
24
+
25
+ /**
26
+ * Derive oracle ID from public key
27
+ *
28
+ * OracleId = SHA256(publicKey)
29
+ */
30
+ export function deriveOracleId(publicKey: HexString | Uint8Array): OracleId {
31
+ const keyBytes = typeof publicKey === 'string'
32
+ ? hexToBytes(publicKey.startsWith('0x') ? publicKey.slice(2) : publicKey)
33
+ : publicKey
34
+
35
+ const hash = sha256(keyBytes)
36
+ return `0x${bytesToHex(hash)}` as OracleId
37
+ }
38
+
39
+ /**
40
+ * Verify a signed oracle attestation
41
+ *
42
+ * Checks:
43
+ * 1. Sufficient signatures (>= threshold)
44
+ * 2. All signatures are from registered, active oracles
45
+ * 3. All signatures are valid Ed25519 signatures
46
+ * 4. No duplicate oracles
47
+ */
48
+ export function verifyAttestation(
49
+ attestation: SignedOracleAttestation,
50
+ registry: OracleRegistry
51
+ ): VerificationResult {
52
+ const { message, signatures } = attestation
53
+ const errors: string[] = []
54
+ const validOracles: OracleId[] = []
55
+
56
+ // Check we have enough signatures
57
+ if (signatures.length < registry.threshold) {
58
+ errors.push(
59
+ `Insufficient signatures: ${signatures.length} < ${registry.threshold} required`
60
+ )
61
+ }
62
+
63
+ // Compute message hash for verification
64
+ const messageHash = computeAttestationHash(message)
65
+
66
+ // Track seen oracles to prevent duplicates
67
+ const seenOracles = new Set<OracleId>()
68
+ let validCount = 0
69
+
70
+ for (const sig of signatures) {
71
+ // Check for duplicate oracles
72
+ if (seenOracles.has(sig.oracleId)) {
73
+ errors.push(`Duplicate signature from oracle: ${sig.oracleId}`)
74
+ continue
75
+ }
76
+ seenOracles.add(sig.oracleId)
77
+
78
+ // Get oracle from registry
79
+ const oracle = registry.oracles.get(sig.oracleId)
80
+ if (!oracle) {
81
+ errors.push(`Unknown oracle: ${sig.oracleId}`)
82
+ continue
83
+ }
84
+
85
+ if (oracle.status !== 'active') {
86
+ errors.push(`Oracle not active: ${sig.oracleId} (status: ${oracle.status})`)
87
+ continue
88
+ }
89
+
90
+ // Verify Ed25519 signature
91
+ try {
92
+ const publicKeyBytes = hexToBytes(
93
+ oracle.publicKey.startsWith('0x')
94
+ ? oracle.publicKey.slice(2)
95
+ : oracle.publicKey
96
+ )
97
+ const signatureBytes = hexToBytes(
98
+ sig.signature.startsWith('0x')
99
+ ? sig.signature.slice(2)
100
+ : sig.signature
101
+ )
102
+
103
+ const isValid = ed25519.verify(signatureBytes, messageHash, publicKeyBytes)
104
+
105
+ if (isValid) {
106
+ validCount++
107
+ validOracles.push(sig.oracleId)
108
+ } else {
109
+ errors.push(`Invalid signature from oracle: ${sig.oracleId}`)
110
+ }
111
+ } catch (e) {
112
+ errors.push(`Signature verification error for ${sig.oracleId}: ${e}`)
113
+ }
114
+ }
115
+
116
+ const valid = validCount >= registry.threshold && errors.length === 0
117
+
118
+ return {
119
+ valid,
120
+ validSignatures: validCount,
121
+ threshold: registry.threshold,
122
+ validOracles,
123
+ errors: errors.length > 0 ? errors : undefined,
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Verify a single oracle signature
129
+ */
130
+ export function verifyOracleSignature(
131
+ signature: OracleSignature,
132
+ messageHash: Uint8Array,
133
+ oracle: OracleInfo
134
+ ): boolean {
135
+ try {
136
+ const publicKeyBytes = hexToBytes(
137
+ oracle.publicKey.startsWith('0x')
138
+ ? oracle.publicKey.slice(2)
139
+ : oracle.publicKey
140
+ )
141
+ const signatureBytes = hexToBytes(
142
+ signature.signature.startsWith('0x')
143
+ ? signature.signature.slice(2)
144
+ : signature.signature
145
+ )
146
+
147
+ return ed25519.verify(signatureBytes, messageHash, publicKeyBytes)
148
+ } catch {
149
+ return false
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Sign an attestation message (for oracle implementations)
155
+ */
156
+ export function signAttestationMessage(
157
+ messageHash: Uint8Array,
158
+ privateKey: Uint8Array
159
+ ): OracleSignature {
160
+ const signature = ed25519.sign(messageHash, privateKey)
161
+ const publicKey = ed25519.getPublicKey(privateKey)
162
+ const oracleId = deriveOracleId(publicKey)
163
+
164
+ return {
165
+ oracleId,
166
+ signature: `0x${bytesToHex(signature)}` as HexString,
167
+ }
168
+ }
169
+
170
+ // ─── Registry Management ──────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * Create a new oracle registry
174
+ */
175
+ export function createOracleRegistry(
176
+ config: OracleRegistryConfig = {}
177
+ ): OracleRegistry {
178
+ const registry: OracleRegistry = {
179
+ oracles: new Map(),
180
+ threshold: config.threshold ?? DEFAULT_THRESHOLD,
181
+ totalOracles: 0,
182
+ version: 1,
183
+ lastUpdated: Date.now(),
184
+ }
185
+
186
+ // Add custom oracles if provided
187
+ if (config.customOracles) {
188
+ for (const oracle of config.customOracles) {
189
+ registry.oracles.set(oracle.id, oracle)
190
+ }
191
+ registry.totalOracles = config.customOracles.length
192
+ }
193
+
194
+ return registry
195
+ }
196
+
197
+ /**
198
+ * Add an oracle to the registry
199
+ */
200
+ export function addOracle(
201
+ registry: OracleRegistry,
202
+ oracle: OracleInfo
203
+ ): void {
204
+ registry.oracles.set(oracle.id, oracle)
205
+ registry.totalOracles = registry.oracles.size
206
+ registry.lastUpdated = Date.now()
207
+ }
208
+
209
+ /**
210
+ * Remove an oracle from the registry
211
+ */
212
+ export function removeOracle(
213
+ registry: OracleRegistry,
214
+ oracleId: OracleId
215
+ ): boolean {
216
+ const removed = registry.oracles.delete(oracleId)
217
+ if (removed) {
218
+ registry.totalOracles = registry.oracles.size
219
+ registry.lastUpdated = Date.now()
220
+ }
221
+ return removed
222
+ }
223
+
224
+ /**
225
+ * Update oracle status
226
+ */
227
+ export function updateOracleStatus(
228
+ registry: OracleRegistry,
229
+ oracleId: OracleId,
230
+ status: OracleInfo['status']
231
+ ): boolean {
232
+ const oracle = registry.oracles.get(oracleId)
233
+ if (!oracle) {
234
+ return false
235
+ }
236
+
237
+ oracle.status = status
238
+ registry.lastUpdated = Date.now()
239
+ return true
240
+ }
241
+
242
+ /**
243
+ * Get all active oracles
244
+ */
245
+ export function getActiveOracles(registry: OracleRegistry): OracleInfo[] {
246
+ return Array.from(registry.oracles.values()).filter(
247
+ (oracle) => oracle.status === 'active'
248
+ )
249
+ }
250
+
251
+ /**
252
+ * Check if registry has enough active oracles for threshold
253
+ */
254
+ export function hasEnoughOracles(registry: OracleRegistry): boolean {
255
+ const activeCount = getActiveOracles(registry).length
256
+ return activeCount >= registry.threshold
257
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Browser-compatible utilities for proof generation
3
+ *
4
+ * These utilities replace Node.js-specific functions (like Buffer)
5
+ * with browser-compatible alternatives using Web APIs.
6
+ *
7
+ * @module proofs/browser-utils
8
+ */
9
+
10
+ /**
11
+ * Convert hex string to Uint8Array (browser-compatible)
12
+ */
13
+ export function hexToBytes(hex: string): Uint8Array {
14
+ const h = hex.startsWith('0x') ? hex.slice(2) : hex
15
+ if (h.length === 0) return new Uint8Array(0)
16
+ if (h.length % 2 !== 0) {
17
+ throw new Error('Hex string must have even length')
18
+ }
19
+ const bytes = new Uint8Array(h.length / 2)
20
+ for (let i = 0; i < bytes.length; i++) {
21
+ bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16)
22
+ }
23
+ return bytes
24
+ }
25
+
26
+ /**
27
+ * Convert Uint8Array to hex string (browser-compatible)
28
+ */
29
+ export function bytesToHex(bytes: Uint8Array): string {
30
+ return Array.from(bytes)
31
+ .map((b) => b.toString(16).padStart(2, '0'))
32
+ .join('')
33
+ }
34
+
35
+ /**
36
+ * Check if running in browser environment
37
+ */
38
+ export function isBrowser(): boolean {
39
+ return typeof window !== 'undefined' && typeof window.document !== 'undefined'
40
+ }
41
+
42
+ /**
43
+ * Check if Web Workers are available
44
+ */
45
+ export function supportsWebWorkers(): boolean {
46
+ return typeof Worker !== 'undefined'
47
+ }
48
+
49
+ /**
50
+ * Check if SharedArrayBuffer is available (required for some WASM operations)
51
+ */
52
+ export function supportsSharedArrayBuffer(): boolean {
53
+ try {
54
+ return typeof SharedArrayBuffer !== 'undefined'
55
+ } catch {
56
+ return false
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get browser info for diagnostics
62
+ */
63
+ export function getBrowserInfo(): {
64
+ isBrowser: boolean
65
+ supportsWorkers: boolean
66
+ supportsSharedArrayBuffer: boolean
67
+ userAgent: string | null
68
+ } {
69
+ return {
70
+ isBrowser: isBrowser(),
71
+ supportsWorkers: supportsWebWorkers(),
72
+ supportsSharedArrayBuffer: supportsSharedArrayBuffer(),
73
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Load script dynamically (for WASM loading)
79
+ */
80
+ export function loadScript(src: string): Promise<void> {
81
+ return new Promise((resolve, reject) => {
82
+ if (!isBrowser()) {
83
+ reject(new Error('loadScript can only be used in browser'))
84
+ return
85
+ }
86
+ const script = document.createElement('script')
87
+ script.src = src
88
+ script.onload = () => resolve()
89
+ script.onerror = () => reject(new Error(`Failed to load script: ${src}`))
90
+ document.head.appendChild(script)
91
+ })
92
+ }
93
+
94
+ /**
95
+ * Estimate available memory (approximate, browser-specific)
96
+ */
97
+ export async function estimateAvailableMemory(): Promise<number | null> {
98
+ if (!isBrowser()) return null
99
+
100
+ // Use Performance API if available (Chrome)
101
+ // @ts-expect-error - Performance.measureUserAgentSpecificMemory is Chrome-specific
102
+ if (typeof performance !== 'undefined' && performance.measureUserAgentSpecificMemory) {
103
+ try {
104
+ // @ts-expect-error - Chrome-specific API
105
+ const result = await performance.measureUserAgentSpecificMemory()
106
+ return result.bytes
107
+ } catch {
108
+ // API not available or permission denied
109
+ }
110
+ }
111
+
112
+ // Use navigator.deviceMemory if available (Chrome, Opera)
113
+ // @ts-expect-error - deviceMemory is non-standard
114
+ if (typeof navigator !== 'undefined' && navigator.deviceMemory) {
115
+ // Returns approximate device memory in GB
116
+ // @ts-expect-error - deviceMemory is non-standard
117
+ return navigator.deviceMemory * 1024 * 1024 * 1024
118
+ }
119
+
120
+ return null
121
+ }
122
+
123
+ /**
124
+ * Create a blob URL for worker code (inline worker support)
125
+ */
126
+ export function createWorkerBlobUrl(code: string): string {
127
+ if (!isBrowser()) {
128
+ throw new Error('createWorkerBlobUrl can only be used in browser')
129
+ }
130
+ const blob = new Blob([code], { type: 'application/javascript' })
131
+ return URL.createObjectURL(blob)
132
+ }
133
+
134
+ /**
135
+ * Revoke a blob URL to free memory
136
+ */
137
+ export function revokeWorkerBlobUrl(url: string): void {
138
+ if (isBrowser() && url.startsWith('blob:')) {
139
+ URL.revokeObjectURL(url)
140
+ }
141
+ }