openttt 0.1.2 → 0.1.3

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.
Files changed (43) hide show
  1. package/README.md +4 -4
  2. package/dist/adaptive_switch.d.ts +22 -7
  3. package/dist/adaptive_switch.js +52 -15
  4. package/dist/auto_mint.d.ts +22 -7
  5. package/dist/auto_mint.js +107 -30
  6. package/dist/ct_log.d.ts +47 -0
  7. package/dist/ct_log.js +107 -0
  8. package/dist/dynamic_fee.d.ts +13 -2
  9. package/dist/dynamic_fee.js +62 -11
  10. package/dist/errors.d.ts +44 -25
  11. package/dist/errors.js +58 -42
  12. package/dist/evm_connector.d.ts +28 -1
  13. package/dist/evm_connector.js +124 -32
  14. package/dist/index.d.ts +4 -5
  15. package/dist/index.js +4 -5
  16. package/dist/logger.d.ts +36 -4
  17. package/dist/logger.js +70 -11
  18. package/dist/networks.d.ts +21 -0
  19. package/dist/networks.js +30 -4
  20. package/dist/pool_registry.d.ts +9 -0
  21. package/dist/pool_registry.js +37 -0
  22. package/dist/pot_signer.d.ts +15 -0
  23. package/dist/pot_signer.js +28 -0
  24. package/dist/protocol_fee.d.ts +42 -26
  25. package/dist/protocol_fee.js +77 -54
  26. package/dist/revenue_tiers.d.ts +36 -0
  27. package/dist/revenue_tiers.js +83 -0
  28. package/dist/signer.d.ts +1 -2
  29. package/dist/signer.js +72 -14
  30. package/dist/time_synthesis.d.ts +38 -0
  31. package/dist/time_synthesis.js +134 -20
  32. package/dist/trust_store.d.ts +49 -0
  33. package/dist/trust_store.js +89 -0
  34. package/dist/ttt_builder.d.ts +1 -1
  35. package/dist/ttt_builder.js +2 -2
  36. package/dist/ttt_client.d.ts +24 -29
  37. package/dist/ttt_client.js +97 -28
  38. package/dist/types.d.ts +46 -3
  39. package/dist/v4_hook.d.ts +10 -2
  40. package/dist/v4_hook.js +10 -2
  41. package/dist/x402_enforcer.d.ts +17 -2
  42. package/dist/x402_enforcer.js +27 -2
  43. package/package.json +1 -1
package/dist/signer.js CHANGED
@@ -36,8 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.KMSSigner = exports.PrivySigner = exports.TurnkeySignerWrapper = exports.PrivateKeySigner = exports.TTTAbstractSigner = exports.SignerType = void 0;
37
37
  exports.createSigner = createSigner;
38
38
  const ethers_1 = require("ethers");
39
- const ethers_2 = require("@turnkey/ethers");
40
- const api_key_stamper_1 = require("@turnkey/api-key-stamper");
41
39
  const errors_1 = require("./errors");
42
40
  /**
43
41
  * Supported signer types in TTT SDK
@@ -98,6 +96,63 @@ class KMSSigner extends TTTAbstractSigner {
98
96
  }
99
97
  }
100
98
  exports.KMSSigner = KMSSigner;
99
+ /**
100
+ * Extract the uncompressed secp256k1 public key from a DER-encoded
101
+ * SubjectPublicKeyInfo structure using proper ASN.1 parsing.
102
+ *
103
+ * DER layout: SEQUENCE { SEQUENCE { OID, PARAMS }, BITSTRING { 0x00, key } }
104
+ * We locate the BITSTRING tag (0x03), read its length, skip the
105
+ * "unused bits" byte, and return the remaining 65 bytes (04 || X || Y).
106
+ */
107
+ function extractPublicKeyFromDER(der) {
108
+ let pos = 0;
109
+ function readTag() {
110
+ if (pos >= der.length)
111
+ throw new Error("DER parse error: unexpected end of data while reading tag");
112
+ return der[pos++];
113
+ }
114
+ function readLength() {
115
+ if (pos >= der.length)
116
+ throw new Error("DER parse error: unexpected end of data while reading length");
117
+ const first = der[pos++];
118
+ if (first < 0x80)
119
+ return first;
120
+ const numBytes = first & 0x7f;
121
+ if (numBytes === 0 || numBytes > 4)
122
+ throw new Error(`DER parse error: unsupported length encoding (${numBytes} bytes)`);
123
+ let length = 0;
124
+ for (let i = 0; i < numBytes; i++) {
125
+ if (pos >= der.length)
126
+ throw new Error("DER parse error: unexpected end of data in multi-byte length");
127
+ length = (length << 8) | der[pos++];
128
+ }
129
+ return length;
130
+ }
131
+ // Outer SEQUENCE
132
+ const outerTag = readTag();
133
+ if (outerTag !== 0x30)
134
+ throw new Error(`DER parse error: expected SEQUENCE (0x30), got 0x${outerTag.toString(16)}`);
135
+ readLength(); // outer sequence length
136
+ // Inner SEQUENCE (algorithm identifier) -- skip it entirely
137
+ const innerTag = readTag();
138
+ if (innerTag !== 0x30)
139
+ throw new Error(`DER parse error: expected inner SEQUENCE (0x30), got 0x${innerTag.toString(16)}`);
140
+ const innerLen = readLength();
141
+ pos += innerLen; // skip OID + params
142
+ // BITSTRING containing the public key
143
+ const bsTag = readTag();
144
+ if (bsTag !== 0x03)
145
+ throw new Error(`DER parse error: expected BITSTRING (0x03), got 0x${bsTag.toString(16)}`);
146
+ const bsLen = readLength();
147
+ const unusedBits = der[pos++];
148
+ if (unusedBits !== 0x00)
149
+ throw new Error(`DER parse error: expected 0 unused bits in BITSTRING, got ${unusedBits}`);
150
+ const keyBytes = der.slice(pos, pos + bsLen - 1);
151
+ if (keyBytes.length !== 65 || keyBytes[0] !== 0x04) {
152
+ throw new Error(`DER parse error: expected 65-byte uncompressed secp256k1 key (04||X||Y), got ${keyBytes.length} bytes`);
153
+ }
154
+ return keyBytes;
155
+ }
101
156
  /**
102
157
  * Internal utility to parse DER signature to R and S
103
158
  */
