@sip-protocol/sdk 0.7.3 → 0.7.4
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/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
- package/dist/browser.d.mts +10 -4
- package/dist/browser.d.ts +10 -4
- package/dist/browser.js +47556 -19603
- package/dist/browser.mjs +628 -48
- package/dist/chunk-4GRJ5MAW.mjs +152 -0
- package/dist/chunk-5D7A3L3W.mjs +717 -0
- package/dist/chunk-64AYA5F5.mjs +7834 -0
- package/dist/chunk-GMDGB22A.mjs +379 -0
- package/dist/chunk-I534WKN7.mjs +328 -0
- package/dist/chunk-IBZVA5Y7.mjs +1003 -0
- package/dist/chunk-PRRZAWJE.mjs +223 -0
- package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
- package/dist/{chunk-3M3HNQCW.mjs → chunk-YWGJ77A2.mjs} +28656 -13103
- package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
- package/dist/constants-LHAAUC2T.mjs +51 -0
- package/dist/dist-2OGQ7FED.mjs +3957 -0
- package/dist/dist-IFHPYLDX.mjs +254 -0
- package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
- package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
- package/dist/{index-DIBZHOOQ.d.ts → index-DXh2IGkz.d.ts} +21239 -10304
- package/dist/{index-8MQz13eJ.d.mts → index-DeE1ZzA4.d.mts} +21239 -10304
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48396 -19623
- package/dist/index.mjs +537 -19
- package/dist/interface-Bf7w1PLW.d.mts +679 -0
- package/dist/interface-Bf7w1PLW.d.ts +679 -0
- package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
- package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
- package/dist/proofs/halo2.d.mts +151 -0
- package/dist/proofs/halo2.d.ts +151 -0
- package/dist/proofs/halo2.js +350 -0
- package/dist/proofs/halo2.mjs +11 -0
- package/dist/proofs/kimchi.d.mts +160 -0
- package/dist/proofs/kimchi.d.ts +160 -0
- package/dist/proofs/kimchi.js +431 -0
- package/dist/proofs/kimchi.mjs +13 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +74 -18
- package/dist/proofs/noir.mjs +84 -24
- package/dist/solana-U3MEGU7W.mjs +280 -0
- package/dist/validity_proof-3POXLPNY.mjs +21 -0
- package/package.json +54 -21
- package/src/adapters/index.ts +41 -0
- package/src/adapters/jupiter.ts +571 -0
- package/src/adapters/near-intents.ts +135 -0
- package/src/advisor/advisor.ts +653 -0
- package/src/advisor/index.ts +54 -0
- package/src/advisor/tools.ts +303 -0
- package/src/advisor/types.ts +164 -0
- package/src/chains/ethereum/announcement.ts +536 -0
- package/src/chains/ethereum/bnb-optimizations.ts +474 -0
- package/src/chains/ethereum/commitment.ts +522 -0
- package/src/chains/ethereum/constants.ts +462 -0
- package/src/chains/ethereum/deployment.ts +596 -0
- package/src/chains/ethereum/gas-estimation.ts +538 -0
- package/src/chains/ethereum/index.ts +268 -0
- package/src/chains/ethereum/optimizations.ts +614 -0
- package/src/chains/ethereum/privacy-adapter.ts +855 -0
- package/src/chains/ethereum/registry.ts +584 -0
- package/src/chains/ethereum/rpc.ts +905 -0
- package/src/chains/ethereum/stealth.ts +491 -0
- package/src/chains/ethereum/token.ts +790 -0
- package/src/chains/ethereum/transfer.ts +637 -0
- package/src/chains/ethereum/types.ts +456 -0
- package/src/chains/ethereum/viewing-key.ts +455 -0
- package/src/chains/near/commitment.ts +608 -0
- package/src/chains/near/constants.ts +284 -0
- package/src/chains/near/function-call.ts +871 -0
- package/src/chains/near/history.ts +654 -0
- package/src/chains/near/implicit-account.ts +840 -0
- package/src/chains/near/index.ts +393 -0
- package/src/chains/near/native-transfer.ts +658 -0
- package/src/chains/near/nep141.ts +775 -0
- package/src/chains/near/privacy-adapter.ts +889 -0
- package/src/chains/near/resolver.ts +971 -0
- package/src/chains/near/rpc.ts +1016 -0
- package/src/chains/near/stealth.ts +419 -0
- package/src/chains/near/types.ts +317 -0
- package/src/chains/near/viewing-key.ts +876 -0
- package/src/chains/solana/anchor-transfer.ts +386 -0
- package/src/chains/solana/commitment.ts +577 -0
- package/src/chains/solana/constants.ts +126 -12
- package/src/chains/solana/ephemeral-keys.ts +543 -0
- package/src/chains/solana/index.ts +252 -1
- package/src/chains/solana/key-derivation.ts +418 -0
- package/src/chains/solana/kit-compat.ts +334 -0
- package/src/chains/solana/optimizations.ts +560 -0
- package/src/chains/solana/privacy-adapter.ts +605 -0
- package/src/chains/solana/providers/generic.ts +47 -6
- package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
- package/src/chains/solana/providers/helius-enhanced.ts +623 -0
- package/src/chains/solana/providers/helius.ts +186 -33
- package/src/chains/solana/providers/index.ts +31 -0
- package/src/chains/solana/providers/interface.ts +61 -18
- package/src/chains/solana/providers/quicknode.ts +409 -0
- package/src/chains/solana/providers/triton.ts +426 -0
- package/src/chains/solana/providers/webhook.ts +338 -67
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +83 -66
- package/src/chains/solana/sol-transfer.ts +732 -0
- package/src/chains/solana/spl-transfer.ts +886 -0
- package/src/chains/solana/stealth-scanner.ts +703 -0
- package/src/chains/solana/sunspot-verifier.ts +453 -0
- package/src/chains/solana/transaction-builder.ts +755 -0
- package/src/chains/solana/transfer.ts +74 -5
- package/src/chains/solana/types.ts +57 -6
- package/src/chains/solana/utils.ts +110 -0
- package/src/chains/solana/viewing-key.ts +807 -0
- package/src/compliance/fireblocks.ts +921 -0
- package/src/compliance/index.ts +23 -0
- package/src/compliance/range-sas.ts +398 -33
- package/src/config/endpoints.ts +100 -0
- package/src/crypto.ts +11 -8
- package/src/errors.ts +82 -0
- package/src/evm/erc4337-relayer.ts +830 -0
- package/src/evm/index.ts +47 -0
- package/src/fees/calculator.ts +396 -0
- package/src/fees/index.ts +87 -0
- package/src/fees/near-contract.ts +429 -0
- package/src/fees/types.ts +268 -0
- package/src/index.ts +686 -1
- package/src/intent.ts +6 -3
- package/src/logger.ts +324 -0
- package/src/network/index.ts +80 -0
- package/src/network/proxy.ts +691 -0
- package/src/optimizations/index.ts +541 -0
- package/src/oracle/types.ts +1 -0
- package/src/privacy-backends/arcium-types.ts +727 -0
- package/src/privacy-backends/arcium.ts +719 -0
- package/src/privacy-backends/combined-privacy.ts +866 -0
- package/src/privacy-backends/cspl-token.ts +595 -0
- package/src/privacy-backends/cspl-types.ts +512 -0
- package/src/privacy-backends/cspl.ts +907 -0
- package/src/privacy-backends/health.ts +488 -0
- package/src/privacy-backends/inco-types.ts +323 -0
- package/src/privacy-backends/inco.ts +616 -0
- package/src/privacy-backends/index.ts +254 -4
- package/src/privacy-backends/interface.ts +649 -6
- package/src/privacy-backends/lru-cache.ts +343 -0
- package/src/privacy-backends/magicblock.ts +458 -0
- package/src/privacy-backends/mock.ts +258 -0
- package/src/privacy-backends/privacycash.ts +13 -17
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +414 -2
- package/src/privacy-backends/router.ts +283 -3
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +3 -0
- package/src/privacy-logger.ts +191 -0
- package/src/production-safety.ts +373 -0
- package/src/proofs/aggregator.ts +1029 -0
- package/src/proofs/browser-composer.ts +1150 -0
- package/src/proofs/browser.ts +113 -25
- package/src/proofs/cache/index.ts +127 -0
- package/src/proofs/cache/interface.ts +545 -0
- package/src/proofs/cache/key-generator.ts +188 -0
- package/src/proofs/cache/lru-cache.ts +481 -0
- package/src/proofs/cache/multi-tier-cache.ts +575 -0
- package/src/proofs/cache/persistent-cache.ts +788 -0
- package/src/proofs/compliance-proof.ts +872 -0
- package/src/proofs/composer/base.ts +923 -0
- package/src/proofs/composer/index.ts +25 -0
- package/src/proofs/composer/interface.ts +518 -0
- package/src/proofs/composer/types.ts +383 -0
- package/src/proofs/converters/halo2.ts +452 -0
- package/src/proofs/converters/index.ts +208 -0
- package/src/proofs/converters/interface.ts +363 -0
- package/src/proofs/converters/kimchi.ts +462 -0
- package/src/proofs/converters/noir.ts +451 -0
- package/src/proofs/fallback.ts +888 -0
- package/src/proofs/halo2.ts +42 -0
- package/src/proofs/index.ts +471 -0
- package/src/proofs/interface.ts +13 -0
- package/src/proofs/kimchi.ts +42 -0
- package/src/proofs/lazy.ts +1004 -0
- package/src/proofs/mock.ts +25 -1
- package/src/proofs/noir.ts +110 -29
- package/src/proofs/orchestrator.ts +960 -0
- package/src/proofs/parallel/concurrency.ts +297 -0
- package/src/proofs/parallel/dependency-graph.ts +602 -0
- package/src/proofs/parallel/executor.ts +420 -0
- package/src/proofs/parallel/index.ts +131 -0
- package/src/proofs/parallel/interface.ts +685 -0
- package/src/proofs/parallel/worker-pool.ts +644 -0
- package/src/proofs/providers/halo2.ts +560 -0
- package/src/proofs/providers/index.ts +34 -0
- package/src/proofs/providers/kimchi.ts +641 -0
- package/src/proofs/validator.ts +881 -0
- package/src/proofs/verifier.ts +867 -0
- package/src/quantum/index.ts +112 -0
- package/src/quantum/winternitz-vault.ts +639 -0
- package/src/quantum/wots.ts +611 -0
- package/src/settlement/backends/direct-chain.ts +1 -0
- package/src/settlement/index.ts +9 -0
- package/src/settlement/router.ts +732 -46
- package/src/solana/index.ts +72 -0
- package/src/solana/jito-relayer.ts +687 -0
- package/src/solana/noir-verifier-types.ts +430 -0
- package/src/solana/noir-verifier.ts +816 -0
- package/src/stealth/address-derivation.ts +193 -0
- package/src/stealth/ed25519.ts +431 -0
- package/src/stealth/index.ts +233 -0
- package/src/stealth/meta-address.ts +221 -0
- package/src/stealth/secp256k1.ts +368 -0
- package/src/stealth/utils.ts +194 -0
- package/src/stealth.ts +50 -1504
- package/src/sync/index.ts +106 -0
- package/src/sync/manager.ts +504 -0
- package/src/sync/mock-provider.ts +318 -0
- package/src/sync/oblivious.ts +625 -0
- package/src/tokens/index.ts +15 -0
- package/src/tokens/registry.ts +301 -0
- package/src/utils/deprecation.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/wallet/ethereum/index.ts +68 -0
- package/src/wallet/ethereum/metamask-privacy.ts +420 -0
- package/src/wallet/ethereum/multi-wallet.ts +646 -0
- package/src/wallet/ethereum/privacy-adapter.ts +700 -0
- package/src/wallet/ethereum/types.ts +3 -1
- package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
- package/src/wallet/hardware/index.ts +10 -0
- package/src/wallet/hardware/ledger-privacy.ts +414 -0
- package/src/wallet/index.ts +71 -0
- package/src/wallet/near/adapter.ts +626 -0
- package/src/wallet/near/index.ts +86 -0
- package/src/wallet/near/meteor-wallet.ts +1153 -0
- package/src/wallet/near/my-near-wallet.ts +790 -0
- package/src/wallet/near/wallet-selector.ts +702 -0
- package/src/wallet/solana/adapter.ts +6 -4
- package/src/wallet/solana/index.ts +13 -0
- package/src/wallet/solana/privacy-adapter.ts +567 -0
- package/src/wallet/sui/types.ts +6 -4
- package/src/zcash/rpc-client.ts +13 -6
- package/dist/chunk-2XIVXWHA.mjs +0 -1930
- package/dist/chunk-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-7RFRWDCW.mjs +0 -1504
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-F6F73W35.mjs +0 -16166
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-OFDBEIEK.mjs +0 -16166
- package/dist/chunk-SF7YSLF5.mjs +0 -1515
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/chunk-WWUSGOXE.mjs +0 -17129
- package/dist/constants-VOI7BSLK.mjs +0 -27
- package/dist/index-B71aXVzk.d.ts +0 -13264
- package/dist/index-BYZbDjal.d.ts +0 -11390
- package/dist/index-CHB3KuOB.d.mts +0 -11859
- package/dist/index-CzWPI6Le.d.ts +0 -11859
- package/dist/index-pOIIuwfV.d.mts +0 -13264
- package/dist/index-xbWjohNq.d.mts +0 -11390
- package/dist/solana-4O4K45VU.mjs +0 -46
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-NDABAZ6P.mjs +0 -56
- package/dist/solana-Q4NAVBTS.mjs +0 -46
- package/dist/solana-ZYO63LY5.mjs +0 -46
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Key Generator for Proof Caching
|
|
3
|
+
*
|
|
4
|
+
* @module proofs/cache/key-generator
|
|
5
|
+
* @description Generates deterministic cache keys from proof inputs
|
|
6
|
+
*
|
|
7
|
+
* M20-13: Implement proof caching layer (#313)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
11
|
+
import { bytesToHex } from '@noble/hashes/utils'
|
|
12
|
+
import type { CacheKey, CacheKeyComponents, ICacheKeyGenerator } from './interface'
|
|
13
|
+
|
|
14
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const KEY_SEPARATOR = ':'
|
|
17
|
+
const KEY_PREFIX = 'sip-proof'
|
|
18
|
+
const KEY_VERSION = 'v1'
|
|
19
|
+
|
|
20
|
+
// ─── Helper Functions ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Canonicalize an object for deterministic hashing
|
|
24
|
+
* Sorts keys recursively and handles special types
|
|
25
|
+
*/
|
|
26
|
+
function canonicalize(value: unknown): string {
|
|
27
|
+
if (value === null) return 'null'
|
|
28
|
+
if (value === undefined) return 'undefined'
|
|
29
|
+
|
|
30
|
+
if (typeof value === 'bigint') {
|
|
31
|
+
return `bigint:${value.toString()}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof value === 'number') {
|
|
35
|
+
if (Number.isNaN(value)) return 'NaN'
|
|
36
|
+
if (!Number.isFinite(value)) return value > 0 ? 'Infinity' : '-Infinity'
|
|
37
|
+
return value.toString()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof value === 'string') {
|
|
41
|
+
return JSON.stringify(value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof value === 'boolean') {
|
|
45
|
+
return value.toString()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (value instanceof Uint8Array) {
|
|
49
|
+
return `bytes:${bytesToHex(value)}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return `[${value.map(canonicalize).join(',')}]`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof value === 'object') {
|
|
57
|
+
const obj = value as Record<string, unknown>
|
|
58
|
+
const sortedKeys = Object.keys(obj).sort()
|
|
59
|
+
const pairs = sortedKeys.map((key) => `${JSON.stringify(key)}:${canonicalize(obj[key])}`)
|
|
60
|
+
return `{${pairs.join(',')}}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return String(value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Cache Key Generator Implementation ──────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates deterministic cache keys from proof input components
|
|
70
|
+
*/
|
|
71
|
+
export class CacheKeyGenerator implements ICacheKeyGenerator {
|
|
72
|
+
/**
|
|
73
|
+
* Generate a cache key from components
|
|
74
|
+
*/
|
|
75
|
+
generate(components: CacheKeyComponents): CacheKey {
|
|
76
|
+
const parts = [
|
|
77
|
+
KEY_PREFIX,
|
|
78
|
+
KEY_VERSION,
|
|
79
|
+
components.system,
|
|
80
|
+
components.circuitId,
|
|
81
|
+
components.privateInputsHash,
|
|
82
|
+
components.publicInputsHash,
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if (components.version) {
|
|
86
|
+
parts.push(components.version)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const key = parts.join(KEY_SEPARATOR)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
key,
|
|
93
|
+
components,
|
|
94
|
+
generatedAt: Date.now(),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse a cache key string back to components
|
|
100
|
+
*/
|
|
101
|
+
parse(key: string): CacheKeyComponents | null {
|
|
102
|
+
const parts = key.split(KEY_SEPARATOR)
|
|
103
|
+
|
|
104
|
+
// Validate prefix and version
|
|
105
|
+
if (parts.length < 6 || parts[0] !== KEY_PREFIX || parts[1] !== KEY_VERSION) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const [, , system, circuitId, privateInputsHash, publicInputsHash, version] = parts
|
|
110
|
+
|
|
111
|
+
// Validate system is a known proof system
|
|
112
|
+
const validSystems = ['noir', 'halo2', 'kimchi', 'groth16', 'plonk', 'stark', 'bulletproofs']
|
|
113
|
+
if (!validSystems.includes(system)) {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
system: system as CacheKeyComponents['system'],
|
|
119
|
+
circuitId,
|
|
120
|
+
privateInputsHash,
|
|
121
|
+
publicInputsHash,
|
|
122
|
+
version,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Hash input data deterministically
|
|
128
|
+
*/
|
|
129
|
+
hashInputs(inputs: Record<string, unknown>): string {
|
|
130
|
+
const canonical = canonicalize(inputs)
|
|
131
|
+
const hash = sha256(new TextEncoder().encode(canonical))
|
|
132
|
+
return bytesToHex(hash)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate a cache key from raw inputs
|
|
137
|
+
*/
|
|
138
|
+
generateFromInputs(
|
|
139
|
+
system: CacheKeyComponents['system'],
|
|
140
|
+
circuitId: string,
|
|
141
|
+
privateInputs: Record<string, unknown>,
|
|
142
|
+
publicInputs: Record<string, unknown>,
|
|
143
|
+
version?: string
|
|
144
|
+
): CacheKey {
|
|
145
|
+
return this.generate({
|
|
146
|
+
system,
|
|
147
|
+
circuitId,
|
|
148
|
+
privateInputsHash: this.hashInputs(privateInputs),
|
|
149
|
+
publicInputsHash: this.hashInputs(publicInputs),
|
|
150
|
+
version,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if two cache keys are equal
|
|
156
|
+
*/
|
|
157
|
+
equals(a: CacheKey | string, b: CacheKey | string): boolean {
|
|
158
|
+
const keyA = typeof a === 'string' ? a : a.key
|
|
159
|
+
const keyB = typeof b === 'string' ? b : b.key
|
|
160
|
+
return keyA === keyB
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if a key matches a pattern (glob-style)
|
|
165
|
+
*/
|
|
166
|
+
matches(key: string, pattern: string): boolean {
|
|
167
|
+
// Convert glob pattern to regex
|
|
168
|
+
const regexPattern = pattern
|
|
169
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
170
|
+
.replace(/\*/g, '.*') // Convert * to .*
|
|
171
|
+
.replace(/\?/g, '.') // Convert ? to .
|
|
172
|
+
|
|
173
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
174
|
+
return regex.test(key)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a cache key generator instance
|
|
180
|
+
*/
|
|
181
|
+
export function createCacheKeyGenerator(): ICacheKeyGenerator {
|
|
182
|
+
return new CacheKeyGenerator()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Singleton instance for convenience
|
|
187
|
+
*/
|
|
188
|
+
export const cacheKeyGenerator = new CacheKeyGenerator()
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU Cache Implementation for Proof Caching
|
|
3
|
+
*
|
|
4
|
+
* @module proofs/cache/lru-cache
|
|
5
|
+
* @description In-memory LRU cache for hot proofs
|
|
6
|
+
*
|
|
7
|
+
* M20-13: Implement proof caching layer (#313)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SingleProof } from '@sip-protocol/types'
|
|
11
|
+
import type {
|
|
12
|
+
CacheKey,
|
|
13
|
+
CacheEntry,
|
|
14
|
+
CacheEntryMetadata,
|
|
15
|
+
CacheLookupResult,
|
|
16
|
+
ProofCacheStats,
|
|
17
|
+
CacheEvent,
|
|
18
|
+
CacheEventListener,
|
|
19
|
+
LRUCacheConfig,
|
|
20
|
+
ILRUCache,
|
|
21
|
+
} from './interface'
|
|
22
|
+
import { DEFAULT_LRU_CONFIG } from './interface'
|
|
23
|
+
|
|
24
|
+
// ─── Internal Types ──────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface InternalEntry<T> {
|
|
27
|
+
key: string
|
|
28
|
+
value: T
|
|
29
|
+
metadata: CacheEntryMetadata
|
|
30
|
+
prev: InternalEntry<T> | null
|
|
31
|
+
next: InternalEntry<T> | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── LRU Cache Implementation ────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* In-memory LRU cache for proofs
|
|
38
|
+
* Uses a doubly-linked list for O(1) LRU operations
|
|
39
|
+
*/
|
|
40
|
+
export class LRUCache<T = SingleProof> implements ILRUCache<T> {
|
|
41
|
+
private readonly config: LRUCacheConfig
|
|
42
|
+
private readonly cache = new Map<string, InternalEntry<T>>()
|
|
43
|
+
private head: InternalEntry<T> | null = null
|
|
44
|
+
private tail: InternalEntry<T> | null = null
|
|
45
|
+
private currentSizeBytes = 0
|
|
46
|
+
private readonly listeners: Set<CacheEventListener> = new Set()
|
|
47
|
+
private evictionTimer: ReturnType<typeof setInterval> | null = null
|
|
48
|
+
|
|
49
|
+
// Statistics
|
|
50
|
+
private totalLookups = 0
|
|
51
|
+
private hits = 0
|
|
52
|
+
private misses = 0
|
|
53
|
+
private evictions = 0
|
|
54
|
+
private expirations = 0
|
|
55
|
+
private totalLookupTimeMs = 0
|
|
56
|
+
|
|
57
|
+
constructor(config: Partial<LRUCacheConfig> = {}) {
|
|
58
|
+
this.config = { ...DEFAULT_LRU_CONFIG, ...config }
|
|
59
|
+
|
|
60
|
+
// Start eviction timer if configured
|
|
61
|
+
if (this.config.evictionIntervalMs > 0) {
|
|
62
|
+
this.startEvictionTimer()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get an entry from the cache
|
|
68
|
+
*/
|
|
69
|
+
async get(key: CacheKey | string): Promise<CacheLookupResult<T>> {
|
|
70
|
+
const startTime = Date.now()
|
|
71
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
72
|
+
|
|
73
|
+
this.totalLookups++
|
|
74
|
+
|
|
75
|
+
const entry = this.cache.get(keyStr)
|
|
76
|
+
|
|
77
|
+
if (!entry) {
|
|
78
|
+
this.misses++
|
|
79
|
+
this.updateLookupTime(startTime)
|
|
80
|
+
this.emitEvent({ type: 'miss', key: keyStr, timestamp: Date.now() })
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
hit: false,
|
|
84
|
+
missReason: 'not_found',
|
|
85
|
+
lookupTimeMs: Date.now() - startTime,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check expiration
|
|
90
|
+
if (entry.metadata.expiresAt > 0 && Date.now() > entry.metadata.expiresAt) {
|
|
91
|
+
this.deleteEntry(keyStr)
|
|
92
|
+
this.expirations++
|
|
93
|
+
this.misses++
|
|
94
|
+
this.updateLookupTime(startTime)
|
|
95
|
+
this.emitEvent({ type: 'expire', key: keyStr, timestamp: Date.now() })
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
hit: false,
|
|
99
|
+
missReason: 'expired',
|
|
100
|
+
lookupTimeMs: Date.now() - startTime,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Update access tracking
|
|
105
|
+
if (this.config.trackAccess) {
|
|
106
|
+
entry.metadata.lastAccessedAt = Date.now()
|
|
107
|
+
entry.metadata.accessCount++
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Move to head (most recently used)
|
|
111
|
+
this.moveToHead(entry)
|
|
112
|
+
|
|
113
|
+
this.hits++
|
|
114
|
+
this.updateLookupTime(startTime)
|
|
115
|
+
this.emitEvent({ type: 'hit', key: keyStr, timestamp: Date.now() })
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
hit: true,
|
|
119
|
+
entry: {
|
|
120
|
+
key: typeof key === 'string' ? { key, components: {} as CacheKey['components'], generatedAt: 0 } : key,
|
|
121
|
+
value: entry.value,
|
|
122
|
+
metadata: entry.metadata,
|
|
123
|
+
},
|
|
124
|
+
lookupTimeMs: Date.now() - startTime,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set an entry in the cache
|
|
130
|
+
*/
|
|
131
|
+
async set(key: CacheKey | string, value: T, ttlMs?: number): Promise<boolean> {
|
|
132
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
133
|
+
const effectiveTtl = ttlMs ?? this.config.defaultTtlMs
|
|
134
|
+
const sizeBytes = this.estimateSize(value)
|
|
135
|
+
|
|
136
|
+
// Check if entry already exists
|
|
137
|
+
const existing = this.cache.get(keyStr)
|
|
138
|
+
if (existing) {
|
|
139
|
+
// Update existing entry
|
|
140
|
+
this.currentSizeBytes -= existing.metadata.sizeBytes
|
|
141
|
+
existing.value = value
|
|
142
|
+
existing.metadata = {
|
|
143
|
+
...existing.metadata,
|
|
144
|
+
sizeBytes,
|
|
145
|
+
ttlMs: effectiveTtl,
|
|
146
|
+
expiresAt: effectiveTtl > 0 ? Date.now() + effectiveTtl : 0,
|
|
147
|
+
}
|
|
148
|
+
this.currentSizeBytes += sizeBytes
|
|
149
|
+
this.moveToHead(existing)
|
|
150
|
+
this.emitEvent({ type: 'set', key: keyStr, timestamp: Date.now() })
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Evict if necessary to make room
|
|
155
|
+
while (
|
|
156
|
+
(this.cache.size >= this.config.maxEntries ||
|
|
157
|
+
this.currentSizeBytes + sizeBytes > this.config.maxSizeBytes) &&
|
|
158
|
+
this.cache.size > 0
|
|
159
|
+
) {
|
|
160
|
+
this.evictLRU()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create new entry
|
|
164
|
+
const metadata: CacheEntryMetadata = {
|
|
165
|
+
createdAt: Date.now(),
|
|
166
|
+
lastAccessedAt: Date.now(),
|
|
167
|
+
accessCount: 0,
|
|
168
|
+
sizeBytes,
|
|
169
|
+
ttlMs: effectiveTtl,
|
|
170
|
+
expiresAt: effectiveTtl > 0 ? Date.now() + effectiveTtl : 0,
|
|
171
|
+
source: 'generation',
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const entry: InternalEntry<T> = {
|
|
175
|
+
key: keyStr,
|
|
176
|
+
value,
|
|
177
|
+
metadata,
|
|
178
|
+
prev: null,
|
|
179
|
+
next: null,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.cache.set(keyStr, entry)
|
|
183
|
+
this.currentSizeBytes += sizeBytes
|
|
184
|
+
this.addToHead(entry)
|
|
185
|
+
|
|
186
|
+
this.emitEvent({ type: 'set', key: keyStr, timestamp: Date.now() })
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete an entry from the cache
|
|
192
|
+
*/
|
|
193
|
+
async delete(key: CacheKey | string): Promise<boolean> {
|
|
194
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
195
|
+
const deleted = this.deleteEntry(keyStr)
|
|
196
|
+
|
|
197
|
+
if (deleted) {
|
|
198
|
+
this.emitEvent({ type: 'delete', key: keyStr, timestamp: Date.now() })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return deleted
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if an entry exists
|
|
206
|
+
*/
|
|
207
|
+
async has(key: CacheKey | string): Promise<boolean> {
|
|
208
|
+
const keyStr = typeof key === 'string' ? key : key.key
|
|
209
|
+
const entry = this.cache.get(keyStr)
|
|
210
|
+
|
|
211
|
+
if (!entry) return false
|
|
212
|
+
|
|
213
|
+
// Check expiration
|
|
214
|
+
if (entry.metadata.expiresAt > 0 && Date.now() > entry.metadata.expiresAt) {
|
|
215
|
+
this.deleteEntry(keyStr)
|
|
216
|
+
this.expirations++
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Clear all entries
|
|
225
|
+
*/
|
|
226
|
+
async clear(): Promise<void> {
|
|
227
|
+
this.cache.clear()
|
|
228
|
+
this.head = null
|
|
229
|
+
this.tail = null
|
|
230
|
+
this.currentSizeBytes = 0
|
|
231
|
+
this.emitEvent({ type: 'clear', timestamp: Date.now() })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get cache statistics
|
|
236
|
+
*/
|
|
237
|
+
getStats(): ProofCacheStats {
|
|
238
|
+
const avgLookupTimeMs =
|
|
239
|
+
this.totalLookups > 0 ? this.totalLookupTimeMs / this.totalLookups : 0
|
|
240
|
+
|
|
241
|
+
// Calculate average entry age
|
|
242
|
+
let totalAge = 0
|
|
243
|
+
const now = Date.now()
|
|
244
|
+
for (const entry of this.cache.values()) {
|
|
245
|
+
totalAge += now - entry.metadata.createdAt
|
|
246
|
+
}
|
|
247
|
+
const avgEntryAgeMs = this.cache.size > 0 ? totalAge / this.cache.size : 0
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
totalLookups: this.totalLookups,
|
|
251
|
+
hits: this.hits,
|
|
252
|
+
misses: this.misses,
|
|
253
|
+
hitRate: this.totalLookups > 0 ? this.hits / this.totalLookups : 0,
|
|
254
|
+
entryCount: this.cache.size,
|
|
255
|
+
sizeBytes: this.currentSizeBytes,
|
|
256
|
+
maxSizeBytes: this.config.maxSizeBytes,
|
|
257
|
+
evictions: this.evictions,
|
|
258
|
+
expirations: this.expirations,
|
|
259
|
+
avgLookupTimeMs,
|
|
260
|
+
avgEntryAgeMs,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get all keys matching a pattern
|
|
266
|
+
*/
|
|
267
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
268
|
+
const allKeys = Array.from(this.cache.keys())
|
|
269
|
+
|
|
270
|
+
if (!pattern) {
|
|
271
|
+
return allKeys
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Convert glob pattern to regex
|
|
275
|
+
const regexPattern = pattern
|
|
276
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
277
|
+
.replace(/\*/g, '.*')
|
|
278
|
+
.replace(/\?/g, '.')
|
|
279
|
+
|
|
280
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
281
|
+
return allKeys.filter((key) => regex.test(key))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Add an event listener
|
|
286
|
+
*/
|
|
287
|
+
addEventListener(listener: CacheEventListener): void {
|
|
288
|
+
this.listeners.add(listener)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Remove an event listener
|
|
293
|
+
*/
|
|
294
|
+
removeEventListener(listener: CacheEventListener): void {
|
|
295
|
+
this.listeners.delete(listener)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the current size in bytes
|
|
300
|
+
*/
|
|
301
|
+
getSizeBytes(): number {
|
|
302
|
+
return this.currentSizeBytes
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get the entry count
|
|
307
|
+
*/
|
|
308
|
+
getEntryCount(): number {
|
|
309
|
+
return this.cache.size
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Manually trigger eviction
|
|
314
|
+
*/
|
|
315
|
+
evict(count?: number): number {
|
|
316
|
+
const toEvict = count ?? 1
|
|
317
|
+
let evicted = 0
|
|
318
|
+
|
|
319
|
+
for (let i = 0; i < toEvict && this.cache.size > 0; i++) {
|
|
320
|
+
if (this.evictLRU()) {
|
|
321
|
+
evicted++
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return evicted
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get entries in LRU order (most recent first)
|
|
330
|
+
*/
|
|
331
|
+
getEntriesLRU(): CacheEntry<T>[] {
|
|
332
|
+
const entries: CacheEntry<T>[] = []
|
|
333
|
+
let current = this.head
|
|
334
|
+
|
|
335
|
+
while (current) {
|
|
336
|
+
entries.push({
|
|
337
|
+
key: { key: current.key, components: {} as CacheKey['components'], generatedAt: 0 },
|
|
338
|
+
value: current.value,
|
|
339
|
+
metadata: current.metadata,
|
|
340
|
+
})
|
|
341
|
+
current = current.next
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return entries
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Dispose of the cache
|
|
349
|
+
*/
|
|
350
|
+
dispose(): void {
|
|
351
|
+
if (this.evictionTimer) {
|
|
352
|
+
clearInterval(this.evictionTimer)
|
|
353
|
+
this.evictionTimer = null
|
|
354
|
+
}
|
|
355
|
+
this.clear()
|
|
356
|
+
this.listeners.clear()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── Private Methods ─────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
private deleteEntry(key: string): boolean {
|
|
362
|
+
const entry = this.cache.get(key)
|
|
363
|
+
if (!entry) return false
|
|
364
|
+
|
|
365
|
+
this.removeFromList(entry)
|
|
366
|
+
this.cache.delete(key)
|
|
367
|
+
this.currentSizeBytes -= entry.metadata.sizeBytes
|
|
368
|
+
|
|
369
|
+
return true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private evictLRU(): boolean {
|
|
373
|
+
if (!this.tail) return false
|
|
374
|
+
|
|
375
|
+
const evictedKey = this.tail.key
|
|
376
|
+
this.deleteEntry(evictedKey)
|
|
377
|
+
this.evictions++
|
|
378
|
+
this.emitEvent({ type: 'evict', key: evictedKey, timestamp: Date.now() })
|
|
379
|
+
|
|
380
|
+
return true
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private addToHead(entry: InternalEntry<T>): void {
|
|
384
|
+
entry.prev = null
|
|
385
|
+
entry.next = this.head
|
|
386
|
+
|
|
387
|
+
if (this.head) {
|
|
388
|
+
this.head.prev = entry
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.head = entry
|
|
392
|
+
|
|
393
|
+
if (!this.tail) {
|
|
394
|
+
this.tail = entry
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private removeFromList(entry: InternalEntry<T>): void {
|
|
399
|
+
if (entry.prev) {
|
|
400
|
+
entry.prev.next = entry.next
|
|
401
|
+
} else {
|
|
402
|
+
this.head = entry.next
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (entry.next) {
|
|
406
|
+
entry.next.prev = entry.prev
|
|
407
|
+
} else {
|
|
408
|
+
this.tail = entry.prev
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
entry.prev = null
|
|
412
|
+
entry.next = null
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private moveToHead(entry: InternalEntry<T>): void {
|
|
416
|
+
if (entry === this.head) return
|
|
417
|
+
|
|
418
|
+
this.removeFromList(entry)
|
|
419
|
+
this.addToHead(entry)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private estimateSize(value: T): number {
|
|
423
|
+
// Estimate size based on JSON serialization
|
|
424
|
+
try {
|
|
425
|
+
const json = JSON.stringify(value, (_, v) => {
|
|
426
|
+
if (typeof v === 'bigint') return v.toString()
|
|
427
|
+
return v
|
|
428
|
+
})
|
|
429
|
+
return new TextEncoder().encode(json).length
|
|
430
|
+
} catch {
|
|
431
|
+
// Fallback for non-serializable values
|
|
432
|
+
return 1024 // Default 1KB
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private updateLookupTime(startTime: number): void {
|
|
437
|
+
this.totalLookupTimeMs += Date.now() - startTime
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private emitEvent(event: CacheEvent): void {
|
|
441
|
+
for (const listener of this.listeners) {
|
|
442
|
+
try {
|
|
443
|
+
listener(event)
|
|
444
|
+
} catch {
|
|
445
|
+
// Ignore listener errors
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private startEvictionTimer(): void {
|
|
451
|
+
this.evictionTimer = setInterval(() => {
|
|
452
|
+
this.cleanupExpired()
|
|
453
|
+
}, this.config.evictionIntervalMs)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private cleanupExpired(): void {
|
|
457
|
+
const now = Date.now()
|
|
458
|
+
const keysToDelete: string[] = []
|
|
459
|
+
|
|
460
|
+
for (const [key, entry] of this.cache) {
|
|
461
|
+
if (entry.metadata.expiresAt > 0 && now > entry.metadata.expiresAt) {
|
|
462
|
+
keysToDelete.push(key)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (const key of keysToDelete) {
|
|
467
|
+
this.deleteEntry(key)
|
|
468
|
+
this.expirations++
|
|
469
|
+
this.emitEvent({ type: 'expire', key, timestamp: now })
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Create an LRU cache instance
|
|
476
|
+
*/
|
|
477
|
+
export function createLRUCache<T = SingleProof>(
|
|
478
|
+
config?: Partial<LRUCacheConfig>
|
|
479
|
+
): ILRUCache<T> {
|
|
480
|
+
return new LRUCache<T>(config)
|
|
481
|
+
}
|