openttt 0.1.0 → 0.1.2

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 (37) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +12 -10
  3. package/dist/auto_mint.d.ts +1 -0
  4. package/dist/auto_mint.js +9 -0
  5. package/dist/grg_forward.d.ts +7 -2
  6. package/dist/grg_forward.js +21 -6
  7. package/dist/grg_inverse.d.ts +2 -2
  8. package/dist/grg_inverse.js +10 -7
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/pot_signer.d.ts +29 -0
  12. package/dist/pot_signer.js +73 -0
  13. package/dist/time_synthesis.d.ts +6 -0
  14. package/dist/time_synthesis.js +71 -14
  15. package/dist/types.d.ts +6 -1
  16. package/package.json +24 -5
  17. package/vendor/helm-crypto/golay.d.ts +6 -0
  18. package/vendor/helm-crypto/golay.js +167 -0
  19. package/vendor/helm-crypto/golay.js.map +1 -0
  20. package/vendor/helm-crypto/grg_forward.d.ts +22 -0
  21. package/vendor/helm-crypto/grg_forward.js +89 -0
  22. package/vendor/helm-crypto/grg_forward.js.map +1 -0
  23. package/vendor/helm-crypto/grg_inverse.d.ts +16 -0
  24. package/vendor/helm-crypto/grg_inverse.js +118 -0
  25. package/vendor/helm-crypto/grg_inverse.js.map +1 -0
  26. package/vendor/helm-crypto/grg_pipeline.d.ts +13 -0
  27. package/vendor/helm-crypto/grg_pipeline.js +66 -0
  28. package/vendor/helm-crypto/grg_pipeline.js.map +1 -0
  29. package/vendor/helm-crypto/index.d.ts +5 -0
  30. package/vendor/helm-crypto/index.js +17 -0
  31. package/vendor/helm-crypto/index.js.map +1 -0
  32. package/vendor/helm-crypto/logger.d.ts +6 -0
  33. package/vendor/helm-crypto/logger.js +11 -0
  34. package/vendor/helm-crypto/logger.js.map +1 -0
  35. package/vendor/helm-crypto/reed_solomon.d.ts +37 -0
  36. package/vendor/helm-crypto/reed_solomon.js +210 -0
  37. package/vendor/helm-crypto/reed_solomon.js.map +1 -0
package/LICENSE ADDED
@@ -0,0 +1,48 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: Helm Protocol Foundation
6
+ Licensed Work: TikitakaTime SDK v0.1.0 (OpenTTT)
7
+ Additional Use
8
+ Grant: Non-production evaluation and testing.
9
+ Change Date: March 14, 2030
10
+ Change License: Apache License, Version 2.0
11
+
12
+ 1. Grant of License. Licensor hereby grants you a world-wide, royalty-free,
13
+ non-exclusive, non-sublicensable, non-transferable license to use the
14
+ Licensed Work, subject to the conditions set forth in this License.
15
+
16
+ 2. Conditions. You may use the Licensed Work for any purpose, except for
17
+ Commercial Use.
18
+
19
+ 3. Commercial Use. Commercial Use means any use of the Licensed Work that is
20
+ intended for or directed toward commercial advantage or monetary
21
+ compensation.
22
+
23
+ 4. Change. On the Change Date, this License shall terminate and the Licensor
24
+ hereby grants you a license to the Licensed Work under the Change License.
25
+
26
+ 5. Intellectual Property. This License does not grant you any rights in the
27
+ Intellectual Property of the Licensor, except for the license to use the
28
+ Licensed Work as set forth in Section 1.
29
+
30
+ 6. Termination. This License shall terminate automatically if you fail to
31
+ comply with any of its terms and conditions.
32
+
33
+ 7. Disclaimer of Warranty. THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT
34
+ WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
35
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
37
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
38
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
39
+ LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
40
+
41
+ 8. Limitation of Liability. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY
42
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
44
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
45
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
46
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
47
+ OUT OF THE USE OF THE LICENSED WORK, EVEN IF ADVISED OF THE POSSIBILITY OF
48
+ SUCH DAMAGE.
package/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  # OpenTTT
2
2
 
3
+ > **Reference implementation of [draft-helmprotocol-tttps-00](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/)**
4
+
3
5
  **OpenSSL for Transaction Ordering** -- TLS-grade Proof of Time for DeFi.
4
6
 
5
7
  OpenTTT brings cryptographic time verification to blockchain transaction ordering. Where TLS made HTTP trustworthy, OpenTTT makes transaction sequencing verifiable. No trust assumptions. No gentleman's agreements. Physics.