@@ -146,7 +201,7 @@ class AWSKMSEthersSigner extends ethers_1.AbstractSigner {
146
201
  const command = new GetPublicKeyCommand({ KeyId: this.keyId });
147
202
  const response = await this.client.send(command);
148
203
  const publicKeyDer = response.PublicKey;
149
- const pubKey = Buffer.from(publicKeyDer).slice(-65);
204
+ const pubKey = extractPublicKeyFromDER(Buffer.from(publicKeyDer));
150
205
  this.address = (0, ethers_1.computeAddress)("0x" + pubKey.toString('hex'));
151
206
  return this.address;
152
207
  }
@@ -213,7 +268,7 @@ class GCPKMSEthersSigner extends ethers_1.AbstractSigner {
213
268
  const pem = publicKey.pem;
214
269
  const base64 = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\n|\r/g, '');
215
270
  const der = Buffer.from(base64, 'base64');
216
- const pubKey = der.slice(-65);
271
+ const pubKey = extractPublicKeyFromDER(der);
217
272
  this.address = (0, ethers_1.computeAddress)("0x" + pubKey.toString('hex'));
218
273
  return this.address;
219
274
  }
@@ -267,20 +322,23 @@ async function createSigner(config) {
267
322
  key = process.env[config.envVar];
268
323
  }
269
324
  if (!key)
270
- throw new errors_1.TTTSignerError("[Signer] Private key missing", `Neither 'key' nor env var '${config.envVar}' was provided`, "Set the environment variable or provide the key directly in config.");
325
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_MISSING_KEY, "[Signer] Private key missing", `Neither 'key' nor env var '${config.envVar}' was provided`, "Set the environment variable or provide the key directly in config.");
271
326
  if (!key.startsWith('0x'))
272
327
  key = '0x' + key;
273
328
  if (!(0, ethers_1.isHexString)(key, 32))
274
- throw new errors_1.TTTSignerError("[Signer] Invalid private key format", "Expected 0x + 64 hex characters", "Provide a valid 32-byte hex private key.");
329
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_INVALID_KEY_FORMAT, "[Signer] Invalid private key format", "Expected 0x + 64 hex characters", "Provide a valid 32-byte hex private key.");
275
330
  const wallet = new ethers_1.Wallet(key);
276
331
  return new PrivateKeySigner(wallet);
277
332
  }
