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.
- package/README.md +4 -4
- package/dist/adaptive_switch.d.ts +22 -7
- package/dist/adaptive_switch.js +52 -15
- package/dist/auto_mint.d.ts +22 -7
- package/dist/auto_mint.js +107 -30
- package/dist/ct_log.d.ts +47 -0
- package/dist/ct_log.js +107 -0
- package/dist/dynamic_fee.d.ts +13 -2
- package/dist/dynamic_fee.js +62 -11
- package/dist/errors.d.ts +44 -25
- package/dist/errors.js +58 -42
- package/dist/evm_connector.d.ts +28 -1
- package/dist/evm_connector.js +124 -32
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -5
- package/dist/logger.d.ts +36 -4
- package/dist/logger.js +70 -11
- package/dist/networks.d.ts +21 -0
- package/dist/networks.js +30 -4
- package/dist/pool_registry.d.ts +9 -0
- package/dist/pool_registry.js +37 -0
- package/dist/pot_signer.d.ts +15 -0
- package/dist/pot_signer.js +28 -0
- package/dist/protocol_fee.d.ts +42 -26
- package/dist/protocol_fee.js +77 -54
- package/dist/revenue_tiers.d.ts +36 -0
- package/dist/revenue_tiers.js +83 -0
- package/dist/signer.d.ts +1 -2
- package/dist/signer.js +72 -14
- package/dist/time_synthesis.d.ts +38 -0
- package/dist/time_synthesis.js +134 -20
- package/dist/trust_store.d.ts +49 -0
- package/dist/trust_store.js +89 -0
- package/dist/ttt_builder.d.ts +1 -1
- package/dist/ttt_builder.js +2 -2
- package/dist/ttt_client.d.ts +24 -29
- package/dist/ttt_client.js +97 -28
- package/dist/types.d.ts +46 -3
- package/dist/v4_hook.d.ts +10 -2
- package/dist/v4_hook.js +10 -2
- package/dist/x402_enforcer.d.ts +17 -2
- package/dist/x402_enforcer.js +27 -2
- 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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/time_synthesis.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/time_synthesis.js
CHANGED
|
@@ -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 =
|
|
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', '
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 >
|
|
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", "
|
|
315
|
-
pot.timestamp
|
|
316
|
-
Math.round(pot.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
|
+
};
|
package/dist/ttt_builder.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/ttt_builder.js
CHANGED
|
@@ -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;
|