6
8
 
7
9
  [![npm](https://img.shields.io/npm/v/openttt)](https://www.npmjs.com/package/openttt)
8
10
  [![License: BSL-1.1](https://img.shields.io/badge/License-BSL--1.1-blue.svg)](LICENSE)
9
- [![Tests](https://img.shields.io/badge/tests-104%20passing%20%C2%B7%2018%20suites-brightgreen)]()
11
+ [![CI](https://github.com/Helm-Protocol/OpenTTT/actions/workflows/ci.yml/badge.svg)](https://github.com/Helm-Protocol/OpenTTT/actions/workflows/ci.yml)
12
+ [![codecov](https://codecov.io/gh/Helm-Protocol/OpenTTT/branch/main/graph/badge.svg)](https://codecov.io/gh/Helm-Protocol/OpenTTT)
13
+ [![Tests](https://img.shields.io/badge/tests-273%20passing%20%C2%B7%2029%20suites-brightgreen)]()
10
14
 
11
15
  ```
12
16
  npm install openttt
@@ -41,14 +45,14 @@ Three lines to start minting TimeTokens:
41
45
  ```typescript
42
46
  import { TTTClient } from "openttt";
43
47
 
44
- const ttt = await TTTClient.forBase({
45
- signer: { type: "privateKey", envVar: "OPERATOR_PK" },
46
- });
48
+ const ttt = await TTTClient.forBase({ privateKey: process.env.OPERATOR_PK! });
47
49
  ttt.startAutoMint();
48
50
  ```
49
51
 
50
52
  That is it. The SDK connects to Base Mainnet, synthesizes time from three atomic clock sources, and begins minting Proof-of-Time tokens at your configured tier interval.
51
53
 
54
+ > **Shorthand**: Pass `privateKey` directly as a string instead of the full `signer` config object. The verbose form `{ signer: { type: "privateKey", key: "0x..." } }` still works for when you need other signer types (Turnkey, KMS, Privy).
55
+
52
56
  ---
53
57
 
54
58
  ## Progressive Disclosure
@@ -60,9 +64,7 @@ OpenTTT is designed around progressive disclosure. Start simple, add control as
60
64
  ```typescript
61
65
  import { TTTClient } from "openttt";
62
66
 
63
- const ttt = await TTTClient.forBase({
64
- signer: { type: "privateKey", envVar: "OPERATOR_PK" },
65
- });
67
+ const ttt = await TTTClient.forBase({ privateKey: process.env.OPERATOR_PK! });
66
68
  ttt.startAutoMint();
67
69
  ```
68
70
 
@@ -70,7 +72,7 @@ ttt.startAutoMint();
70
72
 
71
73
  ```typescript
72
74
  const ttt = await TTTClient.forSepolia({
73
- signer: { type: "privateKey", key: process.env.OPERATOR_PK! },
75
+ privateKey: process.env.OPERATOR_PK!,
74
76
  rpcUrl: "https://my-rpc.example.com",
75
77
  tier: "T2_slot",
76
78
  });
@@ -111,7 +113,7 @@ OpenTTT abstracts away signer complexity. Use a raw private key for development,
111
113
  |---|---|---|
112
114
  | `privateKey` | Development, small operators | `{ type: "privateKey", key: "0x..." }` or `{ type: "privateKey", envVar: "OPERATOR_PK" }` |
113
115
  | `turnkey` | Production, TEE-backed institutional custody | `{ type: "turnkey", apiBaseUrl, organizationId, privateKeyId, apiPublicKey, apiPrivateKey }` |
114
- | `privy` | Embedded wallets, consumer-facing apps | `{ type: "privy", appId, appSecret }` |
116
+ | `privy` | Embedded wallets, consumer-facing apps (coming soon) | `{ type: "privy", appId, appSecret }` |
115
117
  | `kms` | Cloud HSM (AWS KMS or GCP Cloud KMS) | `{ type: "kms", provider: "aws"\|"gcp", keyId, ... }` |
116
118
 
117
119
  **AWS KMS** requires `@aws-sdk/client-kms`. **GCP KMS** requires `@google-cloud/kms`. Both are optional peer dependencies -- install only what you use.
@@ -312,7 +314,7 @@ OpenTTT queries multiple atomic clock-synchronized NTP sources in parallel and p
312
314
  - **KRISS** (time.kriss.re.kr) -- Korean national standard
313
315
  - **Google** (time.google.com) -- Leap-smeared public NTP
314
316
 
315
- All readings must fall within 100ms tolerance of the synthesized median, or the Proof of Time is rejected. Single-source operation triggers a degraded-confidence warning.
317
+ All readings must fall within a stratum-dependent tolerance of the synthesized median (10ms for stratum 1, 25ms for stratum 2, 50ms for stratum 3+), or the Proof of Time is rejected. Single-source operation triggers a degraded-confidence warning.
316
318
 
317
319
  ---
318
320
 
@@ -19,6 +19,7 @@ export declare class AutoMintEngine {
19
19
  private cachedSigner;
20
20
  private consecutiveFailures;
21
21
  private maxConsecutiveFailures;
22
+ private potSigner;
22
23
  constructor(config: AutoMintConfig);
23
24
  getEvmConnector(): EVMConnector;
24
25
  setOnMint(callback: (result: MintResult) => void): void;
package/dist/auto_mint.js CHANGED
@@ -7,6 +7,7 @@ const time_synthesis_1 = require("./time_synthesis");
7
7
  const dynamic_fee_1 = require("./dynamic_fee");
8
8
  const evm_connector_1 = require("./evm_connector");
9
9
  const protocol_fee_1 = require("./protocol_fee");
10
+ const pot_signer_1 = require("./pot_signer");
10
11
  const types_1 = require("./types");
11
12
  const logger_1 = require("./logger");
12
13
  const errors_1 = require("./errors");
@@ -29,6 +30,7 @@ class AutoMintEngine {
29
30
  cachedSigner = null;
30
31
  consecutiveFailures = 0;
31
32
  maxConsecutiveFailures = 5;
33
+ potSigner = null;
32
34
  constructor(config) {
33
35
  this.config = config;
34
36
  this.timeSynthesis = new time_synthesis_1.TimeSynthesis({ sources: config.timeSources });
@@ -40,6 +42,8 @@ class AutoMintEngine {
40
42
  if (config.signer) {
41
43
  this.cachedSigner = config.signer;
42
44
  }
45
+ // Initialize Ed25519 PoT signer for non-repudiation
46
+ this.potSigner = new pot_signer_1.PotSigner();
43
47
  }
44
48
  getEvmConnector() {
45
49
  return this.evmConnector;
@@ -162,6 +166,11 @@ class AutoMintEngine {
162
166
  throw new errors_1.TTTTimeSynthesisError(`[PoT] Insufficient confidence`, `Calculated confidence ${pot.confidence} is below required 0.5`, `Ensure more NTP sources are reachable or decrease uncertainty.`);
163
167
  }
164
168
  const potHash = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(JSON.stringify(pot, (key, value) => typeof value === 'bigint' ? value.toString() : value)));
169
+ // 1-2. Ed25519 issuer signature for non-repudiation
170
+ if (this.potSigner) {
171
+ pot.issuerSignature = this.potSigner.signPot(potHash);
172
+ logger_1.logger.info(`[AutoMint] PoT signed by issuer ${this.potSigner.getPubKeyHex().substring(0, 16)}...`);
173
+ }
165
174
  // 2. tokenId 생성 (keccak256)
166
175
  // chainId, poolAddress, timestamp 기반 유니크 ID
167
176
  const tokenId = ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "address", "uint64"], [BigInt(this.config.chainId), this.config.poolAddress, synthesized.timestamp]));
@@ -1,6 +1,11 @@
1
1
  export declare class GrgForward {
2
2
  static golombEncode(data: Uint8Array, m?: number): Uint8Array;
3
3
  static redstuffEncode(data: Uint8Array, shards?: number, parity?: number): Uint8Array[];
4
- static golayEncodeWrapper(data: Uint8Array): Uint8Array;
5
- static encode(data: Uint8Array): Uint8Array[];
4
+ /**
5
+ * Derives an HMAC key from GRG payload context (chainId + poolAddress).
6
+ * Falls back to a static domain-separation key when no context is provided.
7
+ */
8
+ static deriveHmacKey(chainId?: number, poolAddress?: string): Buffer;
9
+ static golayEncodeWrapper(data: Uint8Array, hmacKey?: Buffer): Uint8Array;
10
+ static encode(data: Uint8Array, chainId?: number, poolAddress?: string): Uint8Array[];
6
11
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GrgForward = void 0;
4
4
  const crypto_1 = require("crypto");
5
+ const ethers_1 = require("ethers");
5
6
  const golay_1 = require("./golay");
6
7
  const reed_solomon_1 = require("./reed_solomon");
7
8
  class GrgForward {
@@ -28,18 +29,31 @@ class GrgForward {
28
29
  static redstuffEncode(data, shards = 4, parity = 2) {
29
30
  return reed_solomon_1.ReedSolomon.encode(data, shards, parity);
30
31
  }
32
+ /**
33
+ * Derives an HMAC key from GRG payload context (chainId + poolAddress).
34
+ * Falls back to a static domain-separation key when no context is provided.
35
+ */
36
+ static deriveHmacKey(chainId, poolAddress) {
37
+ if (chainId !== undefined && poolAddress) {
38
+ const packed = (0, ethers_1.keccak256)(ethers_1.AbiCoder.defaultAbiCoder().encode(["uint256", "address"], [chainId, poolAddress]));
39
+ return Buffer.from(packed.slice(2), "hex"); // 32 bytes
40
+ }
41
+ // Default domain-separation key when no context is available
42
+ return Buffer.from("grg-integrity-hmac-default-key-v1");
43
+ }
31
44
  // 3. Golay(24,12) Error Correction Encoding
32
- static golayEncodeWrapper(data) {
45
+ static golayEncodeWrapper(data, hmacKey) {
33
46
  const encoded = (0, golay_1.golayEncode)(data);
34
- // 🔱 Integrity: Append 8-byte SHA-256 hash of the encoded shard (B1-5: 4 -> 8 bytes)
35
- const hash = (0, crypto_1.createHash)("sha256").update(Buffer.from(encoded)).digest();
36
- const checksum = hash.subarray(0, 8);
47
+ // 🔱 Integrity: Append 8-byte HMAC-SHA256 of the encoded shard (keyed hash)
48
+ const key = hmacKey || this.deriveHmacKey();
49
+ const mac = (0, crypto_1.createHmac)("sha256", key).update(Buffer.from(encoded)).digest();
50
+ const checksum = mac.subarray(0, 8);
37
51
  const final = new Uint8Array(encoded.length + 8);
38
52
  final.set(encoded);
39
53
  final.set(checksum, encoded.length);
40
54
  return final;
41
55
  }
42
- static encode(data) {
56
+ static encode(data, chainId, poolAddress) {
43
57
  // R3-P0-3: Reject empty input — roundtrip breaks ([] → [0])
44
58
  if (data.length === 0) {
45
59
  throw new Error("[GRG] Cannot encode empty input — roundtrip identity violation");
@@ -53,7 +67,8 @@ class GrgForward {
53
67
  withLen[3] = data.length & 0xFF;
54
68
  withLen.set(compressed, 4);
55
69
  const shards = this.redstuffEncode(withLen);
56
- return shards.map(s => this.golayEncodeWrapper(s));
70
+ const hmacKey = this.deriveHmacKey(chainId, poolAddress);
71
+ return shards.map(s => this.golayEncodeWrapper(s, hmacKey));
57
72
  }
58
73
  }
59
74
  exports.GrgForward = GrgForward;
@@ -1,7 +1,7 @@
1
1
  export declare class GrgInverse {
2
- static golayDecodeWrapper(data: Uint8Array): Uint8Array;
2
+ static golayDecodeWrapper(data: Uint8Array, hmacKey?: Buffer): Uint8Array;
3
3
  static redstuffDecode(shards: (Uint8Array | null)[], dataShardCount?: number, parityShardCount?: number): Uint8Array;
4
4
  private static readonly MAX_GOLOMB_Q;
5
5
  static golombDecode(data: Uint8Array, m?: number): Uint8Array;
6
- static verify(data: Uint8Array, originalShards: Uint8Array[]): boolean;
6
+ static verify(data: Uint8Array, originalShards: Uint8Array[], chainId?: number, poolAddress?: string): boolean;
7
7
  }
@@ -3,21 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GrgInverse = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const golay_1 = require("./golay");
6
+ const grg_forward_1 = require("./grg_forward");
6
7
  const logger_1 = require("./logger");
7
8
  const reed_solomon_1 = require("./reed_solomon");
8
9
  class GrgInverse {
9
10
  // 1. Golay Decoding & Integrity Check 🔱
10
- static golayDecodeWrapper(data) {
11
+ static golayDecodeWrapper(data, hmacKey) {
11
12
  if (data.length < 8)
12
13
  throw new Error("GRG shard too short for checksum");
13
14
  // Split data and checksum (last 8 bytes)
14
15
  const encoded = data.subarray(0, data.length - 8);
15
16
  const checksum = data.subarray(data.length - 8);
16
- // Verify SHA-256 Checksum (B1-5: 4 -> 8 bytes)
17
- const hash = (0, crypto_1.createHash)("sha256").update(Buffer.from(encoded)).digest();
18
- const expected = hash.subarray(0, 8);
17
+ // Verify HMAC-SHA256 Checksum (keyed hash, B1-5: 8 bytes truncated)
18
+ const key = hmacKey || grg_forward_1.GrgForward.deriveHmacKey();
19
+ const mac = (0, crypto_1.createHmac)("sha256", key).update(Buffer.from(encoded)).digest();
20
+ const expected = mac.subarray(0, 8);
19
21
  if (!Buffer.from(checksum).equals(Buffer.from(expected))) {
20
- throw new Error("GRG tamper detected: SHA-256 checksum mismatch");
22
+ throw new Error("GRG tamper detected: HMAC-SHA256 checksum mismatch");
21
23
  }
22
24
  // Proceed to Golay decode
23
25
  const res = (0, golay_1.golayDecode)(encoded);
@@ -62,11 +64,12 @@ class GrgInverse {
62
64
  }
63
65
  return new Uint8Array(result);
64
66
  }
65
- static verify(data, originalShards) {
67
+ static verify(data, originalShards, chainId, poolAddress) {
66
68
  try {
69
+ const hmacKey = grg_forward_1.GrgForward.deriveHmacKey(chainId, poolAddress);
67
70
  const decodedShards = originalShards.map(s => {
68
71
  try {
69
- return this.golayDecodeWrapper(s);
72
+ return this.golayDecodeWrapper(s, hmacKey);
70
73
  }
71
74
  catch {
72
75
  return null;
package/dist/index.d.ts CHANGED
@@ -19,3 +19,4 @@ export * from "./signer";
19
19
  export * from "./networks";
20
20
  export * from "./reed_solomon";
21
21
  export * from "./errors";
22
+ export * from "./pot_signer";
package/dist/index.js CHANGED
@@ -36,3 +36,4 @@ __exportStar(require("./signer"), exports);
36
36
  __exportStar(require("./networks"), exports);
37
37
  __exportStar(require("./reed_solomon"), exports);
38
38
  __exportStar(require("./errors"), exports);
39
+ __exportStar(require("./pot_signer"), exports);
@@ -0,0 +1,29 @@
1
+ export interface PotSignature {
2
+ issuerPubKey: string;
3
+ signature: string;
4
+ issuedAt: bigint;
5
+ }
6
+ export declare class PotSigner {
7
+ private privateKey;
8
+ private publicKey;
9
+ private pubKeyHex;
10
+ constructor(privateKeyHex?: string);
11
+ /** Returns the hex-encoded SPKI DER public key */
12
+ getPubKeyHex(): string;
13
+ /** Returns the hex-encoded PKCS8 DER private key (for persistence) */
14
+ getPrivateKeyHex(): string;
15
+ /**
16
+ * Sign a PoT hash with Ed25519.
17
+ * @param potHash - hex string (with or without 0x prefix) of the PoT hash
18
+ * @returns PotSignature with issuerPubKey, signature, and issuedAt
19
+ */
20
+ signPot(potHash: string): PotSignature;
21
+ /**
22
+ * Verify a PotSignature against a PoT hash.
23
+ * @param potHash - hex string (with or without 0x prefix)
24
+ * @param potSig - the PotSignature to verify
25
+ * @param expectedPubKey - optional: reject if issuerPubKey doesn't match
26
+ * @returns true if signature is valid
27
+ */
28
+ static verifyPotSignature(potHash: string, potSig: PotSignature, expectedPubKey?: string): boolean;
29
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PotSigner = void 0;
4
+ // sdk/src/pot_signer.ts — Ed25519 signing for Proof of Time (Non-repudiation)
5
+ // Uses Node.js built-in crypto.sign/verify with Ed25519
6
+ const crypto_1 = require("crypto");
7
+ class PotSigner {
8
+ privateKey;
9
+ publicKey;
10
+ pubKeyHex;
11
+ constructor(privateKeyHex) {
12
+ if (privateKeyHex) {
13
+ // Import existing key from PKCS8 DER hex
14
+ const keyBuffer = Buffer.from(privateKeyHex, 'hex');
15
+ this.privateKey = (0, crypto_1.createPrivateKey)({ key: keyBuffer, format: 'der', type: 'pkcs8' });
16
+ this.publicKey = (0, crypto_1.createPublicKey)(this.privateKey);
17
+ }
18
+ else {
19
+ // Generate new Ed25519 keypair
20
+ const { privateKey, publicKey } = (0, crypto_1.generateKeyPairSync)('ed25519');
21
+ this.privateKey = privateKey;
22
+ this.publicKey = publicKey;
23
+ }
24
+ this.pubKeyHex = this.publicKey.export({ type: 'spki', format: 'der' }).toString('hex');
25
+ }
26
+ /** Returns the hex-encoded SPKI DER public key */
27
+ getPubKeyHex() {
28
+ return this.pubKeyHex;
29
+ }
30
+ /** Returns the hex-encoded PKCS8 DER private key (for persistence) */
31
+ getPrivateKeyHex() {
32
+ return this.privateKey.export({ type: 'pkcs8', format: 'der' }).toString('hex');
33
+ }
34
+ /**
35
+ * Sign a PoT hash with Ed25519.
36
+ * @param potHash - hex string (with or without 0x prefix) of the PoT hash
37
+ * @returns PotSignature with issuerPubKey, signature, and issuedAt
38
+ */
39
+ signPot(potHash) {
40
+ const data = Buffer.from(potHash.startsWith('0x') ? potHash.slice(2) : potHash, 'hex');
41
+ const sig = (0, crypto_1.sign)(null, data, this.privateKey);
42
+ return {
43
+ issuerPubKey: this.pubKeyHex,
44
+ signature: sig.toString('hex'),
45
+ issuedAt: BigInt(Math.floor(Date.now() / 1000)),
46
+ };
47
+ }
48
+ /**
49
+ * Verify a PotSignature against a PoT hash.
50
+ * @param potHash - hex string (with or without 0x prefix)
51
+ * @param potSig - the PotSignature to verify
52
+ * @param expectedPubKey - optional: reject if issuerPubKey doesn't match
53
+ * @returns true if signature is valid
54
+ */
55
+ static verifyPotSignature(potHash, potSig, expectedPubKey) {
56
+ if (expectedPubKey && potSig.issuerPubKey !== expectedPubKey)
57
+ return false;
58
+ try {
59
+ const data = Buffer.from(potHash.startsWith('0x') ? potHash.slice(2) : potHash, 'hex');
60
+ const sigBuffer = Buffer.from(potSig.signature, 'hex');
61
+ const pubKey = (0, crypto_1.createPublicKey)({
62
+ key: Buffer.from(potSig.issuerPubKey, 'hex'),
63
+ format: 'der',
64
+ type: 'spki',
65
+ });
66
+ return (0, crypto_1.verify)(null, data, pubKey, sigBuffer);
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ }
73
+ exports.PotSigner = PotSigner;
@@ -13,6 +13,9 @@ export declare class NTPSource implements TimeSource {
13
13
  }
14
14
  export declare class TimeSynthesis {
15
15
  private sources;
16
+ private usedNonces;
17
+ private readonly MAX_NONCE_CACHE;
18
+ private readonly NONCE_TTL_MS;
16
19
  constructor(config?: {
17
20
  sources?: string[];
18
21
  });
@@ -24,6 +27,8 @@ export declare class TimeSynthesis {
24
27
  generateProofOfTime(): Promise<ProofOfTime>;
25
28
  /**
26
29
  * Verify Proof of Time integrity.
30
+ * Fix 2: Checks expiration and nonce replay.
31
+ * Fix 3: Uses sourceReadings (renamed from signatures).
27
32
  */
28
33
  verifyProofOfTime(pot: ProofOfTime): boolean;
29
34
  /**
@@ -40,6 +45,7 @@ export declare class TimeSynthesis {
40
45
  static deserializeFromJSON(json: string): ProofOfTime;
41
46
  /**
42
47
  * Serializes PoT to compact binary format.
48
+ * Layout: header(19) + nonce(1+N) + expiresAt(8) + readings(variable)
43
49
  */
44
50
  static serializeToBinary(pot: ProofOfTime): Buffer;
45
51
  /**
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.TimeSynthesis = exports.NTPSource = void 0;
37
+ const crypto = __importStar(require("crypto"));
37
38
  const dgram = __importStar(require("dgram"));
38
39
  const buffer_1 = require("buffer");
39
40
  const ethers_1 = require("ethers");
@@ -132,6 +133,10 @@ class NTPSource {
132
133
  exports.NTPSource = NTPSource;
133
134
  class TimeSynthesis {
134
135
  sources = [];
136
+ // Fix 2: Bounded nonce replay cache (max 10K entries, 60s TTL) — same pattern as protocol_fee.ts
137
+ usedNonces = new Map();
138
+ MAX_NONCE_CACHE = 10000;
139
+ NONCE_TTL_MS = 60000; // 60 seconds
135
140
  constructor(config) {
136
141
  const sourceNames = config?.sources || ['nist', 'kriss', 'google'];
137
142
  for (const s of sourceNames) {
@@ -233,18 +238,23 @@ class TimeSynthesis {
233
238
  finalUncertainty = readings[mid].uncertainty;
234
239
  finalStratum = readings[mid].stratum;
235
240
  }
236
- const signatures = readings.map(r => ({
241
+ const sourceReadings = readings.map(r => ({
237
242
  source: r.source,
238
243
  timestamp: r.timestamp,
239
244
  uncertainty: r.uncertainty
240
245
  }));
246
+ // Fix 2: PoT nonce + expiration for replay protection
247
+ const nonce = crypto.randomBytes(16).toString("hex");
248
+ const expiresAt = BigInt(Date.now()) + 60000n; // +60 seconds
241
249
  const pot = {
242
250
  timestamp: finalTimestamp,
243
251
  uncertainty: finalUncertainty,
244
252
  sources: readings.length,
245
253
  stratum: finalStratum,
246
254
  confidence: readings.length / this.sources.length,
247
- signatures
255
+ sourceReadings,
256
+ nonce,
257
+ expiresAt,
248
258
  };
249
259
  // Verification Logic: Ensure all source timestamps are within tolerance of synthesized median
250
260
  if (!this.verifyProofOfTime(pot)) {
@@ -254,17 +264,44 @@ class TimeSynthesis {
254
264
  }
255
265
  /**
256
266
  * Verify Proof of Time integrity.
267
+ * Fix 2: Checks expiration and nonce replay.
268
+ * Fix 3: Uses sourceReadings (renamed from signatures).
257
269
  */
258
270
  verifyProofOfTime(pot) {
259
271
  const TOLERANCE_NS = 100000000n; // 100ms
260
- if (pot.signatures.length === 0)
272
+ if (pot.sourceReadings.length === 0)
261
273
  return false;
262
274
  if (pot.confidence <= 0)
263
275
  return false;
264
- for (const sig of pot.signatures) {
276
+ // Fix 2: Expiration check
277
+ if (BigInt(Date.now()) > pot.expiresAt) {
278
+ logger_1.logger.warn(`[TimeSynthesis] PoT expired at ${pot.expiresAt}`);
279
+ return false;
280
+ }
281
+ // Fix 2: Nonce replay protection with bounded cache + TTL cleanup
282
+ const now = Date.now();
283
+ // TTL cleanup pass (evict expired entries)
284
+ if (this.usedNonces.size > this.MAX_NONCE_CACHE / 2) {
285
+ for (const [k, ts] of this.usedNonces) {
286
+ if (now - ts > this.NONCE_TTL_MS)
287
+ this.usedNonces.delete(k);
288
+ }
289
+ }
290
+ if (this.usedNonces.has(pot.nonce)) {
291
+ logger_1.logger.warn(`[TimeSynthesis] Duplicate nonce detected: ${pot.nonce}`);
292
+ return false;
293
+ }
294
+ if (this.usedNonces.size >= this.MAX_NONCE_CACHE) {
295
+ // Evict oldest entry
296
+ const oldest = this.usedNonces.keys().next().value;
297
+ if (oldest !== undefined)
298
+ this.usedNonces.delete(oldest);
299
+ }
300
+ this.usedNonces.set(pot.nonce, now);
301
+ for (const sig of pot.sourceReadings) {
265
302
  const diff = sig.timestamp > pot.timestamp ? sig.timestamp - pot.timestamp : pot.timestamp - sig.timestamp;
266
303
  if (diff > TOLERANCE_NS) {
267
- logger_1.logger.warn(`[TimeSynthesis] Signature from ${sig.source} outside tolerance: ${diff}ns`);
304
+ logger_1.logger.warn(`[TimeSynthesis] Reading from ${sig.source} outside tolerance: ${diff}ns`);
268
305
  return false;
269
306
  }
270
307
  }
@@ -296,7 +333,8 @@ class TimeSynthesis {
296
333
  return {
297
334
  ...data,
298
335
  timestamp: BigInt(data.timestamp),
299
- signatures: data.signatures.map((s) => ({
336
+ expiresAt: BigInt(data.expiresAt),
337
+ sourceReadings: data.sourceReadings.map((s) => ({
300
338
  ...s,
301
339
  timestamp: BigInt(s.timestamp)
302
340
  }))
@@ -304,11 +342,14 @@ class TimeSynthesis {
304
342
  }
305
343
  /**
306
344
  * Serializes PoT to compact binary format.
345
+ * Layout: header(19) + nonce(1+N) + expiresAt(8) + readings(variable)
307
346
  */
308
347
  static serializeToBinary(pot) {
309
- // Header: timestamp(8), uncertainty(4), sources(1), stratum(1), confidence(4), sigCount(1) = 19 bytes
310
- let size = 19;
311
- for (const sig of pot.signatures) {
348
+ const nonceBytes = buffer_1.Buffer.from(pot.nonce, "utf8");
349
+ // Header: timestamp(8) + uncertainty(4) + sources(1) + stratum(1) + confidence(4) + readingCount(1) = 19
350
+ // + nonceLen(1) + nonce(N) + expiresAt(8)
351
+ let size = 19 + 1 + nonceBytes.length + 8;
352
+ for (const sig of pot.sourceReadings) {
312
353
  size += 1 + sig.source.length + 8 + 4; // nameLen(1) + name(N) + ts(8) + unc(4)
313
354
  }
314
355
  const buf = buffer_1.Buffer.alloc(size);
@@ -323,9 +364,17 @@ class TimeSynthesis {
323
364
  offset += 1;
324
365
  buf.writeFloatBE(pot.confidence, offset);
325
366
  offset += 4;
326
- buf.writeUInt8(pot.signatures.length, offset);
367
+ buf.writeUInt8(pot.sourceReadings.length, offset);
368
+ offset += 1;
369
+ // Nonce
370
+ buf.writeUInt8(nonceBytes.length, offset);
327
371
  offset += 1;
328
- for (const sig of pot.signatures) {
372
+ nonceBytes.copy(buf, offset);
373
+ offset += nonceBytes.length;
374
+ // ExpiresAt
375
+ buf.writeBigUInt64BE(pot.expiresAt, offset);
376
+ offset += 8;
377
+ for (const sig of pot.sourceReadings) {
329
378
  buf.writeUInt8(sig.source.length, offset);
330
379
  offset += 1;
331
380
  buf.write(sig.source, offset);
@@ -354,7 +403,15 @@ class TimeSynthesis {
354
403
  offset += 4;
355
404
  const sigCount = buf.readUInt8(offset);
356
405
  offset += 1;
357
- const signatures = [];
406
+ // Nonce
407
+ const nonceLen = buf.readUInt8(offset);
408
+ offset += 1;
409
+ const nonce = buf.toString('utf8', offset, offset + nonceLen);
410
+ offset += nonceLen;
411
+ // ExpiresAt
412
+ const expiresAt = buf.readBigUInt64BE(offset);
413
+ offset += 8;
414
+ const sourceReadings = [];
358
415
  for (let i = 0; i < sigCount; i++) {
359
416
  const nameLen = buf.readUInt8(offset);
360
417
  offset += 1;
@@ -364,9 +421,9 @@ class TimeSynthesis {
364
421
  offset += 8;
365
422
  const unc = buf.readFloatBE(offset);
366
423
  offset += 4;
367
- signatures.push({ source, timestamp: ts, uncertainty: unc });
424
+ sourceReadings.push({ source, timestamp: ts, uncertainty: unc });
368
425
  }
369
- return { timestamp, uncertainty, sources, stratum, confidence, signatures };
426
+ return { timestamp, uncertainty, sources, stratum, confidence, sourceReadings, nonce, expiresAt };
370
427
  }
371
428
  }
372
429
  exports.TimeSynthesis = TimeSynthesis;
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Signer } from "ethers";
2
2
  import { SignerConfig } from "./signer";
3
3
  import { NetworkConfig } from "./networks";
4
+ import { PotSignature } from "./pot_signer";
4
5
  export type TierType = "T0_epoch" | "T1_block" | "T2_slot" | "T3_micro";
5
6
  export declare const TierIntervals: Record<TierType, number>;
6
7
  /**
@@ -133,9 +134,13 @@ export interface ProofOfTime {
133
134
  sources: number;
134
135
  stratum: number;
135
136
  confidence: number;
136
- signatures: {
137
+ sourceReadings: {
137
138
  source: string;
138
139
  timestamp: bigint;
139
140
  uncertainty: number;
140
141
  }[];
142
+ nonce: string;
143
+ expiresAt: bigint;
144
+ issuerSignature?: PotSignature;
141
145
  }
146
+ export type { PotSignature } from "./pot_signer";