278
333
  case 'turnkey': {
279
- const stamper = new api_key_stamper_1.ApiKeyStamper({
334
+ // Dynamic import to avoid mandatory dependency on @turnkey packages
335
+ const { TurnkeySigner } = await Promise.resolve().then(() => __importStar(require("@turnkey/ethers")));
336
+ const { ApiKeyStamper } = await Promise.resolve().then(() => __importStar(require("@turnkey/api-key-stamper")));
337
+ const stamper = new ApiKeyStamper({
280
338
  apiPublicKey: config.apiPublicKey,
281
339
  apiPrivateKey: config.apiPrivateKey,
282
340
  });
283
- const turnkeySigner = new ethers_2.TurnkeySigner({
341
+ const turnkeySigner = new TurnkeySigner({
284
342
  baseUrl: config.apiBaseUrl,
285
343
  stamper,
286
344
  organizationId: config.organizationId,
@@ -289,7 +347,7 @@ async function createSigner(config) {
289
347
  return new TurnkeySignerWrapper(turnkeySigner);
290
348
  }
291
349
  case 'privy': {
292
- throw new errors_1.TTTSignerError("[Signer] Privy wallet signer extraction not fully implemented", "Privy requires proper session context and walletId", "Refer to Privy server-auth documentation for server-side signing.");
350
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_PRIVY_NOT_IMPLEMENTED, "[Signer] Privy signer is not yet implemented. Use 'privateKey' or 'turnkey' instead.", "Privy embedded wallet support is planned but not available in this release.", "Use { type: 'privateKey', key: '0x...' } or { type: 'turnkey', ... } as your signer config.");
293
351
  }
294
352
  case 'kms': {
295
353
  if (config.provider === 'aws') {
@@ -301,12 +359,12 @@ async function createSigner(config) {
301
359
  return new KMSSigner(awsSigner);
302
360
  }
303
361
  catch (e) {
304
- throw new errors_1.TTTSignerError("[Signer] AWS KMS initialization failed", e.message, "Ensure @aws-sdk/client-kms is installed and credentials are configured.");
362
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_KMS_AWS_INIT_FAILED, "[Signer] AWS KMS initialization failed", e.message, "Ensure @aws-sdk/client-kms is installed and credentials are configured.");
305
363
  }
306
364
  }
307
365
  else if (config.provider === 'gcp') {
308
366
  if (!config.projectId || !config.locationId || !config.keyRingId || !config.keyVersionId) {
309
- throw new errors_1.TTTSignerError("[Signer] GCP KMS missing required fields", `projectId=${config.projectId}, locationId=${config.locationId}, keyRingId=${config.keyRingId}, keyVersionId=${config.keyVersionId}`, "Provide all required GCP KMS fields: projectId, locationId, keyRingId, keyVersionId.");
367
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_KMS_GCP_MISSING_FIELDS, "[Signer] GCP KMS missing required fields", `projectId=${config.projectId}, locationId=${config.locationId}, keyRingId=${config.keyRingId}, keyVersionId=${config.keyVersionId}`, "Provide all required GCP KMS fields: projectId, locationId, keyRingId, keyVersionId.");
310
368
  }
311
369
  try {
312
370
  // @ts-ignore
@@ -317,13 +375,13 @@ async function createSigner(config) {
317
375
  return new KMSSigner(gcpSigner);
318
376
  }
319
377
  catch (e) {
320
- throw new errors_1.TTTSignerError("[Signer] GCP KMS initialization failed", e.message, "Ensure @google-cloud/kms is installed and application default credentials are set.");
378
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_KMS_GCP_INIT_FAILED, "[Signer] GCP KMS initialization failed", e.message, "Ensure @google-cloud/kms is installed and application default credentials are set.");
321
379
  }
322
380
  }
323
- throw new errors_1.TTTSignerError("[Signer] Unsupported KMS provider", `Provider: ${config.provider}`, "Use 'aws' or 'gcp'.");
381
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_KMS_UNSUPPORTED_PROVIDER, "[Signer] Unsupported KMS provider", `Provider: ${config.provider}`, "Use 'aws' or 'gcp'.");
324
382
  }
325
383
  default:
326
384
  // @ts-ignore
327
- throw new errors_1.TTTSignerError(`[Signer] Unsupported signer type`, `Type: ${config.type}`, "Provide a supported signer type: privateKey, turnkey, privy, kms.");
385
+ throw new errors_1.TTTSignerError(errors_1.ERROR_CODES.SIGNER_UNSUPPORTED_TYPE, `[Signer] Unsupported signer type`, `Type: ${config.type}`, "Provide a supported signer type: privateKey, turnkey, privy, kms.");
328
386
  }
329
387
  }
@@ -3,6 +3,7 @@ import { TimeReading, SynthesizedTime, ProofOfTime } from "./types";
3
3
  export interface TimeSource {
4
4
  name: string;
5
5
  getTime(): Promise<TimeReading>;
6
+ close?(): void;
6
7
  }
7
8
  export declare class NTPSource implements TimeSource {
8
9
  name: string;
@@ -11,6 +12,28 @@ export declare class NTPSource implements TimeSource {
11
12
  constructor(name: string, host: string, port?: number);
12
13
  getTime(): Promise<TimeReading>;
13
14
  }
15
+ /**
16
+ * HTTPS-based time source (TLS-protected, immune to MITM).
17
+ * Uses HTTP Date header from trusted TLS endpoints.
18
+ * Preferred over plaintext NTP (UDP) which is vulnerable to spoofing.
19
+ *
20
+ * SECURITY MODEL — HTTPS Time Sources:
21
+ * - All HTTPS requests use Node.js `https.request()` with default TLS settings.
22
+ * - Default TLS behavior: certificate verification is ON (rejectUnauthorized=true).
23
+ * - No certificate bypass (rejectUnauthorized: false) is used anywhere in this module.
24
+ * - The TLS handshake itself provides authentication of the time server identity,
25
+ * preventing MITM attacks that plaintext NTP (UDP port 123) is vulnerable to.
26
+ * - Base uncertainty for HTTPS Date header is 500ms (HTTP Date has 1-second resolution).
27
+ * - For ±10ns precision, HTTPS is a cross-check only; KTSat is the primary source.
28
+ */
29
+ export declare class HTTPSTimeSource implements TimeSource {
30
+ name: string;
31
+ private url;
32
+ private activeRequests;
33
+ constructor(name: string, url: string);
34
+ getTime(): Promise<TimeReading>;
35
+ close(): void;
36
+ }
14
37
  export declare class TimeSynthesis {
15
38
  private sources;
16
39
  private usedNonces;
@@ -30,6 +53,16 @@ export declare class TimeSynthesis {
30
53
  * Fix 2: Checks expiration and nonce replay.
31
54
  * Fix 3: Uses sourceReadings (renamed from signatures).
32
55
  */
56
+ /**
57
+ * Determine PoT verification tolerance based on the lowest stratum
58
+ * observed across source readings. Lower stratum = more precise source
59
+ * = tighter tolerance allowed.
60
+ *
61
+ * Stratum 1: 10ms (10_000_000ns) - atomic clock direct
62
+ * Stratum 2: 25ms (25_000_000ns) - NTP server synced to stratum 1
63
+ * Stratum 3+: 50ms (50_000_000ns) - downstream NTP
64
+ */
65
+ private static getToleranceForStratum;
33
66
  verifyProofOfTime(pot: ProofOfTime): boolean;
34
67
  /**
35
68
  * Generates a bytes32 hash of the PoT for on-chain submission.
@@ -51,5 +84,10 @@ export declare class TimeSynthesis {
51
84
  /**
52
85
  * Deserializes PoT from compact binary format.
53
86
  */
87
+ /**
88
+ * Closes all sources and clears internal state.
89
+ * Call this in tests (afterEach/afterAll) to prevent open handle warnings.
90
+ */
91
+ close(): void;
54
92
  static deserializeFromBinary(buf: Buffer): ProofOfTime;
55
93
  }
@@ -33,9 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.TimeSynthesis = exports.NTPSource = void 0;
36
+ exports.TimeSynthesis = exports.HTTPSTimeSource = exports.NTPSource = void 0;
37
37
  const crypto = __importStar(require("crypto"));
38
38
  const dgram = __importStar(require("dgram"));
39
+ const https = __importStar(require("https"));
39
40
  const buffer_1 = require("buffer");
40
41
  const ethers_1 = require("ethers");
41
42
  const logger_1 = require("./logger");
@@ -131,30 +132,106 @@ class NTPSource {
131
132
  }
132
133
  }
133
134
  exports.NTPSource = NTPSource;
135
+ /**
136
+ * HTTPS-based time source (TLS-protected, immune to MITM).
137
+ * Uses HTTP Date header from trusted TLS endpoints.
138
+ * Preferred over plaintext NTP (UDP) which is vulnerable to spoofing.
139
+ *
140
+ * SECURITY MODEL — HTTPS Time Sources:
141
+ * - All HTTPS requests use Node.js `https.request()` with default TLS settings.
142
+ * - Default TLS behavior: certificate verification is ON (rejectUnauthorized=true).
143
+ * - No certificate bypass (rejectUnauthorized: false) is used anywhere in this module.
144
+ * - The TLS handshake itself provides authentication of the time server identity,
145
+ * preventing MITM attacks that plaintext NTP (UDP port 123) is vulnerable to.
146
+ * - Base uncertainty for HTTPS Date header is 500ms (HTTP Date has 1-second resolution).
147
+ * - For ±10ns precision, HTTPS is a cross-check only; KTSat is the primary source.
148
+ */
149
+ class HTTPSTimeSource {
150
+ name;
151
+ url;
152
+ activeRequests = new Set();
153
+ constructor(name, url) {
154
+ this.name = name;
155
+ this.url = url;
156
+ }
157
+ async getTime() {
158
+ const t1 = BigInt(Date.now()) * 1000000n;
159
+ return new Promise((resolve, reject) => {
160
+ const timeout = setTimeout(() => {
161
+ reject(new errors_1.TTTTimeSynthesisError(`HTTPS time timeout for ${this.name}`, `${this.url} did not respond within 3000ms`, "Check network connectivity or try a different HTTPS time endpoint."));
162
+ }, 3000);
163
+ const req = https.request(this.url, { method: 'HEAD' }, (res) => {
164
+ clearTimeout(timeout);
165
+ // Consume and discard the response to free the socket
166
+ res.resume();
167
+ const t4 = BigInt(Date.now()) * 1000000n;
168
+ const dateHeader = res.headers['date'];
169
+ if (!dateHeader) {
170
+ this.activeRequests.delete(req);
171
+ reject(new errors_1.TTTTimeSynthesisError(`No Date header from ${this.name}`, "HTTPS response missing Date header", "The endpoint must return a Date header (RFC 7231)."));
172
+ return;
173
+ }
174
+ const serverTime = BigInt(new Date(dateHeader).getTime()) * 1000000n;
175
+ const rttNs = t4 - t1;
176
+ const rttMs = Number(rttNs) / 1_000_000;
177
+ // Offset-corrected: server time + half RTT
178
+ const corrected = serverTime + rttNs / 2n;
179
+ this.activeRequests.delete(req);
180
+ resolve({
181
+ timestamp: corrected,
182
+ uncertainty: rttMs / 2 + 500, // 500ms base uncertainty for HTTP Date (1s resolution)
183
+ stratum: 2, // HTTPS endpoints typically sync to stratum-1
184
+ source: this.name
185
+ });
186
+ });
187
+ req.on('error', (err) => {
188
+ clearTimeout(timeout);
189
+ this.activeRequests.delete(req);
190
+ reject(new errors_1.TTTTimeSynthesisError(`HTTPS time error for ${this.name}`, err.message, "Check TLS connectivity."));
191
+ });
192
+ this.activeRequests.add(req);
193
+ req.end();
194
+ });
195
+ }
196
+ close() {
197
+ for (const req of this.activeRequests) {
198
+ req.destroy();
199
+ }
200
+ this.activeRequests.clear();
201
+ }
202
+ }
203
+ exports.HTTPSTimeSource = HTTPSTimeSource;
134
204
  class TimeSynthesis {
135
205
  sources = [];
136
206
  // Fix 2: Bounded nonce replay cache (max 10K entries, 60s TTL) — same pattern as protocol_fee.ts
137
207
  usedNonces = new Map();
138
208
  MAX_NONCE_CACHE = 10000;
139
- NONCE_TTL_MS = 60000; // 60 seconds
209
+ NONCE_TTL_MS = 300_000; // 5 minutes — must exceed PoT expiresAt (60s) to prevent edge-case replay
140
210
  constructor(config) {
141
- const sourceNames = config?.sources || ['nist', 'kriss', 'google'];
211
+ const sourceNames = config?.sources || ['nist', 'google', 'cloudflare', 'apple'];
142
212
  for (const s of sourceNames) {
143
213
  if (s === 'nist') {
144
- this.sources.push(new NTPSource('nist', 'time.nist.gov'));
214
+ this.sources.push(new HTTPSTimeSource('nist', 'https://time.nist.gov/'));
215
+ }
216
+ else if (s === 'google' || s === 'galileo') {
217
+ this.sources.push(new HTTPSTimeSource('google', 'https://time.google.com/'));
218
+ }
219
+ else if (s === 'cloudflare') {
220
+ this.sources.push(new HTTPSTimeSource('cloudflare', 'https://time.cloudflare.com/'));
221
+ }
222
+ else if (s === 'apple') {
223
+ this.sources.push(new HTTPSTimeSource('apple', 'https://time.apple.com/'));
145
224
  }
146
225
  else if (s === 'kriss') {
226
+ // Legacy: KRISS NTP (plaintext UDP) — kept for backward compat, not in defaults
147
227
  this.sources.push(new NTPSource('kriss', 'time.kriss.re.kr'));
148
228
  }
149
- else if (s === 'google' || s === 'galileo') {
150
- this.sources.push(new NTPSource('google', 'time.google.com'));
151
- }
152
229
  }
153
230
  }
154
231
  async getFromSource(name) {
155
232
  const source = this.sources.find(s => s.name === name);
156
233
  if (!source)
157
- throw new errors_1.TTTTimeSynthesisError(`Source ${name} not found`, "Requested source name is not configured", "Configure the source in the TimeSynthesis constructor.");
234
+ throw new errors_1.TTTTimeSynthesisError(errors_1.ERROR_CODES.TIME_SYNTHESIS_SOURCE_NOT_FOUND, `Source ${name} not found`, "Requested source name is not configured", "Configure the source in the TimeSynthesis constructor.");
158
235
  return source.getTime();
159
236
  }
160
237
  async synthesize() {
@@ -169,7 +246,7 @@ class TimeSynthesis {
169
246
  }
170
247
  }
171
248
  if (readings.length === 0) {
172
- throw new errors_1.TTTTimeSynthesisError('[TimeSynthesis] CRITICAL: All NTP sources failed.', "Zero readings returned from all configured NTP sources", "Ensure UDP port 123 is open and NTP servers are reachable.");
249
+ throw new errors_1.TTTTimeSynthesisError(errors_1.ERROR_CODES.TIME_SYNTHESIS_ALL_SOURCES_FAILED, '[TimeSynthesis] CRITICAL: All NTP sources failed.', "Zero readings returned from all configured NTP sources", "Ensure UDP port 123 is open and NTP servers are reachable.");
173
250
  }
174
251
  if (readings.length === 1) {
175
252
  logger_1.logger.warn(`[TimeSynthesis] WARNING: Only 1 NTP source available (${readings[0].source}). Time may be unreliable.`);
@@ -216,7 +293,7 @@ class TimeSynthesis {
216
293
  }
217
294
  }
218
295
  if (readings.length === 0) {
219
- throw new errors_1.TTTTimeSynthesisError('[TimeSynthesis] Cannot generate PoT: All NTP sources failed.', "No successful readings from NTP servers.", "Check internet connection and UDP 123 access.");
296
+ throw new errors_1.TTTTimeSynthesisError(errors_1.ERROR_CODES.TIME_SYNTHESIS_POT_ALL_FAILED, '[TimeSynthesis] Cannot generate PoT: All NTP sources failed.', "No successful readings from NTP servers.", "Check internet connection and UDP 123 access.");
220
297
  }
221
298
  readings.sort((a, b) => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0));
222
299
  let finalTimestamp;
@@ -241,7 +318,8 @@ class TimeSynthesis {
241
318
  const sourceReadings = readings.map(r => ({
242
319
  source: r.source,
243
320
  timestamp: r.timestamp,
244
- uncertainty: r.uncertainty
321
+ uncertainty: r.uncertainty,
322
+ stratum: r.stratum
245
323
  }));
246
324
  // Fix 2: PoT nonce + expiration for replay protection
247
325
  const nonce = crypto.randomBytes(16).toString("hex");
@@ -258,7 +336,7 @@ class TimeSynthesis {
258
336
  };
259
337
  // Verification Logic: Ensure all source timestamps are within tolerance of synthesized median
260
338
  if (!this.verifyProofOfTime(pot)) {
261
- throw new errors_1.TTTTimeSynthesisError("[PoT] Self-verification failed", "Source readings are too far apart from synthesized median", "Check for high network jitter or malicious NTP spoofing.");
339
+ throw new errors_1.TTTTimeSynthesisError(errors_1.ERROR_CODES.TIME_SYNTHESIS_SELF_VERIFY_FAILED, "[PoT] Self-verification failed", "Source readings are too far apart from synthesized median", "Check for high network jitter or malicious NTP spoofing.");
262
340
  }
263
341
  return pot;
264
342
  }
@@ -267,18 +345,42 @@ class TimeSynthesis {
267
345
  * Fix 2: Checks expiration and nonce replay.
268
346
  * Fix 3: Uses sourceReadings (renamed from signatures).
269
347
  */
348
+ /**
349
+ * Determine PoT verification tolerance based on the lowest stratum
350
+ * observed across source readings. Lower stratum = more precise source
351
+ * = tighter tolerance allowed.
352
+ *
353
+ * Stratum 1: 10ms (10_000_000ns) - atomic clock direct
354
+ * Stratum 2: 25ms (25_000_000ns) - NTP server synced to stratum 1
355
+ * Stratum 3+: 50ms (50_000_000ns) - downstream NTP
356
+ */
357
+ static getToleranceForStratum(stratum) {
358
+ if (stratum <= 1)
359
+ return 10000000n;
360
+ if (stratum === 2)
361
+ return 25000000n;
362
+ return 50000000n;
363
+ }
270
364
  verifyProofOfTime(pot) {
271
- const TOLERANCE_NS = 100000000n; // 100ms
272
365
  if (pot.sourceReadings.length === 0)
273
366
  return false;
274
367
  if (pot.confidence <= 0)
275
368
  return false;
276
- // Fix 2: Expiration check
369
+ // Determine tolerance from the lowest stratum in sourceReadings.
370
+ // Fall back to pot.stratum if individual readings lack stratum info.
371
+ let lowestStratum = pot.stratum;
372
+ for (const reading of pot.sourceReadings) {
373
+ if (reading.stratum !== undefined && reading.stratum < lowestStratum) {
374
+ lowestStratum = reading.stratum;
375
+ }
376
+ }
377
+ const toleranceNs = TimeSynthesis.getToleranceForStratum(lowestStratum);
378
+ // Expiration check
277
379
  if (BigInt(Date.now()) > pot.expiresAt) {
278
380
  logger_1.logger.warn(`[TimeSynthesis] PoT expired at ${pot.expiresAt}`);
279
381
  return false;
280
382
  }
281
- // Fix 2: Nonce replay protection with bounded cache + TTL cleanup
383
+ // Nonce replay protection with bounded cache + TTL cleanup
282
384
  const now = Date.now();
283
385
  // TTL cleanup pass (evict expired entries)
284
386
  if (this.usedNonces.size > this.MAX_NONCE_CACHE / 2) {
@@ -300,8 +402,8 @@ class TimeSynthesis {
300
402
  this.usedNonces.set(pot.nonce, now);
301
403
  for (const sig of pot.sourceReadings) {
302
404
  const diff = sig.timestamp > pot.timestamp ? sig.timestamp - pot.timestamp : pot.timestamp - sig.timestamp;
303
- if (diff > TOLERANCE_NS) {
304
- logger_1.logger.warn(`[TimeSynthesis] Reading from ${sig.source} outside tolerance: ${diff}ns`);
405
+ if (diff > toleranceNs) {
406
+ logger_1.logger.warn(`[TimeSynthesis] Reading from ${sig.source} outside tolerance (${toleranceNs}ns, stratum ${lowestStratum}): ${diff}ns`);
305
407
  return false;
306
408
  }
307
409
  }
@@ -311,9 +413,9 @@ class TimeSynthesis {
311
413
  * Generates a bytes32 hash of the PoT for on-chain submission.
312
414
  */
313
415
  static getOnChainHash(pot) {
314
- return (0, ethers_1.keccak256)(ethers_1.AbiCoder.defaultAbiCoder().encode(["uint64", "uint32", "uint8", "uint8", "uint32"], [
315
- pot.timestamp / 1000000n, // Convert to ms for storage efficiency
316
- Math.round(pot.uncertainty * 1000), // Scale uncertainty
416
+ return (0, ethers_1.keccak256)(ethers_1.AbiCoder.defaultAbiCoder().encode(["uint64", "uint64", "uint8", "uint8", "uint32"], [
417
+ pot.timestamp, // Keep full nanosecond precision (uint64 max ~year 2554)
418
+ Math.round(pot.uncertainty * 1_000_000), // Scale uncertainty to ns
317
419
  pot.sources,
318
420
  pot.stratum,
319
421
  Math.round(pot.confidence * 1000000)
@@ -389,6 +491,18 @@ class TimeSynthesis {
389
491
  /**
390
492
  * Deserializes PoT from compact binary format.
391
493
  */
494
+ /**
495
+ * Closes all sources and clears internal state.
496
+ * Call this in tests (afterEach/afterAll) to prevent open handle warnings.
497
+ */
498
+ close() {
499
+ for (const source of this.sources) {
500
+ if (typeof source.close === 'function') {
501
+ source.close();
502
+ }
503
+ }
504
+ this.usedNonces.clear();
505
+ }
392
506
  static deserializeFromBinary(buf) {
393
507
  let offset = 0;
394
508
  const timestamp = buf.readBigUInt64BE(offset);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * TTT Labs Trust Store
3
+ * Manages trusted NTP sources and PoT verifier registry.
4
+ * TTT Labs operates as the trust store authority, similar to browser CA bundles.
5
+ */
6
+ export interface TrustedSource {
7
+ name: string;
8
+ endpoint: string;
9
+ stratum: number;
10
+ region: string;
11
+ active: boolean;
12
+ addedAt: number;
13
+ }
14
+ export interface TrustStoreConfig {
15
+ sources: TrustedSource[];
16
+ minSources: number;
17
+ maxStratumDrift: number;
18
+ }
19
+ export declare class TTTTrustStore {
20
+ private sources;
21
+ private minSources;
22
+ private maxStratumDrift;
23
+ constructor(config: TrustStoreConfig);
24
+ /**
25
+ * Get all registered trusted sources.
26
+ */
27
+ getSources(): TrustedSource[];
28
+ /**
29
+ * Add a new trusted source with validation.
30
+ */
31
+ addSource(source: TrustedSource): void;
32
+ /**
33
+ * Remove a source by name.
34
+ */
35
+ removeSource(name: string): boolean;
36
+ /**
37
+ * Validate that the active source quorum is met.
38
+ */
39
+ validateSourceQuorum(): {
40
+ valid: boolean;
41
+ activeSources: number;
42
+ required: number;
43
+ };
44
+ /**
45
+ * Filter sources by region.
46
+ */
47
+ getSourcesByRegion(region: string): TrustedSource[];
48
+ }
49
+ export declare const DEFAULT_TRUST_STORE: TrustStoreConfig;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * TTT Labs Trust Store
4
+ * Manages trusted NTP sources and PoT verifier registry.
5
+ * TTT Labs operates as the trust store authority, similar to browser CA bundles.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.DEFAULT_TRUST_STORE = exports.TTTTrustStore = void 0;
9
+ class TTTTrustStore {
10
+ sources = new Map();
11
+ minSources;
12
+ maxStratumDrift;
13
+ constructor(config) {
14
+ this.minSources = config.minSources;
15
+ this.maxStratumDrift = config.maxStratumDrift;
16
+ for (const source of config.sources) {
17
+ this.sources.set(source.name, source);
18
+ }
19
+ }
20
+ /**
21
+ * Get all registered trusted sources.
22
+ */
23
+ getSources() {
24
+ return Array.from(this.sources.values());
25
+ }
26
+ /**
27
+ * Add a new trusted source with validation.
28
+ */
29
+ addSource(source) {
30
+ if (!source.name || !source.endpoint || source.stratum < 1 || source.stratum > 15) {
31
+ throw new Error(`Invalid source configuration: ${source.name}`);
32
+ }
33
+ this.sources.set(source.name, { ...source, addedAt: Date.now() });
34
+ }
35
+ /**
36
+ * Remove a source by name.
37
+ */
38
+ removeSource(name) {
39
+ return this.sources.delete(name);
40
+ }
41
+ /**
42
+ * Validate that the active source quorum is met.
43
+ */
44
+ validateSourceQuorum() {
45
+ const activeSources = this.getSources().filter(s => s.active).length;
46
+ return {
47
+ valid: activeSources >= this.minSources,
48
+ activeSources,
49
+ required: this.minSources
50
+ };
51
+ }
52
+ /**
53
+ * Filter sources by region.
54
+ */
55
+ getSourcesByRegion(region) {
56
+ return this.getSources().filter(s => s.region === region);
57
+ }
58
+ }
59
+ exports.TTTTrustStore = TTTTrustStore;
60
+ exports.DEFAULT_TRUST_STORE = {
61
+ sources: [
62
+ {
63
+ name: "NIST",
64
+ endpoint: "time.nist.gov",
65
+ stratum: 1,
66
+ region: "US",
67
+ active: true,
68
+ addedAt: 1710412800000 // March 14, 2024
69
+ },
70
+ {
71
+ name: "Apple",
72
+ endpoint: "time.apple.com",
73
+ stratum: 1,
74
+ region: "US",
75
+ active: true,
76
+ addedAt: 1710412800000
77
+ },
78
+ {
79
+ name: "Google",
80
+ endpoint: "time.google.com",
81
+ stratum: 1,
82
+ region: "Global",
83
+ active: true,
84
+ addedAt: 1710412800000
85
+ }
86
+ ],
87
+ minSources: 2,
88
+ maxStratumDrift: 1
89
+ };
@@ -20,7 +20,7 @@ export declare class TTTBuilder {
20
20
  * Verify block data using the AdaptiveSwitch pipeline.
21
21
  * Updates the current mode based on verification results.
22
22
  */
23
- verifyBlock(blockData: Block, tttRecord: TTTRecord): Promise<AdaptiveMode>;
23
+ verifyBlock(blockData: Block, tttRecord: TTTRecord, chainId: number, poolAddress: string, tier?: string): Promise<AdaptiveMode>;
24
24
  /**
25
25
  * Return the current TURBO/FULL mode.
26
26
  */
@@ -61,9 +61,9 @@ class TTTBuilder {
61
61
  * Verify block data using the AdaptiveSwitch pipeline.
62
62
  * Updates the current mode based on verification results.
63
63
  */
64
- async verifyBlock(blockData, tttRecord) {
64
+ async verifyBlock(blockData, tttRecord, chainId, poolAddress, tier) {
65
65
  logger_1.logger.info(`[TTTBuilder] Verifying block at timestamp: ${blockData.timestamp}`);
66
- const result = this.adaptiveSwitch.verifyBlock(blockData, tttRecord);
66
+ const result = this.adaptiveSwitch.verifyBlock(blockData, tttRecord, chainId, poolAddress, tier);
67
67
  this.mode = result;
68
68
  logger_1.logger.info(`[TTTBuilder] Verification complete. Mode: ${this.mode}`);
69
69
  return this.mode;