@sip-protocol/sdk 0.1.9 → 0.2.1

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,237 @@
1
+ /**
2
+ * Oracle Attestation Serialization
3
+ *
4
+ * Canonical serialization of attestation messages for signing and verification.
5
+ *
6
+ * @see docs/specs/ORACLE-ATTESTATION.md Section 2.2
7
+ */
8
+
9
+ import { sha256 } from '@noble/hashes/sha256'
10
+ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils'
11
+ import type { HexString } from '@sip-protocol/types'
12
+ import type { OracleAttestationMessage } from './types'
13
+ import { ORACLE_DOMAIN, CHAIN_NUMERIC_IDS } from './types'
14
+ import { ValidationError } from '../errors'
15
+
16
+ /**
17
+ * Serialize an attestation message to canonical byte format
18
+ *
19
+ * Layout (197 bytes total):
20
+ * - version: 1 byte
21
+ * - chainId: 4 bytes (big-endian)
22
+ * - intentHash: 32 bytes
23
+ * - recipient: 32 bytes
24
+ * - amount: 16 bytes (big-endian u128)
25
+ * - assetId: 32 bytes
26
+ * - txHash: 32 bytes
27
+ * - blockNumber: 8 bytes (big-endian)
28
+ * - blockHash: 32 bytes
29
+ * - timestamp: 8 bytes (big-endian)
30
+ */
31
+ export function serializeAttestationMessage(
32
+ message: OracleAttestationMessage
33
+ ): Uint8Array {
34
+ const buffer = new Uint8Array(197)
35
+ const view = new DataView(buffer.buffer)
36
+ let offset = 0
37
+
38
+ // version (1 byte)
39
+ buffer[offset++] = message.version
40
+
41
+ // chainId (4 bytes, big-endian)
42
+ view.setUint32(offset, message.chainId, false)
43
+ offset += 4
44
+
45
+ // intentHash (32 bytes)
46
+ const intentHashBytes = normalizeToBytes(message.intentHash, 32, 'intentHash')
47
+ buffer.set(intentHashBytes, offset)
48
+ offset += 32
49
+
50
+ // recipient (32 bytes, zero-padded if needed)
51
+ const recipientBytes = normalizeToBytes(message.recipient, 32, 'recipient')
52
+ buffer.set(recipientBytes, offset)
53
+ offset += 32
54
+
55
+ // amount (16 bytes, big-endian u128)
56
+ const amountBytes = bigintToBytes(message.amount, 16)
57
+ buffer.set(amountBytes, offset)
58
+ offset += 16
59
+
60
+ // assetId (32 bytes)
61
+ const assetIdBytes = normalizeToBytes(message.assetId, 32, 'assetId')
62
+ buffer.set(assetIdBytes, offset)
63
+ offset += 32
64
+
65
+ // txHash (32 bytes)
66
+ const txHashBytes = normalizeToBytes(message.txHash, 32, 'txHash')
67
+ buffer.set(txHashBytes, offset)
68
+ offset += 32
69
+
70
+ // blockNumber (8 bytes, big-endian)
71
+ view.setBigUint64(offset, message.blockNumber, false)
72
+ offset += 8
73
+
74
+ // blockHash (32 bytes)
75
+ const blockHashBytes = normalizeToBytes(message.blockHash, 32, 'blockHash')
76
+ buffer.set(blockHashBytes, offset)
77
+ offset += 32
78
+
79
+ // timestamp (8 bytes, big-endian)
80
+ view.setBigUint64(offset, BigInt(message.timestamp), false)
81
+
82
+ return buffer
83
+ }
84
+
85
+ /**
86
+ * Deserialize bytes back to attestation message
87
+ */
88
+ export function deserializeAttestationMessage(
89
+ bytes: Uint8Array
90
+ ): OracleAttestationMessage {
91
+ if (bytes.length !== 197) {
92
+ throw new ValidationError(
93
+ `Invalid attestation message length: ${bytes.length}, expected 197`,
94
+ 'bytes'
95
+ )
96
+ }
97
+
98
+ const view = new DataView(bytes.buffer, bytes.byteOffset)
99
+ let offset = 0
100
+
101
+ // version
102
+ const version = bytes[offset++]
103
+
104
+ // chainId
105
+ const chainId = view.getUint32(offset, false)
106
+ offset += 4
107
+
108
+ // intentHash
109
+ const intentHash = `0x${bytesToHex(bytes.slice(offset, offset + 32))}` as HexString
110
+ offset += 32
111
+
112
+ // recipient
113
+ const recipient = `0x${bytesToHex(bytes.slice(offset, offset + 32))}` as HexString
114
+ offset += 32
115
+
116
+ // amount
117
+ const amount = bytesToBigint(bytes.slice(offset, offset + 16))
118
+ offset += 16
119
+
120
+ // assetId
121
+ const assetId = `0x${bytesToHex(bytes.slice(offset, offset + 32))}` as HexString
122
+ offset += 32
123
+
124
+ // txHash
125
+ const txHash = `0x${bytesToHex(bytes.slice(offset, offset + 32))}` as HexString
126
+ offset += 32
127
+
128
+ // blockNumber
129
+ const blockNumber = view.getBigUint64(offset, false)
130
+ offset += 8
131
+
132
+ // blockHash
133
+ const blockHash = `0x${bytesToHex(bytes.slice(offset, offset + 32))}` as HexString
134
+ offset += 32
135
+
136
+ // timestamp
137
+ const timestamp = Number(view.getBigUint64(offset, false))
138
+
139
+ return {
140
+ version,
141
+ chainId,
142
+ intentHash,
143
+ recipient,
144
+ amount,
145
+ assetId,
146
+ txHash,
147
+ blockNumber,
148
+ blockHash,
149
+ timestamp,
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Compute the hash to be signed for an attestation
155
+ *
156
+ * hash = SHA256(domain || serialized_message)
157
+ */
158
+ export function computeAttestationHash(
159
+ message: OracleAttestationMessage
160
+ ): Uint8Array {
161
+ const domain = utf8ToBytes(ORACLE_DOMAIN)
162
+ const messageBytes = serializeAttestationMessage(message)
163
+
164
+ // Concatenate domain and message
165
+ const toHash = new Uint8Array(domain.length + messageBytes.length)
166
+ toHash.set(domain, 0)
167
+ toHash.set(messageBytes, domain.length)
168
+
169
+ return sha256(toHash)
170
+ }
171
+
172
+ /**
173
+ * Get the numeric chain ID for a chain identifier
174
+ */
175
+ export function getChainNumericId(chain: string): number {
176
+ const id = CHAIN_NUMERIC_IDS[chain as keyof typeof CHAIN_NUMERIC_IDS]
177
+ if (id === undefined) {
178
+ throw new ValidationError(`Unknown chain: ${chain}`, 'chain')
179
+ }
180
+ return id
181
+ }
182
+
183
+ // ─── Utility Functions ────────────────────────────────────────────────────────
184
+
185
+ /**
186
+ * Convert hex string to bytes, normalizing to specified length
187
+ */
188
+ function normalizeToBytes(
189
+ hex: HexString,
190
+ length: number,
191
+ field: string
192
+ ): Uint8Array {
193
+ const stripped = hex.startsWith('0x') ? hex.slice(2) : hex
194
+ const bytes = hexToBytes(stripped)
195
+
196
+ if (bytes.length === length) {
197
+ return bytes
198
+ }
199
+
200
+ if (bytes.length > length) {
201
+ throw new ValidationError(
202
+ `${field} is too long: ${bytes.length} bytes, max ${length}`,
203
+ field
204
+ )
205
+ }
206
+
207
+ // Zero-pad on the left
208
+ const padded = new Uint8Array(length)
209
+ padded.set(bytes, length - bytes.length)
210
+ return padded
211
+ }
212
+
213
+ /**
214
+ * Convert bigint to big-endian bytes
215
+ */
216
+ function bigintToBytes(value: bigint, length: number): Uint8Array {
217
+ const bytes = new Uint8Array(length)
218
+ let v = value
219
+
220
+ for (let i = length - 1; i >= 0; i--) {
221
+ bytes[i] = Number(v & 0xffn)
222
+ v >>= 8n
223
+ }
224
+
225
+ return bytes
226
+ }
227
+
228
+ /**
229
+ * Convert big-endian bytes to bigint
230
+ */
231
+ function bytesToBigint(bytes: Uint8Array): bigint {
232
+ let result = 0n
233
+ for (const byte of bytes) {
234
+ result = (result << 8n) + BigInt(byte)
235
+ }
236
+ return result
237
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Oracle Attestation Types
3
+ *
4
+ * Type definitions for the oracle attestation protocol.
5
+ *
6
+ * @see docs/specs/ORACLE-ATTESTATION.md
7
+ */
8
+
9
+ import type { HexString, ChainId } from '@sip-protocol/types'
10
+
11
+ // ─── Oracle Identity ──────────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * Oracle identifier (SHA256 hash of public key)
15
+ */
16
+ export type OracleId = HexString
17
+
18
+ /**
19
+ * Oracle status in the registry
20
+ */
21
+ export type OracleStatus = 'active' | 'suspended' | 'removed'
22
+
23
+ /**
24
+ * Oracle information stored in registry
25
+ */
26
+ export interface OracleInfo {
27
+ /** Unique oracle identifier (hash of public key) */
28
+ id: OracleId
29
+
30
+ /** Oracle's Ed25519 public key (32 bytes) */
31
+ publicKey: HexString
32
+
33
+ /** Human-readable name */
34
+ name: string
35
+
36
+ /** Supported destination chains */
37
+ supportedChains: ChainId[]
38
+
39
+ /** Oracle endpoint URL */
40
+ endpoint: string
41
+
42
+ /** Registration timestamp (Unix seconds) */
43
+ registeredAt: number
44
+
45
+ /** Current status */
46
+ status: OracleStatus
47
+
48
+ /** Reputation score (0-100) */
49
+ reputation: number
50
+
51
+ /** Staked amount in smallest unit */
52
+ stake: bigint
53
+ }
54
+
55
+ // ─── Attestation Message ──────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * The canonical message format that oracles sign
59
+ *
60
+ * This structure is serialized deterministically for signing.
61
+ * Total serialized size: 197 bytes
62
+ */
63
+ export interface OracleAttestationMessage {
64
+ /** Protocol version (current: 1) */
65
+ version: number
66
+
67
+ /** Destination chain numeric ID */
68
+ chainId: number
69
+
70
+ /** Hash of original intent (32 bytes) */
71
+ intentHash: HexString
72
+
73
+ /** Recipient address, normalized to 32 bytes */
74
+ recipient: HexString
75
+
76
+ /** Amount delivered in smallest unit */
77
+ amount: bigint
78
+
79
+ /** Asset identifier hash (32 bytes) */
80
+ assetId: HexString
81
+
82
+ /** Transaction hash on destination chain (32 bytes) */
83
+ txHash: HexString
84
+
85
+ /** Block number containing transaction */
86
+ blockNumber: bigint
87
+
88
+ /** Block hash for finality verification (32 bytes) */
89
+ blockHash: HexString
90
+
91
+ /** Unix timestamp of attestation creation */
92
+ timestamp: number
93
+ }
94
+
95
+ // ─── Signatures ───────────────────────────────────────────────────────────────
96
+
97
+ /**
98
+ * A single oracle's signature on an attestation
99
+ */
100
+ export interface OracleSignature {
101
+ /** Oracle that produced this signature */
102
+ oracleId: OracleId
103
+
104
+ /** Ed25519 signature (64 bytes) */
105
+ signature: HexString
106
+ }
107
+
108
+ /**
109
+ * Complete attestation with message and signatures
110
+ */
111
+ export interface SignedOracleAttestation {
112
+ /** The attested message */
113
+ message: OracleAttestationMessage
114
+
115
+ /** Signatures from k-of-n oracles */
116
+ signatures: OracleSignature[]
117
+ }
118
+
119
+ // ─── Registry ─────────────────────────────────────────────────────────────────
120
+
121
+ /**
122
+ * Oracle registry containing all registered oracles
123
+ */
124
+ export interface OracleRegistry {
125
+ /** Registered oracles indexed by ID */
126
+ oracles: Map<OracleId, OracleInfo>
127
+
128
+ /** Required signature threshold (k in k-of-n) */
129
+ threshold: number
130
+
131
+ /** Total number of oracles (n in k-of-n) */
132
+ totalOracles: number
133
+
134
+ /** Registry version for upgrades */
135
+ version: number
136
+
137
+ /** Last update timestamp */
138
+ lastUpdated: number
139
+ }
140
+
141
+ /**
142
+ * Oracle registry configuration
143
+ */
144
+ export interface OracleRegistryConfig {
145
+ /** Minimum required signatures (default: 3) */
146
+ threshold?: number
147
+
148
+ /** Oracle endpoint timeout in ms (default: 30000) */
149
+ timeout?: number
150
+
151
+ /** Custom registry data (for testing) */
152
+ customOracles?: OracleInfo[]
153
+ }
154
+
155
+ // ─── Attestation Request ──────────────────────────────────────────────────────
156
+
157
+ /**
158
+ * Request for oracle attestation
159
+ */
160
+ export interface AttestationRequest {
161
+ /** Intent hash to attest */
162
+ intentHash: HexString
163
+
164
+ /** Destination chain */
165
+ destinationChain: ChainId
166
+
167
+ /** Expected recipient address */
168
+ expectedRecipient: HexString
169
+
170
+ /** Expected asset identifier */
171
+ expectedAsset: HexString
172
+
173
+ /** Minimum amount expected */
174
+ minAmount: bigint
175
+
176
+ /** Deadline for fulfillment (Unix timestamp) */
177
+ deadline: number
178
+ }
179
+
180
+ /**
181
+ * Result of attestation request
182
+ */
183
+ export interface AttestationResult {
184
+ /** Whether attestation was successful */
185
+ success: boolean
186
+
187
+ /** The signed attestation (if successful) */
188
+ attestation?: SignedOracleAttestation
189
+
190
+ /** Error message (if failed) */
191
+ error?: string
192
+
193
+ /** Number of oracles that responded */
194
+ oracleResponses: number
195
+
196
+ /** Number of valid signatures collected */
197
+ validSignatures: number
198
+ }
199
+
200
+ // ─── Verification ─────────────────────────────────────────────────────────────
201
+
202
+ /**
203
+ * Result of attestation verification
204
+ */
205
+ export interface VerificationResult {
206
+ /** Whether verification passed */
207
+ valid: boolean
208
+
209
+ /** Number of valid signatures found */
210
+ validSignatures: number
211
+
212
+ /** Required threshold */
213
+ threshold: number
214
+
215
+ /** List of oracle IDs with valid signatures */
216
+ validOracles: OracleId[]
217
+
218
+ /** Error details (if invalid) */
219
+ errors?: string[]
220
+ }
221
+
222
+ // ─── Constants ────────────────────────────────────────────────────────────────
223
+
224
+ /**
225
+ * Domain separator for attestation signing
226
+ */
227
+ export const ORACLE_DOMAIN = 'SIP-ORACLE-ATTESTATION-V1'
228
+
229
+ /**
230
+ * Current attestation protocol version
231
+ */
232
+ export const ATTESTATION_VERSION = 1
233
+
234
+ /**
235
+ * Default signature threshold
236
+ */
237
+ export const DEFAULT_THRESHOLD = 3
238
+
239
+ /**
240
+ * Default total oracles
241
+ */
242
+ export const DEFAULT_TOTAL_ORACLES = 5
243
+
244
+ /**
245
+ * Chain numeric IDs for attestation message
246
+ */
247
+ export const CHAIN_NUMERIC_IDS: Record<ChainId, number> = {
248
+ ethereum: 1,
249
+ polygon: 137,
250
+ arbitrum: 42161,
251
+ optimism: 10,
252
+ base: 8453,
253
+ bitcoin: 0, // Non-standard, SIP-specific (Bitcoin mainnet)
254
+ solana: 501, // Non-standard, SIP-specific
255
+ near: 502, // Non-standard, SIP-specific
256
+ zcash: 503, // Non-standard, SIP-specific
257
+ }