mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-05-17 23:59:26 +00:00
Merge remote-tracking branch 'origin/master' into feat/sds-repair
This commit is contained in:
commit
942e040def
@ -24,9 +24,11 @@
|
|||||||
"cipherparams",
|
"cipherparams",
|
||||||
"ciphertext",
|
"ciphertext",
|
||||||
"circleci",
|
"circleci",
|
||||||
|
"circom",
|
||||||
"codecov",
|
"codecov",
|
||||||
"codegen",
|
"codegen",
|
||||||
"commitlint",
|
"commitlint",
|
||||||
|
"cooldown",
|
||||||
"dependabot",
|
"dependabot",
|
||||||
"dialable",
|
"dialable",
|
||||||
"dingpu",
|
"dingpu",
|
||||||
@ -41,9 +43,7 @@
|
|||||||
"Encrypters",
|
"Encrypters",
|
||||||
"enr",
|
"enr",
|
||||||
"enrs",
|
"enrs",
|
||||||
"unsubscription",
|
|
||||||
"enrtree",
|
"enrtree",
|
||||||
"unhandle",
|
|
||||||
"ephem",
|
"ephem",
|
||||||
"esnext",
|
"esnext",
|
||||||
"ethersproject",
|
"ethersproject",
|
||||||
@ -62,7 +62,6 @@
|
|||||||
"ineed",
|
"ineed",
|
||||||
"IPAM",
|
"IPAM",
|
||||||
"ipfs",
|
"ipfs",
|
||||||
"cooldown",
|
|
||||||
"iwant",
|
"iwant",
|
||||||
"jdev",
|
"jdev",
|
||||||
"jswaku",
|
"jswaku",
|
||||||
@ -122,9 +121,11 @@
|
|||||||
"typedoc",
|
"typedoc",
|
||||||
"undialable",
|
"undialable",
|
||||||
"unencrypted",
|
"unencrypted",
|
||||||
|
"unhandle",
|
||||||
"unmarshal",
|
"unmarshal",
|
||||||
"unmount",
|
"unmount",
|
||||||
"unmounts",
|
"unmounts",
|
||||||
|
"unsubscription",
|
||||||
"untracked",
|
"untracked",
|
||||||
"upgrader",
|
"upgrader",
|
||||||
"vacp",
|
"vacp",
|
||||||
@ -139,6 +140,7 @@
|
|||||||
"weboko",
|
"weboko",
|
||||||
"websockets",
|
"websockets",
|
||||||
"wifi",
|
"wifi",
|
||||||
|
"WTNS",
|
||||||
"xsalsa20",
|
"xsalsa20",
|
||||||
"zerokit",
|
"zerokit",
|
||||||
"Привет",
|
"Привет",
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"packages/utils": "0.0.26",
|
"packages/utils": "0.0.27",
|
||||||
"packages/proto": "0.0.13",
|
"packages/proto": "0.0.14",
|
||||||
"packages/interfaces": "0.0.33",
|
"packages/interfaces": "0.0.34",
|
||||||
"packages/enr": "0.0.32",
|
"packages/enr": "0.0.33",
|
||||||
"packages/core": "0.0.38",
|
"packages/core": "0.0.39",
|
||||||
"packages/message-encryption": "0.0.36",
|
"packages/message-encryption": "0.0.37",
|
||||||
"packages/relay": "0.0.21",
|
"packages/relay": "0.0.22",
|
||||||
"packages/sdk": "0.0.34",
|
"packages/sdk": "0.0.35",
|
||||||
"packages/discovery": "0.0.11",
|
"packages/discovery": "0.0.12",
|
||||||
"packages/sds": "0.0.6",
|
"packages/sds": "0.0.7",
|
||||||
"packages/rln": "0.1.8"
|
"packages/rln": "0.1.9"
|
||||||
}
|
}
|
||||||
|
|||||||
210
package-lock.json
generated
210
package-lock.json
generated
@ -13,10 +13,10 @@
|
|||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/discovery",
|
"packages/discovery",
|
||||||
"packages/message-encryption",
|
"packages/message-encryption",
|
||||||
"packages/sdk",
|
|
||||||
"packages/relay",
|
|
||||||
"packages/sds",
|
"packages/sds",
|
||||||
"packages/rln",
|
"packages/rln",
|
||||||
|
"packages/sdk",
|
||||||
|
"packages/relay",
|
||||||
"packages/tests",
|
"packages/tests",
|
||||||
"packages/reliability-tests",
|
"packages/reliability-tests",
|
||||||
"packages/headless-tests",
|
"packages/headless-tests",
|
||||||
@ -7536,9 +7536,17 @@
|
|||||||
"version": "4.17.18",
|
"version": "4.17.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.18.tgz",
|
||||||
"integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==",
|
"integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash.debounce": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/markdown-it": {
|
"node_modules/@types/markdown-it": {
|
||||||
"version": "14.1.2",
|
"version": "14.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
@ -8406,9 +8414,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@waku/zerokit-rln-wasm": {
|
"node_modules/@waku/zerokit-rln-wasm": {
|
||||||
"version": "0.0.13",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.2.1.tgz",
|
||||||
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==",
|
"integrity": "sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==",
|
||||||
"license": "MIT or Apache2"
|
"license": "MIT or Apache2"
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
@ -36650,15 +36658,15 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@waku/core",
|
"name": "@waku/core",
|
||||||
"version": "0.0.38",
|
"version": "0.0.39",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@libp2p/ping": "2.0.35",
|
"@libp2p/ping": "2.0.35",
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
"@waku/enr": "^0.0.32",
|
"@waku/enr": "^0.0.33",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/proto": "0.0.13",
|
"@waku/proto": "0.0.14",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"it-all": "^3.0.4",
|
"it-all": "^3.0.4",
|
||||||
"it-length-prefixed": "^9.0.4",
|
"it-length-prefixed": "^9.0.4",
|
||||||
@ -36718,14 +36726,14 @@
|
|||||||
},
|
},
|
||||||
"packages/discovery": {
|
"packages/discovery": {
|
||||||
"name": "@waku/discovery",
|
"name": "@waku/discovery",
|
||||||
"version": "0.0.11",
|
"version": "0.0.12",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@waku/core": "0.0.38",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/enr": "0.0.32",
|
"@waku/enr": "0.0.33",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/proto": "^0.0.13",
|
"@waku/proto": "^0.0.14",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"dns-over-http-resolver": "^3.0.8",
|
"dns-over-http-resolver": "^3.0.8",
|
||||||
"hi-base32": "^0.5.1",
|
"hi-base32": "^0.5.1",
|
||||||
@ -36754,7 +36762,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enr": {
|
"packages/enr": {
|
||||||
"name": "@waku/enr",
|
"name": "@waku/enr",
|
||||||
"version": "0.0.32",
|
"version": "0.0.33",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/rlp": "^5.7.0",
|
"@ethersproject/rlp": "^5.7.0",
|
||||||
@ -36762,7 +36770,7 @@
|
|||||||
"@libp2p/peer-id": "5.1.7",
|
"@libp2p/peer-id": "5.1.7",
|
||||||
"@multiformats/multiaddr": "^12.0.0",
|
"@multiformats/multiaddr": "^12.0.0",
|
||||||
"@noble/secp256k1": "^1.7.1",
|
"@noble/secp256k1": "^1.7.1",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"js-sha3": "^0.9.2"
|
"js-sha3": "^0.9.2"
|
||||||
},
|
},
|
||||||
@ -36773,7 +36781,7 @@
|
|||||||
"@types/chai": "^4.3.11",
|
"@types/chai": "^4.3.11",
|
||||||
"@types/mocha": "^10.0.6",
|
"@types/mocha": "^10.0.6",
|
||||||
"@waku/build-utils": "*",
|
"@waku/build-utils": "*",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
"cspell": "^8.6.1",
|
"cspell": "^8.6.1",
|
||||||
"fast-check": "^3.19.0",
|
"fast-check": "^3.19.0",
|
||||||
@ -37293,7 +37301,7 @@
|
|||||||
},
|
},
|
||||||
"packages/interfaces": {
|
"packages/interfaces": {
|
||||||
"name": "@waku/interfaces",
|
"name": "@waku/interfaces",
|
||||||
"version": "0.0.33",
|
"version": "0.0.34",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chainsafe/libp2p-gossipsub": "14.1.1",
|
"@chainsafe/libp2p-gossipsub": "14.1.1",
|
||||||
@ -37308,14 +37316,14 @@
|
|||||||
},
|
},
|
||||||
"packages/message-encryption": {
|
"packages/message-encryption": {
|
||||||
"name": "@waku/message-encryption",
|
"name": "@waku/message-encryption",
|
||||||
"version": "0.0.36",
|
"version": "0.0.37",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/secp256k1": "^1.7.1",
|
"@noble/secp256k1": "^1.7.1",
|
||||||
"@waku/core": "0.0.38",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/proto": "0.0.13",
|
"@waku/proto": "0.0.14",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"js-sha3": "^0.9.2",
|
"js-sha3": "^0.9.2",
|
||||||
"uint8arrays": "^5.0.1"
|
"uint8arrays": "^5.0.1"
|
||||||
@ -37345,7 +37353,7 @@
|
|||||||
},
|
},
|
||||||
"packages/proto": {
|
"packages/proto": {
|
||||||
"name": "@waku/proto",
|
"name": "@waku/proto",
|
||||||
"version": "0.0.13",
|
"version": "0.0.14",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"protons-runtime": "^5.4.0"
|
"protons-runtime": "^5.4.0"
|
||||||
@ -37367,16 +37375,16 @@
|
|||||||
},
|
},
|
||||||
"packages/relay": {
|
"packages/relay": {
|
||||||
"name": "@waku/relay",
|
"name": "@waku/relay",
|
||||||
"version": "0.0.21",
|
"version": "0.0.22",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chainsafe/libp2p-gossipsub": "14.1.1",
|
"@chainsafe/libp2p-gossipsub": "14.1.1",
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
"@waku/core": "0.0.38",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/proto": "0.0.13",
|
"@waku/proto": "0.0.14",
|
||||||
"@waku/sdk": "0.0.34",
|
"@waku/sdk": "0.0.35",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-check": "^3.19.0",
|
"fast-check": "^3.19.0",
|
||||||
@ -37453,14 +37461,14 @@
|
|||||||
},
|
},
|
||||||
"packages/rln": {
|
"packages/rln": {
|
||||||
"name": "@waku/rln",
|
"name": "@waku/rln",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chainsafe/bls-keystore": "3.0.0",
|
"@chainsafe/bls-keystore": "3.0.0",
|
||||||
"@noble/hashes": "^1.2.0",
|
"@noble/hashes": "^1.2.0",
|
||||||
"@waku/core": "^0.0.38",
|
"@waku/core": "^0.0.39",
|
||||||
"@waku/utils": "^0.0.26",
|
"@waku/utils": "^0.0.27",
|
||||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
"@waku/zerokit-rln-wasm": "^0.2.1",
|
||||||
"chai": "^5.1.2",
|
"chai": "^5.1.2",
|
||||||
"chai-as-promised": "^8.0.1",
|
"chai-as-promised": "^8.0.1",
|
||||||
"chai-spies": "^1.1.0",
|
"chai-spies": "^1.1.0",
|
||||||
@ -37481,8 +37489,8 @@
|
|||||||
"@types/lodash": "^4.17.15",
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@waku/build-utils": "^1.0.0",
|
"@waku/build-utils": "^1.0.0",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/message-encryption": "^0.0.36",
|
"@waku/message-encryption": "^0.0.37",
|
||||||
"deep-equal-in-any-order": "^2.0.6",
|
"deep-equal-in-any-order": "^2.0.6",
|
||||||
"fast-check": "^3.23.2",
|
"fast-check": "^3.23.2",
|
||||||
"rollup-plugin-copy": "^3.5.0"
|
"rollup-plugin-copy": "^3.5.0"
|
||||||
@ -37598,7 +37606,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk": {
|
"packages/sdk": {
|
||||||
"name": "@waku/sdk",
|
"name": "@waku/sdk",
|
||||||
"version": "0.0.34",
|
"version": "0.0.35",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chainsafe/libp2p-noise": "16.1.3",
|
"@chainsafe/libp2p-noise": "16.1.3",
|
||||||
@ -37608,12 +37616,15 @@
|
|||||||
"@libp2p/ping": "2.0.35",
|
"@libp2p/ping": "2.0.35",
|
||||||
"@libp2p/websockets": "9.2.16",
|
"@libp2p/websockets": "9.2.16",
|
||||||
"@noble/hashes": "^1.3.3",
|
"@noble/hashes": "^1.3.3",
|
||||||
"@waku/core": "0.0.38",
|
"@types/lodash.debounce": "^4.0.9",
|
||||||
"@waku/discovery": "0.0.11",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/discovery": "0.0.12",
|
||||||
"@waku/proto": "^0.0.13",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/proto": "^0.0.14",
|
||||||
"libp2p": "2.8.11"
|
"@waku/sds": "^0.0.7",
|
||||||
|
"@waku/utils": "0.0.27",
|
||||||
|
"libp2p": "2.8.11",
|
||||||
|
"lodash.debounce": "^4.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@libp2p/interface": "2.10.4",
|
"@libp2p/interface": "2.10.4",
|
||||||
@ -37624,6 +37635,7 @@
|
|||||||
"@types/chai": "^4.3.11",
|
"@types/chai": "^4.3.11",
|
||||||
"@types/mocha": "^10.0.9",
|
"@types/mocha": "^10.0.9",
|
||||||
"@waku/build-utils": "*",
|
"@waku/build-utils": "*",
|
||||||
|
"@waku/message-encryption": "^0.0.37",
|
||||||
"chai": "^5.1.1",
|
"chai": "^5.1.1",
|
||||||
"cspell": "^8.6.1",
|
"cspell": "^8.6.1",
|
||||||
"interface-datastore": "8.3.2",
|
"interface-datastore": "8.3.2",
|
||||||
@ -37644,6 +37656,102 @@
|
|||||||
"@sinonjs/commons": "^3.0.1"
|
"@sinonjs/commons": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/@waku/interfaces": {
|
||||||
|
"version": "0.0.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.32.tgz",
|
||||||
|
"integrity": "sha512-4MNfc7ZzQCyQZR1GQQKPgHaWTuPTIvE2wo/b7iokjdeOT+ZSKyJFSetcV07cqnBwyzUv1gc53bJdzyHwVIa5Vw==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/@waku/proto": {
|
||||||
|
"version": "0.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.12.tgz",
|
||||||
|
"integrity": "sha512-JR7wiy3Di628Ywo9qKIi7rhfdC2K7ABoaWa9WX4ZQKieYDs+YwOK+syE53VNwXrtponNeLDI0JIOFzRDalUm1A==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"protons-runtime": "^5.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/@waku/utils": {
|
||||||
|
"version": "0.0.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.25.tgz",
|
||||||
|
"integrity": "sha512-yCbfQ3uqByGNUvCNTj6oHi8fJ6BdVvg+Rj0y2YKrZDSNn73uTMF856lCJdsE86eqDZNCDaRaawTs3ZNEXyWaXw==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^1.3.2",
|
||||||
|
"@waku/interfaces": "0.0.32",
|
||||||
|
"chai": "^4.3.10",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"uint8arrays": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/assertion-error": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/check-error": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-func-name": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/deep-eql": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"type-detect": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/loupe": {
|
||||||
|
"version": "2.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
|
||||||
|
"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-func-name": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/sdk/node_modules/@waku/sds/node_modules/pathval": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/sdk/node_modules/assertion-error": {
|
"packages/sdk/node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -37723,13 +37831,13 @@
|
|||||||
},
|
},
|
||||||
"packages/sds": {
|
"packages/sds": {
|
||||||
"name": "@waku/sds",
|
"name": "@waku/sds",
|
||||||
"version": "0.0.6",
|
"version": "0.0.7",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@libp2p/interface": "2.10.4",
|
"@libp2p/interface": "2.10.4",
|
||||||
"@noble/hashes": "^1.7.1",
|
"@noble/hashes": "^1.7.1",
|
||||||
"@waku/proto": "^0.0.13",
|
"@waku/proto": "^0.0.14",
|
||||||
"@waku/utils": "^0.0.26",
|
"@waku/utils": "^0.0.27",
|
||||||
"chai": "^5.1.2",
|
"chai": "^5.1.2",
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
@ -37849,11 +37957,11 @@
|
|||||||
},
|
},
|
||||||
"packages/utils": {
|
"packages/utils": {
|
||||||
"name": "@waku/utils",
|
"name": "@waku/utils",
|
||||||
"version": "0.0.26",
|
"version": "0.0.27",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"uint8arrays": "^5.0.1"
|
"uint8arrays": "^5.0.1"
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/discovery",
|
"packages/discovery",
|
||||||
"packages/message-encryption",
|
"packages/message-encryption",
|
||||||
"packages/sdk",
|
|
||||||
"packages/relay",
|
|
||||||
"packages/sds",
|
"packages/sds",
|
||||||
"packages/rln",
|
"packages/rln",
|
||||||
|
"packages/sdk",
|
||||||
|
"packages/relay",
|
||||||
"packages/tests",
|
"packages/tests",
|
||||||
"packages/reliability-tests",
|
"packages/reliability-tests",
|
||||||
"packages/headless-tests",
|
"packages/headless-tests",
|
||||||
|
|||||||
@ -69,7 +69,8 @@ test.describe("waku", () => {
|
|||||||
console.log("Debug:", debug);
|
console.log("Debug:", debug);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can dial peers", async ({ page }) => {
|
// TODO: https://github.com/waku-org/js-waku/issues/2619
|
||||||
|
test.skip("can dial peers", async ({ page }) => {
|
||||||
const result = await page.evaluate((peerAddrs) => {
|
const result = await page.evaluate((peerAddrs) => {
|
||||||
return window.wakuAPI.dialPeers(window.waku, peerAddrs);
|
return window.wakuAPI.dialPeers(window.waku, peerAddrs);
|
||||||
}, ACTIVE_PEERS);
|
}, ACTIVE_PEERS);
|
||||||
|
|||||||
@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
|||||||
The file is maintained by [Release Please](https://github.com/googleapis/release-please) based on [Conventional Commits](https://www.conventionalcommits.org) specification,
|
The file is maintained by [Release Please](https://github.com/googleapis/release-please) based on [Conventional Commits](https://www.conventionalcommits.org) specification,
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.0.39](https://github.com/waku-org/js-waku/compare/core-v0.0.38...core-v0.0.39) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add start/stop to filter ([#2592](https://github.com/waku-org/js-waku/issues/2592)) ([2fba052](https://github.com/waku-org/js-waku/commit/2fba052b8b98cb64f6383de95d01b33beb771448))
|
||||||
|
* Expose message hash from IDecodedMessage ([#2578](https://github.com/waku-org/js-waku/issues/2578)) ([836d6b8](https://github.com/waku-org/js-waku/commit/836d6b8793a5124747684f6ea76b6dd47c73048b))
|
||||||
|
* Implement lp-v3 error codes with backwards compatibility ([#2501](https://github.com/waku-org/js-waku/issues/2501)) ([1625302](https://github.com/waku-org/js-waku/commit/16253026c6e30052d87d9975b58480951de469d8))
|
||||||
|
* Implement peer-store re-bootstrapping ([#2641](https://github.com/waku-org/js-waku/issues/2641)) ([11d84ad](https://github.com/waku-org/js-waku/commit/11d84ad342fe45158ef0734f9ca070f14704503f))
|
||||||
|
* StoreConnect events ([#2601](https://github.com/waku-org/js-waku/issues/2601)) ([0dfbcf6](https://github.com/waku-org/js-waku/commit/0dfbcf6b6bd9225dcb0dec540aeb1eb2703c8397))
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/enr bumped from ^0.0.32 to ^0.0.33
|
||||||
|
* @waku/interfaces bumped from 0.0.33 to 0.0.34
|
||||||
|
* @waku/proto bumped from 0.0.13 to 0.0.14
|
||||||
|
* @waku/utils bumped from 0.0.26 to 0.0.27
|
||||||
|
|
||||||
## [0.0.38](https://github.com/waku-org/js-waku/compare/core-v0.0.37...core-v0.0.38) (2025-08-14)
|
## [0.0.38](https://github.com/waku-org/js-waku/compare/core-v0.0.37...core-v0.0.38) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/core",
|
"name": "@waku/core",
|
||||||
"version": "0.0.38",
|
"version": "0.0.39",
|
||||||
"description": "TypeScript implementation of the Waku v2 protocol",
|
"description": "TypeScript implementation of the Waku v2 protocol",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@ -64,11 +64,11 @@
|
|||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@waku/enr": "^0.0.32",
|
"@waku/enr": "^0.0.33",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@libp2p/ping": "2.0.35",
|
"@libp2p/ping": "2.0.35",
|
||||||
"@waku/proto": "0.0.13",
|
"@waku/proto": "0.0.14",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
"it-all": "^3.0.4",
|
"it-all": "^3.0.4",
|
||||||
|
|||||||
@ -10,7 +10,11 @@ export * as waku_filter from "./lib/filter/index.js";
|
|||||||
export { FilterCore, FilterCodecs } from "./lib/filter/index.js";
|
export { FilterCore, FilterCodecs } from "./lib/filter/index.js";
|
||||||
|
|
||||||
export * as waku_light_push from "./lib/light_push/index.js";
|
export * as waku_light_push from "./lib/light_push/index.js";
|
||||||
export { LightPushCodec, LightPushCore } from "./lib/light_push/index.js";
|
export {
|
||||||
|
LightPushCore,
|
||||||
|
LightPushCodec,
|
||||||
|
LightPushCodecV2
|
||||||
|
} from "./lib/light_push/index.js";
|
||||||
|
|
||||||
export * as waku_store from "./lib/store/index.js";
|
export * as waku_store from "./lib/store/index.js";
|
||||||
export { StoreCore, StoreCodec } from "./lib/store/index.js";
|
export { StoreCore, StoreCodec } from "./lib/store/index.js";
|
||||||
|
|||||||
@ -87,6 +87,12 @@ describe("ConnectionLimiter", () => {
|
|||||||
mockPeer2 = createMockPeer("12D3KooWTest2", [Tags.BOOTSTRAP]); // Ensure mockPeer2 is prioritized and dialed
|
mockPeer2 = createMockPeer("12D3KooWTest2", [Tags.BOOTSTRAP]); // Ensure mockPeer2 is prioritized and dialed
|
||||||
mockConnection = createMockConnection(mockPeerId, [Tags.BOOTSTRAP]);
|
mockConnection = createMockConnection(mockPeerId, [Tags.BOOTSTRAP]);
|
||||||
|
|
||||||
|
dialer = {
|
||||||
|
start: sinon.stub(),
|
||||||
|
stop: sinon.stub(),
|
||||||
|
dial: sinon.stub().resolves()
|
||||||
|
} as unknown as sinon.SinonStubbedInstance<Dialer>;
|
||||||
|
|
||||||
libp2p = {
|
libp2p = {
|
||||||
addEventListener: sinon.stub(),
|
addEventListener: sinon.stub(),
|
||||||
removeEventListener: sinon.stub(),
|
removeEventListener: sinon.stub(),
|
||||||
@ -95,7 +101,11 @@ describe("ConnectionLimiter", () => {
|
|||||||
getConnections: sinon.stub().returns([]),
|
getConnections: sinon.stub().returns([]),
|
||||||
peerStore: {
|
peerStore: {
|
||||||
all: sinon.stub().resolves([]),
|
all: sinon.stub().resolves([]),
|
||||||
get: sinon.stub().resolves(mockPeer)
|
get: sinon.stub().resolves(mockPeer),
|
||||||
|
merge: sinon.stub().resolves()
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
components: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,6 +122,20 @@ describe("ConnectionLimiter", () => {
|
|||||||
isConnected: sinon.stub().returns(true),
|
isConnected: sinon.stub().returns(true),
|
||||||
isP2PConnected: sinon.stub().returns(true)
|
isP2PConnected: sinon.stub().returns(true)
|
||||||
} as unknown as sinon.SinonStubbedInstance<NetworkMonitor>;
|
} as unknown as sinon.SinonStubbedInstance<NetworkMonitor>;
|
||||||
|
|
||||||
|
// Mock the libp2p components needed by isAddressesSupported
|
||||||
|
libp2p.components = {
|
||||||
|
components: {},
|
||||||
|
transportManager: {
|
||||||
|
getTransports: sinon.stub().returns([
|
||||||
|
{
|
||||||
|
dialFilter: sinon
|
||||||
|
.stub()
|
||||||
|
.returns([multiaddr("/dns4/test/tcp/443/wss")])
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -274,11 +298,6 @@ describe("ConnectionLimiter", () => {
|
|||||||
|
|
||||||
describe("dialPeersFromStore", () => {
|
describe("dialPeersFromStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dialer = {
|
|
||||||
start: sinon.stub(),
|
|
||||||
stop: sinon.stub(),
|
|
||||||
dial: sinon.stub().resolves()
|
|
||||||
} as unknown as sinon.SinonStubbedInstance<Dialer>;
|
|
||||||
libp2p.hangUp = sinon.stub().resolves();
|
libp2p.hangUp = sinon.stub().resolves();
|
||||||
connectionLimiter = createLimiter();
|
connectionLimiter = createLimiter();
|
||||||
mockPeer.addresses = [
|
mockPeer.addresses = [
|
||||||
@ -404,11 +423,6 @@ describe("ConnectionLimiter", () => {
|
|||||||
|
|
||||||
describe("maintainConnectionsCount", () => {
|
describe("maintainConnectionsCount", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dialer = {
|
|
||||||
start: sinon.stub(),
|
|
||||||
stop: sinon.stub(),
|
|
||||||
dial: sinon.stub().resolves()
|
|
||||||
} as unknown as sinon.SinonStubbedInstance<Dialer>;
|
|
||||||
libp2p.hangUp = sinon.stub().resolves();
|
libp2p.hangUp = sinon.stub().resolves();
|
||||||
connectionLimiter = createLimiter({ maxConnections: 2 });
|
connectionLimiter = createLimiter({ maxConnections: 2 });
|
||||||
mockPeer.addresses = [
|
mockPeer.addresses = [
|
||||||
@ -515,6 +529,7 @@ describe("ConnectionLimiter", () => {
|
|||||||
];
|
];
|
||||||
libp2p.peerStore.all.resolves([bootstrapPeer, pxPeer, localPeer]);
|
libp2p.peerStore.all.resolves([bootstrapPeer, pxPeer, localPeer]);
|
||||||
libp2p.getConnections.returns([]);
|
libp2p.getConnections.returns([]);
|
||||||
|
connectionLimiter = createLimiter();
|
||||||
const peers = await (connectionLimiter as any).getPrioritizedPeers();
|
const peers = await (connectionLimiter as any).getPrioritizedPeers();
|
||||||
expect(peers[0].id.toString()).to.equal("b");
|
expect(peers[0].id.toString()).to.equal("b");
|
||||||
expect(peers[1].id.toString()).to.equal("px");
|
expect(peers[1].id.toString()).to.equal("px");
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { Logger } from "@waku/utils";
|
|||||||
|
|
||||||
import { Dialer } from "./dialer.js";
|
import { Dialer } from "./dialer.js";
|
||||||
import { NetworkMonitor } from "./network_monitor.js";
|
import { NetworkMonitor } from "./network_monitor.js";
|
||||||
|
import { isAddressesSupported } from "./utils.js";
|
||||||
|
|
||||||
const log = new Logger("connection-limiter");
|
const log = new Logger("connection-limiter");
|
||||||
|
|
||||||
@ -145,13 +146,15 @@ export class ConnectionLimiter implements IConnectionLimiter {
|
|||||||
const peers = await this.getPrioritizedPeers();
|
const peers = await this.getPrioritizedPeers();
|
||||||
|
|
||||||
if (peers.length === 0) {
|
if (peers.length === 0) {
|
||||||
log.info(`No peers to dial, node is utilizing all known peers`);
|
log.info(`No peers to dial, skipping`);
|
||||||
|
await this.triggerBootstrap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = peers
|
const promises = peers
|
||||||
.slice(0, this.options.maxConnections - connections.length)
|
.slice(0, this.options.maxConnections - connections.length)
|
||||||
.map((p) => this.dialer.dial(p.id));
|
.map((p) => this.dialer.dial(p.id));
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -218,6 +221,7 @@ export class ConnectionLimiter implements IConnectionLimiter {
|
|||||||
|
|
||||||
if (peers.length === 0) {
|
if (peers.length === 0) {
|
||||||
log.info(`No peers to dial, skipping`);
|
log.info(`No peers to dial, skipping`);
|
||||||
|
await this.triggerBootstrap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,6 +244,9 @@ export class ConnectionLimiter implements IConnectionLimiter {
|
|||||||
private async getPrioritizedPeers(): Promise<Peer[]> {
|
private async getPrioritizedPeers(): Promise<Peer[]> {
|
||||||
const allPeers = await this.libp2p.peerStore.all();
|
const allPeers = await this.libp2p.peerStore.all();
|
||||||
const allConnections = this.libp2p.getConnections();
|
const allConnections = this.libp2p.getConnections();
|
||||||
|
const allConnectionsSet = new Set(
|
||||||
|
allConnections.map((c) => c.remotePeer.toString())
|
||||||
|
);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`Found ${allPeers.length} peers in store, and found ${allConnections.length} connections`
|
`Found ${allPeers.length} peers in store, and found ${allConnections.length} connections`
|
||||||
@ -247,11 +254,10 @@ export class ConnectionLimiter implements IConnectionLimiter {
|
|||||||
|
|
||||||
const notConnectedPeers = allPeers.filter(
|
const notConnectedPeers = allPeers.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
!allConnections.some((c) => c.remotePeer.equals(p.id)) &&
|
!allConnectionsSet.has(p.id.toString()) &&
|
||||||
p.addresses.some(
|
isAddressesSupported(
|
||||||
(a) =>
|
this.libp2p,
|
||||||
a.multiaddr.toString().includes("wss") ||
|
p.addresses.map((a) => a.multiaddr)
|
||||||
a.multiaddr.toString().includes("ws")
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -267,7 +273,19 @@ export class ConnectionLimiter implements IConnectionLimiter {
|
|||||||
p.tags.has(Tags.PEER_CACHE)
|
p.tags.has(Tags.PEER_CACHE)
|
||||||
);
|
);
|
||||||
|
|
||||||
return [...bootstrapPeers, ...peerExchangePeers, ...localStorePeers];
|
const restPeers = notConnectedPeers.filter(
|
||||||
|
(p) =>
|
||||||
|
!p.tags.has(Tags.BOOTSTRAP) &&
|
||||||
|
!p.tags.has(Tags.PEER_EXCHANGE) &&
|
||||||
|
!p.tags.has(Tags.PEER_CACHE)
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
...bootstrapPeers,
|
||||||
|
...peerExchangePeers,
|
||||||
|
...localStorePeers,
|
||||||
|
...restPeers
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBootstrapPeers(): Promise<Peer[]> {
|
private async getBootstrapPeers(): Promise<Peer[]> {
|
||||||
@ -291,4 +309,41 @@ export class ConnectionLimiter implements IConnectionLimiter {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the bootstrap or peer cache discovery if they are mounted.
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
private async triggerBootstrap(): Promise<void> {
|
||||||
|
log.info("Triggering bootstrap discovery");
|
||||||
|
|
||||||
|
const bootstrapComponents = Object.values(this.libp2p.components.components)
|
||||||
|
.filter((c) => !!c)
|
||||||
|
.filter((c: unknown) =>
|
||||||
|
[`@waku/${Tags.BOOTSTRAP}`, `@waku/${Tags.PEER_CACHE}`].includes(
|
||||||
|
(c as { [Symbol.toStringTag]: string })?.[Symbol.toStringTag]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (bootstrapComponents.length === 0) {
|
||||||
|
log.warn("No bootstrap components found to trigger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`Found ${bootstrapComponents.length} bootstrap components, starting them`
|
||||||
|
);
|
||||||
|
|
||||||
|
const promises = bootstrapComponents.map(async (component) => {
|
||||||
|
try {
|
||||||
|
await (component as { stop: () => Promise<void> })?.stop?.();
|
||||||
|
await (component as { start: () => Promise<void> })?.start?.();
|
||||||
|
log.info("Successfully started bootstrap component");
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to start bootstrap component", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,12 @@ describe("ConnectionManager", () => {
|
|||||||
dialProtocol: sinon.stub().resolves({} as Stream),
|
dialProtocol: sinon.stub().resolves({} as Stream),
|
||||||
hangUp: sinon.stub().resolves(),
|
hangUp: sinon.stub().resolves(),
|
||||||
getPeers: sinon.stub().returns([]),
|
getPeers: sinon.stub().returns([]),
|
||||||
|
getConnections: sinon.stub().returns([]),
|
||||||
|
addEventListener: sinon.stub(),
|
||||||
|
removeEventListener: sinon.stub(),
|
||||||
|
components: {
|
||||||
|
components: {}
|
||||||
|
},
|
||||||
peerStore: {
|
peerStore: {
|
||||||
get: sinon.stub().resolves(null),
|
get: sinon.stub().resolves(null),
|
||||||
merge: sinon.stub().resolves()
|
merge: sinon.stub().resolves()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { isPeerId, type Peer, type PeerId } from "@libp2p/interface";
|
import { isPeerId, type Peer, type PeerId } from "@libp2p/interface";
|
||||||
import { peerIdFromString } from "@libp2p/peer-id";
|
import { peerIdFromString } from "@libp2p/peer-id";
|
||||||
import { Multiaddr, multiaddr, MultiaddrInput } from "@multiformats/multiaddr";
|
import { Multiaddr, multiaddr, MultiaddrInput } from "@multiformats/multiaddr";
|
||||||
|
import { Libp2p } from "@waku/interfaces";
|
||||||
import { bytesToUtf8 } from "@waku/utils/bytes";
|
import { bytesToUtf8 } from "@waku/utils/bytes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,3 +50,25 @@ export const mapToPeerId = (input: PeerId | MultiaddrInput): PeerId => {
|
|||||||
? input
|
? input
|
||||||
: peerIdFromString(multiaddr(input).getPeerId()!);
|
: peerIdFromString(multiaddr(input).getPeerId()!);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the address is supported by the libp2p instance.
|
||||||
|
* @param libp2p - The libp2p instance.
|
||||||
|
* @param addresses - The addresses to check.
|
||||||
|
* @returns True if the addresses are supported, false otherwise.
|
||||||
|
*/
|
||||||
|
export const isAddressesSupported = (
|
||||||
|
libp2p: Libp2p,
|
||||||
|
addresses: Multiaddr[]
|
||||||
|
): boolean => {
|
||||||
|
const transports =
|
||||||
|
libp2p?.components?.transportManager?.getTransports() || [];
|
||||||
|
|
||||||
|
if (transports.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transports
|
||||||
|
.map((transport) => transport.dialFilter(addresses))
|
||||||
|
.some((supportedAddresses) => supportedAddresses.length > 0);
|
||||||
|
};
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import type { PeerId } from "@libp2p/interface";
|
|||||||
import type { IncomingStreamData } from "@libp2p/interface-internal";
|
import type { IncomingStreamData } from "@libp2p/interface-internal";
|
||||||
import {
|
import {
|
||||||
type ContentTopic,
|
type ContentTopic,
|
||||||
type CoreProtocolResult,
|
type FilterCoreResult,
|
||||||
|
FilterError,
|
||||||
type Libp2p,
|
type Libp2p,
|
||||||
ProtocolError,
|
|
||||||
type PubsubTopic
|
type PubsubTopic
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { WakuMessage } from "@waku/proto";
|
import { WakuMessage } from "@waku/proto";
|
||||||
@ -42,34 +42,44 @@ export class FilterCore {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private handleIncomingMessage: IncomingMessageHandler,
|
private handleIncomingMessage: IncomingMessageHandler,
|
||||||
libp2p: Libp2p
|
private libp2p: Libp2p
|
||||||
) {
|
) {
|
||||||
this.streamManager = new StreamManager(
|
this.streamManager = new StreamManager(
|
||||||
FilterCodecs.SUBSCRIBE,
|
FilterCodecs.SUBSCRIBE,
|
||||||
libp2p.components
|
libp2p.components
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
libp2p
|
public async start(): Promise<void> {
|
||||||
.handle(FilterCodecs.PUSH, this.onRequest.bind(this), {
|
try {
|
||||||
|
await this.libp2p.handle(FilterCodecs.PUSH, this.onRequest.bind(this), {
|
||||||
maxInboundStreams: 100
|
maxInboundStreams: 100
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
log.error("Failed to register ", FilterCodecs.PUSH, e);
|
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Failed to register ", FilterCodecs.PUSH, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.libp2p.unhandle(FilterCodecs.PUSH);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Failed to unregister ", FilterCodecs.PUSH, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async subscribe(
|
public async subscribe(
|
||||||
pubsubTopic: PubsubTopic,
|
pubsubTopic: PubsubTopic,
|
||||||
peerId: PeerId,
|
peerId: PeerId,
|
||||||
contentTopics: ContentTopic[]
|
contentTopics: ContentTopic[]
|
||||||
): Promise<CoreProtocolResult> {
|
): Promise<FilterCoreResult> {
|
||||||
const stream = await this.streamManager.getStream(peerId);
|
const stream = await this.streamManager.getStream(peerId);
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.NO_STREAM_AVAILABLE,
|
error: FilterError.NO_STREAM_AVAILABLE,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -98,7 +108,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.GENERIC_FAIL,
|
error: FilterError.GENERIC_FAIL,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -113,7 +123,7 @@ export class FilterCore {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.REMOTE_PEER_REJECTED,
|
error: FilterError.REMOTE_PEER_REJECTED,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
},
|
},
|
||||||
success: null
|
success: null
|
||||||
@ -130,7 +140,7 @@ export class FilterCore {
|
|||||||
pubsubTopic: PubsubTopic,
|
pubsubTopic: PubsubTopic,
|
||||||
peerId: PeerId,
|
peerId: PeerId,
|
||||||
contentTopics: ContentTopic[]
|
contentTopics: ContentTopic[]
|
||||||
): Promise<CoreProtocolResult> {
|
): Promise<FilterCoreResult> {
|
||||||
const stream = await this.streamManager.getStream(peerId);
|
const stream = await this.streamManager.getStream(peerId);
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
@ -138,7 +148,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.NO_STREAM_AVAILABLE,
|
error: FilterError.NO_STREAM_AVAILABLE,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -156,7 +166,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.GENERIC_FAIL,
|
error: FilterError.GENERIC_FAIL,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -171,7 +181,7 @@ export class FilterCore {
|
|||||||
public async unsubscribeAll(
|
public async unsubscribeAll(
|
||||||
pubsubTopic: PubsubTopic,
|
pubsubTopic: PubsubTopic,
|
||||||
peerId: PeerId
|
peerId: PeerId
|
||||||
): Promise<CoreProtocolResult> {
|
): Promise<FilterCoreResult> {
|
||||||
const stream = await this.streamManager.getStream(peerId);
|
const stream = await this.streamManager.getStream(peerId);
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
@ -179,7 +189,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.NO_STREAM_AVAILABLE,
|
error: FilterError.NO_STREAM_AVAILABLE,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -198,7 +208,7 @@ export class FilterCore {
|
|||||||
if (!res || !res.length) {
|
if (!res || !res.length) {
|
||||||
return {
|
return {
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.NO_RESPONSE,
|
error: FilterError.NO_RESPONSE,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
},
|
},
|
||||||
success: null
|
success: null
|
||||||
@ -214,7 +224,7 @@ export class FilterCore {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.REMOTE_PEER_REJECTED,
|
error: FilterError.REMOTE_PEER_REJECTED,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
},
|
},
|
||||||
success: null
|
success: null
|
||||||
@ -227,7 +237,7 @@ export class FilterCore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ping(peerId: PeerId): Promise<CoreProtocolResult> {
|
public async ping(peerId: PeerId): Promise<FilterCoreResult> {
|
||||||
const stream = await this.streamManager.getStream(peerId);
|
const stream = await this.streamManager.getStream(peerId);
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
@ -235,7 +245,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.NO_STREAM_AVAILABLE,
|
error: FilterError.NO_STREAM_AVAILABLE,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -257,7 +267,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.GENERIC_FAIL,
|
error: FilterError.GENERIC_FAIL,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -267,7 +277,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.NO_RESPONSE,
|
error: FilterError.NO_RESPONSE,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -283,7 +293,7 @@ export class FilterCore {
|
|||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.REMOTE_PEER_REJECTED,
|
error: FilterError.REMOTE_PEER_REJECTED,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
7
packages/core/src/lib/light_push/constants.ts
Normal file
7
packages/core/src/lib/light_push/constants.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const CODECS = {
|
||||||
|
v2: "/vac/waku/lightpush/2.0.0-beta1",
|
||||||
|
v3: "/vac/waku/lightpush/3.0.0"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const LightPushCodecV2 = CODECS.v2;
|
||||||
|
export const LightPushCodec = CODECS.v3;
|
||||||
@ -1 +1,2 @@
|
|||||||
export { LightPushCore, LightPushCodec, PushResponse } from "./light_push.js";
|
export { LightPushCore } from "./light_push.js";
|
||||||
|
export { LightPushCodec, LightPushCodecV2 } from "./constants.js";
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import type { PeerId } from "@libp2p/interface";
|
import type { PeerId, Stream } from "@libp2p/interface";
|
||||||
import {
|
import {
|
||||||
type CoreProtocolResult,
|
|
||||||
type IEncoder,
|
type IEncoder,
|
||||||
type IMessage,
|
type IMessage,
|
||||||
type Libp2p,
|
type Libp2p,
|
||||||
ProtocolError,
|
type LightPushCoreResult,
|
||||||
type ThisOrThat
|
LightPushError
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { PushResponse } from "@waku/proto";
|
|
||||||
import { isMessageSizeUnderCap } from "@waku/utils";
|
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
import all from "it-all";
|
import all from "it-all";
|
||||||
import * as lp from "it-length-prefixed";
|
import * as lp from "it-length-prefixed";
|
||||||
@ -17,92 +14,71 @@ import { Uint8ArrayList } from "uint8arraylist";
|
|||||||
|
|
||||||
import { StreamManager } from "../stream_manager/index.js";
|
import { StreamManager } from "../stream_manager/index.js";
|
||||||
|
|
||||||
import { PushRpc } from "./push_rpc.js";
|
import { CODECS } from "./constants.js";
|
||||||
import { isRLNResponseError } from "./utils.js";
|
import { ProtocolHandler } from "./protocol_handler.js";
|
||||||
|
|
||||||
const log = new Logger("light-push");
|
const log = new Logger("light-push");
|
||||||
|
|
||||||
export const LightPushCodec = "/vac/waku/lightpush/2.0.0-beta1";
|
|
||||||
export { PushResponse };
|
|
||||||
|
|
||||||
type PreparePushMessageResult = ThisOrThat<"query", PushRpc>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the [Waku v2 Light Push protocol](https://rfc.vac.dev/spec/19/).
|
* Implements the [Waku v2 Light Push protocol](https://rfc.vac.dev/spec/19/).
|
||||||
*/
|
*/
|
||||||
export class LightPushCore {
|
export class LightPushCore {
|
||||||
private readonly streamManager: StreamManager;
|
private readonly streamManager: StreamManager;
|
||||||
|
private readonly streamManagerV2: StreamManager;
|
||||||
|
|
||||||
public readonly multicodec = LightPushCodec;
|
public readonly multicodec = [CODECS.v3, CODECS.v2];
|
||||||
|
|
||||||
public constructor(libp2p: Libp2p) {
|
public constructor(private libp2p: Libp2p) {
|
||||||
this.streamManager = new StreamManager(LightPushCodec, libp2p.components);
|
this.streamManagerV2 = new StreamManager(CODECS.v2, libp2p.components);
|
||||||
}
|
this.streamManager = new StreamManager(CODECS.v3, libp2p.components);
|
||||||
|
|
||||||
private async preparePushMessage(
|
|
||||||
encoder: IEncoder,
|
|
||||||
message: IMessage
|
|
||||||
): Promise<PreparePushMessageResult> {
|
|
||||||
try {
|
|
||||||
if (!message.payload || message.payload.length === 0) {
|
|
||||||
log.error("Failed to send waku light push: payload is empty");
|
|
||||||
return { query: null, error: ProtocolError.EMPTY_PAYLOAD };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await isMessageSizeUnderCap(encoder, message))) {
|
|
||||||
log.error("Failed to send waku light push: message is bigger than 1MB");
|
|
||||||
return { query: null, error: ProtocolError.SIZE_TOO_BIG };
|
|
||||||
}
|
|
||||||
|
|
||||||
const protoMessage = await encoder.toProtoObj(message);
|
|
||||||
if (!protoMessage) {
|
|
||||||
log.error("Failed to encode to protoMessage, aborting push");
|
|
||||||
return {
|
|
||||||
query: null,
|
|
||||||
error: ProtocolError.ENCODE_FAILED
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = PushRpc.createRequest(protoMessage, encoder.pubsubTopic);
|
|
||||||
return { query, error: null };
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Failed to prepare push message", error);
|
|
||||||
|
|
||||||
return {
|
|
||||||
query: null,
|
|
||||||
error: ProtocolError.GENERIC_FAIL
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(
|
public async send(
|
||||||
encoder: IEncoder,
|
encoder: IEncoder,
|
||||||
message: IMessage,
|
message: IMessage,
|
||||||
peerId: PeerId
|
peerId: PeerId,
|
||||||
): Promise<CoreProtocolResult> {
|
useLegacy: boolean = false
|
||||||
const { query, error: preparationError } = await this.preparePushMessage(
|
): Promise<LightPushCoreResult> {
|
||||||
encoder,
|
const protocol = await this.getProtocol(peerId, useLegacy);
|
||||||
message
|
|
||||||
|
log.info(
|
||||||
|
`Sending light push request to peer:${peerId.toString()}, protocol:${protocol}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (preparationError || !query) {
|
if (!protocol) {
|
||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: preparationError,
|
error: LightPushError.GENERIC_FAIL,
|
||||||
peerId
|
peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = await this.streamManager.getStream(peerId);
|
const { rpc, error: prepError } = await ProtocolHandler.preparePushMessage(
|
||||||
|
encoder,
|
||||||
|
message,
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prepError) {
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error: prepError,
|
||||||
|
peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = await this.getStream(peerId, protocol);
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
log.error(`Failed to get a stream for remote peer:${peerId.toString()}`);
|
log.error(`Failed to get a stream for remote peer:${peerId.toString()}`);
|
||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.NO_STREAM_AVAILABLE,
|
error: LightPushError.NO_STREAM_AVAILABLE,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -111,76 +87,74 @@ export class LightPushCore {
|
|||||||
let res: Uint8ArrayList[] | undefined;
|
let res: Uint8ArrayList[] | undefined;
|
||||||
try {
|
try {
|
||||||
res = await pipe(
|
res = await pipe(
|
||||||
[query.encode()],
|
[rpc.encode()],
|
||||||
lp.encode,
|
lp.encode,
|
||||||
stream,
|
stream,
|
||||||
lp.decode,
|
lp.decode,
|
||||||
async (source) => await all(source)
|
async (source) => await all(source)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// can fail only because of `stream` abortion
|
|
||||||
log.error("Failed to send waku light push request", err);
|
log.error("Failed to send waku light push request", err);
|
||||||
return {
|
return {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
error: ProtocolError.STREAM_ABORTED,
|
error: LightPushError.STREAM_ABORTED,
|
||||||
peerId: peerId
|
peerId: peerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const bytes = new Uint8ArrayList();
|
const bytes = new Uint8ArrayList();
|
||||||
res.forEach((chunk) => {
|
res.forEach((chunk) => bytes.append(chunk));
|
||||||
bytes.append(chunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
let response: PushResponse | undefined;
|
if (bytes.length === 0) {
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error: LightPushError.NO_RESPONSE,
|
||||||
|
peerId: peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProtocolHandler.handleResponse(bytes, protocol, peerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProtocol(
|
||||||
|
peerId: PeerId,
|
||||||
|
useLegacy: boolean
|
||||||
|
): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
response = PushRpc.decode(bytes).response;
|
const peer = await this.libp2p.peerStore.get(peerId);
|
||||||
} catch (err) {
|
|
||||||
log.error("Failed to decode push reply", err);
|
|
||||||
return {
|
|
||||||
success: null,
|
|
||||||
failure: {
|
|
||||||
error: ProtocolError.DECODE_FAILED,
|
|
||||||
peerId: peerId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response) {
|
if (
|
||||||
log.error("Remote peer fault: No response in PushRPC");
|
useLegacy ||
|
||||||
return {
|
(!peer.protocols.includes(CODECS.v3) &&
|
||||||
success: null,
|
peer.protocols.includes(CODECS.v2))
|
||||||
failure: {
|
) {
|
||||||
error: ProtocolError.NO_RESPONSE,
|
return CODECS.v2;
|
||||||
peerId: peerId
|
} else if (peer.protocols.includes(CODECS.v3)) {
|
||||||
}
|
return CODECS.v3;
|
||||||
};
|
} else {
|
||||||
|
throw new Error("No supported protocol found");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to get protocol", error);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isRLNResponseError(response.info)) {
|
private async getStream(
|
||||||
log.error("Remote peer fault: RLN generation");
|
peerId: PeerId,
|
||||||
return {
|
protocol: string
|
||||||
success: null,
|
): Promise<Stream | undefined> {
|
||||||
failure: {
|
switch (protocol) {
|
||||||
error: ProtocolError.RLN_PROOF_GENERATION,
|
case CODECS.v2:
|
||||||
peerId: peerId
|
return this.streamManagerV2.getStream(peerId);
|
||||||
}
|
case CODECS.v3:
|
||||||
};
|
return this.streamManager.getStream(peerId);
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.isSuccess) {
|
|
||||||
log.error("Remote peer rejected the message: ", response.info);
|
|
||||||
return {
|
|
||||||
success: null,
|
|
||||||
failure: {
|
|
||||||
error: ProtocolError.REMOTE_PEER_REJECTED,
|
|
||||||
peerId: peerId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: peerId, failure: null };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
191
packages/core/src/lib/light_push/protocol_handler.ts
Normal file
191
packages/core/src/lib/light_push/protocol_handler.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import type { PeerId } from "@libp2p/interface";
|
||||||
|
import type { IEncoder, IMessage, LightPushCoreResult } from "@waku/interfaces";
|
||||||
|
import { LightPushError, LightPushStatusCode } from "@waku/interfaces";
|
||||||
|
import { PushResponse, WakuMessage } from "@waku/proto";
|
||||||
|
import { isMessageSizeUnderCap, Logger } from "@waku/utils";
|
||||||
|
import { Uint8ArrayList } from "uint8arraylist";
|
||||||
|
|
||||||
|
import { CODECS } from "./constants.js";
|
||||||
|
import { PushRpcV2 } from "./push_rpc.js";
|
||||||
|
import { PushRpc } from "./push_rpc_v3.js";
|
||||||
|
import { isRLNResponseError } from "./utils.js";
|
||||||
|
|
||||||
|
type VersionedPushRpc =
|
||||||
|
| ({ version: "v2" } & PushRpcV2)
|
||||||
|
| ({ version: "v3" } & PushRpc);
|
||||||
|
|
||||||
|
type PreparePushMessageResult =
|
||||||
|
| { rpc: VersionedPushRpc; error: null }
|
||||||
|
| { rpc: null; error: LightPushError };
|
||||||
|
|
||||||
|
const log = new Logger("light-push:protocol-handler");
|
||||||
|
|
||||||
|
export class ProtocolHandler {
|
||||||
|
public static async preparePushMessage(
|
||||||
|
encoder: IEncoder,
|
||||||
|
message: IMessage,
|
||||||
|
protocol: string
|
||||||
|
): Promise<PreparePushMessageResult> {
|
||||||
|
try {
|
||||||
|
if (!message.payload || message.payload.length === 0) {
|
||||||
|
log.error("Failed to send waku light push: payload is empty");
|
||||||
|
return { rpc: null, error: LightPushError.EMPTY_PAYLOAD };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await isMessageSizeUnderCap(encoder, message))) {
|
||||||
|
log.error("Failed to send waku light push: message is bigger than 1MB");
|
||||||
|
return { rpc: null, error: LightPushError.SIZE_TOO_BIG };
|
||||||
|
}
|
||||||
|
|
||||||
|
const protoMessage = await encoder.toProtoObj(message);
|
||||||
|
if (!protoMessage) {
|
||||||
|
log.error("Failed to encode to protoMessage, aborting push");
|
||||||
|
return { rpc: null, error: LightPushError.ENCODE_FAILED };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol === CODECS.v3) {
|
||||||
|
log.info("Creating v3 RPC message");
|
||||||
|
return {
|
||||||
|
rpc: ProtocolHandler.createV3Rpc(protoMessage, encoder.pubsubTopic),
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Creating v2 RPC message");
|
||||||
|
return {
|
||||||
|
rpc: ProtocolHandler.createV2Rpc(protoMessage, encoder.pubsubTopic),
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
log.error("Failed to prepare push message", err);
|
||||||
|
return { rpc: null, error: LightPushError.GENERIC_FAIL };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode and evaluate a LightPush response according to the protocol version
|
||||||
|
*/
|
||||||
|
public static handleResponse(
|
||||||
|
bytes: Uint8ArrayList,
|
||||||
|
protocol: string,
|
||||||
|
peerId: PeerId
|
||||||
|
): LightPushCoreResult {
|
||||||
|
if (protocol === CODECS.v3) {
|
||||||
|
return ProtocolHandler.handleV3Response(bytes, peerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProtocolHandler.handleV2Response(bytes, peerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static handleV3Response(
|
||||||
|
bytes: Uint8ArrayList,
|
||||||
|
peerId: PeerId
|
||||||
|
): LightPushCoreResult {
|
||||||
|
try {
|
||||||
|
const decodedRpcV3 = PushRpc.decodeResponse(bytes);
|
||||||
|
const statusCode = decodedRpcV3.statusCode;
|
||||||
|
const statusDesc = decodedRpcV3.statusDesc;
|
||||||
|
|
||||||
|
if (statusCode !== LightPushStatusCode.SUCCESS) {
|
||||||
|
const error = LightPushError.REMOTE_PEER_REJECTED;
|
||||||
|
log.error(
|
||||||
|
`Remote peer rejected with v3 status code ${statusCode}: ${statusDesc}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error,
|
||||||
|
peerId: peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedRpcV3.relayPeerCount !== undefined) {
|
||||||
|
log.info(`Message relayed to ${decodedRpcV3.relayPeerCount} peers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: peerId, failure: null };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error: LightPushError.DECODE_FAILED,
|
||||||
|
peerId: peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static handleV2Response(
|
||||||
|
bytes: Uint8ArrayList,
|
||||||
|
peerId: PeerId
|
||||||
|
): LightPushCoreResult {
|
||||||
|
let response: PushResponse | undefined;
|
||||||
|
try {
|
||||||
|
const decodedRpc = PushRpcV2.decode(bytes);
|
||||||
|
response = decodedRpc.response;
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error: LightPushError.DECODE_FAILED,
|
||||||
|
peerId: peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error: LightPushError.NO_RESPONSE,
|
||||||
|
peerId: peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRLNResponseError(response.info)) {
|
||||||
|
log.error("Remote peer fault: RLN generation");
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error: LightPushError.RLN_PROOF_GENERATION,
|
||||||
|
peerId: peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.isSuccess) {
|
||||||
|
log.error("Remote peer rejected the message: ", response.info);
|
||||||
|
return {
|
||||||
|
success: null,
|
||||||
|
failure: {
|
||||||
|
error: LightPushError.REMOTE_PEER_REJECTED,
|
||||||
|
peerId: peerId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: peerId, failure: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createV2Rpc(
|
||||||
|
message: WakuMessage,
|
||||||
|
pubsubTopic: string
|
||||||
|
): VersionedPushRpc {
|
||||||
|
const v2Rpc = PushRpcV2.createRequest(message, pubsubTopic);
|
||||||
|
return Object.assign(v2Rpc, { version: "v2" as const });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createV3Rpc(
|
||||||
|
message: WakuMessage,
|
||||||
|
pubsubTopic: string
|
||||||
|
): VersionedPushRpc {
|
||||||
|
if (!message.timestamp) {
|
||||||
|
message.timestamp = BigInt(Date.now()) * BigInt(1_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const v3Rpc = PushRpc.createRequest(message, pubsubTopic);
|
||||||
|
return Object.assign(v3Rpc, { version: "v3" as const });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,14 +2,14 @@ import { proto_lightpush as proto } from "@waku/proto";
|
|||||||
import type { Uint8ArrayList } from "uint8arraylist";
|
import type { Uint8ArrayList } from "uint8arraylist";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
export class PushRpc {
|
export class PushRpcV2 {
|
||||||
public constructor(public proto: proto.PushRpc) {}
|
public constructor(public proto: proto.PushRpc) {}
|
||||||
|
|
||||||
public static createRequest(
|
public static createRequest(
|
||||||
message: proto.WakuMessage,
|
message: proto.WakuMessage,
|
||||||
pubsubTopic: string
|
pubsubTopic: string
|
||||||
): PushRpc {
|
): PushRpcV2 {
|
||||||
return new PushRpc({
|
return new PushRpcV2({
|
||||||
requestId: uuid(),
|
requestId: uuid(),
|
||||||
request: {
|
request: {
|
||||||
message: message,
|
message: message,
|
||||||
@ -19,9 +19,9 @@ export class PushRpc {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static decode(bytes: Uint8ArrayList): PushRpc {
|
public static decode(bytes: Uint8ArrayList): PushRpcV2 {
|
||||||
const res = proto.PushRpc.decode(bytes);
|
const res = proto.PushRpc.decode(bytes);
|
||||||
return new PushRpc(res);
|
return new PushRpcV2(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
public encode(): Uint8Array {
|
public encode(): Uint8Array {
|
||||||
|
|||||||
162
packages/core/src/lib/light_push/push_rpc_v3.ts
Normal file
162
packages/core/src/lib/light_push/push_rpc_v3.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { proto_lightpush as proto } from "@waku/proto";
|
||||||
|
import type { Uint8ArrayList } from "uint8arraylist";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LightPush v3 protocol RPC handler.
|
||||||
|
* Implements the v3 message format with correct field numbers:
|
||||||
|
* - requestId: 1
|
||||||
|
* - pubsubTopic: 20
|
||||||
|
* - message: 21
|
||||||
|
*/
|
||||||
|
export class PushRpc {
|
||||||
|
public constructor(
|
||||||
|
public proto: proto.LightPushRequestV3 | proto.LightPushResponseV3
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a v3 request message with proper field numbering
|
||||||
|
*/
|
||||||
|
public static createRequest(
|
||||||
|
message: proto.WakuMessage,
|
||||||
|
pubsubTopic: string
|
||||||
|
): PushRpc {
|
||||||
|
return new PushRpc({
|
||||||
|
requestId: uuid(),
|
||||||
|
pubsubTopic: pubsubTopic,
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a v3 response message with status code handling
|
||||||
|
*/
|
||||||
|
public static createResponse(
|
||||||
|
requestId: string,
|
||||||
|
statusCode: number,
|
||||||
|
statusDesc?: string,
|
||||||
|
relayPeerCount?: number
|
||||||
|
): PushRpc {
|
||||||
|
return new PushRpc({
|
||||||
|
requestId,
|
||||||
|
statusCode,
|
||||||
|
statusDesc,
|
||||||
|
relayPeerCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode v3 request message
|
||||||
|
*/
|
||||||
|
public static decodeRequest(bytes: Uint8ArrayList): PushRpc {
|
||||||
|
const res = proto.LightPushRequestV3.decode(bytes);
|
||||||
|
return new PushRpc(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode v3 response message
|
||||||
|
*/
|
||||||
|
public static decodeResponse(bytes: Uint8ArrayList): PushRpc {
|
||||||
|
const res = proto.LightPushResponseV3.decode(bytes);
|
||||||
|
return new PushRpc(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode message to bytes
|
||||||
|
*/
|
||||||
|
public encode(): Uint8Array {
|
||||||
|
if (this.isRequest()) {
|
||||||
|
return proto.LightPushRequestV3.encode(
|
||||||
|
this.proto as proto.LightPushRequestV3
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return proto.LightPushResponseV3.encode(
|
||||||
|
this.proto as proto.LightPushResponseV3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request data (if this is a request message)
|
||||||
|
*/
|
||||||
|
public get request(): proto.LightPushRequestV3 | undefined {
|
||||||
|
return this.isRequest()
|
||||||
|
? (this.proto as proto.LightPushRequestV3)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get response data (if this is a response message)
|
||||||
|
*/
|
||||||
|
public get response(): proto.LightPushResponseV3 | undefined {
|
||||||
|
return this.isResponse()
|
||||||
|
? (this.proto as proto.LightPushResponseV3)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the request ID
|
||||||
|
*/
|
||||||
|
public get requestId(): string {
|
||||||
|
return this.proto.requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the pubsub topic (only available in requests)
|
||||||
|
*/
|
||||||
|
public get pubsubTopic(): string | undefined {
|
||||||
|
return this.isRequest()
|
||||||
|
? (this.proto as proto.LightPushRequestV3).pubsubTopic
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message (only available in requests)
|
||||||
|
*/
|
||||||
|
public get message(): proto.WakuMessage | undefined {
|
||||||
|
return this.isRequest()
|
||||||
|
? (this.proto as proto.LightPushRequestV3).message
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status code (only available in responses)
|
||||||
|
*/
|
||||||
|
public get statusCode(): number | undefined {
|
||||||
|
return this.isResponse()
|
||||||
|
? (this.proto as proto.LightPushResponseV3).statusCode
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status description (only available in responses)
|
||||||
|
*/
|
||||||
|
public get statusDesc(): string | undefined {
|
||||||
|
return this.isResponse()
|
||||||
|
? (this.proto as proto.LightPushResponseV3).statusDesc
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the relay peer count (only available in responses)
|
||||||
|
*/
|
||||||
|
public get relayPeerCount(): number | undefined {
|
||||||
|
return this.isResponse()
|
||||||
|
? (this.proto as proto.LightPushResponseV3).relayPeerCount
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a request message
|
||||||
|
*/
|
||||||
|
private isRequest(): boolean {
|
||||||
|
return "pubsubTopic" in this.proto && "message" in this.proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a response message
|
||||||
|
*/
|
||||||
|
private isResponse(): boolean {
|
||||||
|
return "statusCode" in this.proto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,7 +13,7 @@ export class StreamManager {
|
|||||||
private streamPool: Map<string, Promise<void>> = new Map();
|
private streamPool: Map<string, Promise<void>> = new Map();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private multicodec: string,
|
private readonly multicodec: string,
|
||||||
private readonly libp2p: Libp2pComponents
|
private readonly libp2p: Libp2pComponents
|
||||||
) {
|
) {
|
||||||
this.log = new Logger(`stream-manager:${multicodec}`);
|
this.log = new Logger(`stream-manager:${multicodec}`);
|
||||||
|
|||||||
@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.0.12](https://github.com/waku-org/js-waku/compare/discovery-v0.0.11...discovery-v0.0.12) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/core bumped from 0.0.38 to 0.0.39
|
||||||
|
* @waku/enr bumped from 0.0.32 to 0.0.33
|
||||||
|
* @waku/interfaces bumped from 0.0.33 to 0.0.34
|
||||||
|
* @waku/proto bumped from ^0.0.13 to ^0.0.14
|
||||||
|
* @waku/utils bumped from 0.0.26 to 0.0.27
|
||||||
|
|
||||||
## [0.0.11](https://github.com/waku-org/js-waku/compare/discovery-v0.0.10...discovery-v0.0.11) (2025-08-14)
|
## [0.0.11](https://github.com/waku-org/js-waku/compare/discovery-v0.0.10...discovery-v0.0.11) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/discovery",
|
"name": "@waku/discovery",
|
||||||
"version": "0.0.11",
|
"version": "0.0.12",
|
||||||
"description": "Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.",
|
"description": "Contains various discovery mechanisms: DNS Discovery (EIP-1459, Peer Exchange, Local Peer Cache Discovery.",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@ -51,11 +51,11 @@
|
|||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@waku/core": "0.0.38",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/enr": "0.0.32",
|
"@waku/enr": "0.0.33",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/proto": "^0.0.13",
|
"@waku/proto": "^0.0.14",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"dns-over-http-resolver": "^3.0.8",
|
"dns-over-http-resolver": "^3.0.8",
|
||||||
"hi-base32": "^0.5.1",
|
"hi-base32": "^0.5.1",
|
||||||
|
|||||||
@ -99,6 +99,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* devDependencies
|
* devDependencies
|
||||||
* @waku/interfaces bumped from 0.0.27 to 0.0.28
|
* @waku/interfaces bumped from 0.0.27 to 0.0.28
|
||||||
|
|
||||||
|
## [0.0.33](https://github.com/waku-org/js-waku/compare/enr-v0.0.32...enr-v0.0.33) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/utils bumped from 0.0.26 to 0.0.27
|
||||||
|
* devDependencies
|
||||||
|
* @waku/interfaces bumped from 0.0.33 to 0.0.34
|
||||||
|
|
||||||
## [0.0.32](https://github.com/waku-org/js-waku/compare/enr-v0.0.31...enr-v0.0.32) (2025-08-14)
|
## [0.0.32](https://github.com/waku-org/js-waku/compare/enr-v0.0.31...enr-v0.0.32) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/enr",
|
"name": "@waku/enr",
|
||||||
"version": "0.0.32",
|
"version": "0.0.33",
|
||||||
"description": "ENR (EIP-778) for Waku",
|
"description": "ENR (EIP-778) for Waku",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@ -56,7 +56,7 @@
|
|||||||
"@libp2p/peer-id": "5.1.7",
|
"@libp2p/peer-id": "5.1.7",
|
||||||
"@multiformats/multiaddr": "^12.0.0",
|
"@multiformats/multiaddr": "^12.0.0",
|
||||||
"@noble/secp256k1": "^1.7.1",
|
"@noble/secp256k1": "^1.7.1",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"js-sha3": "^0.9.2"
|
"js-sha3": "^0.9.2"
|
||||||
},
|
},
|
||||||
@ -67,7 +67,7 @@
|
|||||||
"@types/chai": "^4.3.11",
|
"@types/chai": "^4.3.11",
|
||||||
"@types/mocha": "^10.0.6",
|
"@types/mocha": "^10.0.6",
|
||||||
"@waku/build-utils": "*",
|
"@waku/build-utils": "*",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
"cspell": "^8.6.1",
|
"cspell": "^8.6.1",
|
||||||
"fast-check": "^3.19.0",
|
"fast-check": "^3.19.0",
|
||||||
|
|||||||
@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|||||||
The file is maintained by [Release Please](https://github.com/googleapis/release-please) based on [Conventional Commits](https://www.conventionalcommits.org) specification,
|
The file is maintained by [Release Please](https://github.com/googleapis/release-please) based on [Conventional Commits](https://www.conventionalcommits.org) specification,
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.0.34](https://github.com/waku-org/js-waku/compare/interfaces-v0.0.33...interfaces-v0.0.34) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add start/stop to filter ([#2592](https://github.com/waku-org/js-waku/issues/2592)) ([2fba052](https://github.com/waku-org/js-waku/commit/2fba052b8b98cb64f6383de95d01b33beb771448))
|
||||||
|
* Expose message hash from IDecodedMessage ([#2578](https://github.com/waku-org/js-waku/issues/2578)) ([836d6b8](https://github.com/waku-org/js-waku/commit/836d6b8793a5124747684f6ea76b6dd47c73048b))
|
||||||
|
* Implement lp-v3 error codes with backwards compatibility ([#2501](https://github.com/waku-org/js-waku/issues/2501)) ([1625302](https://github.com/waku-org/js-waku/commit/16253026c6e30052d87d9975b58480951de469d8))
|
||||||
|
* Query on connect ([#2602](https://github.com/waku-org/js-waku/issues/2602)) ([8542d04](https://github.com/waku-org/js-waku/commit/8542d04bf5c9472f955ef8c9e5bc9e89c70f4738))
|
||||||
|
* StoreConnect events ([#2601](https://github.com/waku-org/js-waku/issues/2601)) ([0dfbcf6](https://github.com/waku-org/js-waku/commit/0dfbcf6b6bd9225dcb0dec540aeb1eb2703c8397))
|
||||||
|
|
||||||
## [0.0.33](https://github.com/waku-org/js-waku/compare/interfaces-v0.0.32...interfaces-v0.0.33) (2025-08-14)
|
## [0.0.33](https://github.com/waku-org/js-waku/compare/interfaces-v0.0.32...interfaces-v0.0.33) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/interfaces",
|
"name": "@waku/interfaces",
|
||||||
"version": "0.0.33",
|
"version": "0.0.34",
|
||||||
"description": "Definition of Waku interfaces",
|
"description": "Definition of Waku interfaces",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
|||||||
@ -4,6 +4,16 @@ import type { Callback } from "./protocols.js";
|
|||||||
export type IFilter = {
|
export type IFilter = {
|
||||||
readonly multicodec: string;
|
readonly multicodec: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the filter protocol.
|
||||||
|
*/
|
||||||
|
start(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the filter protocol.
|
||||||
|
*/
|
||||||
|
stop(): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to messages that match the filtering criteria defined in the specified decoders.
|
* Subscribes to messages that match the filtering criteria defined in the specified decoders.
|
||||||
* Executes a callback upon receiving each message.
|
* Executes a callback upon receiving each message.
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import type { ISender, ISendOptions } from "./sender.js";
|
import { IEncoder, IMessage } from "./message.js";
|
||||||
|
import { LightPushSDKResult } from "./protocols.js";
|
||||||
|
import type { ISendOptions } from "./sender.js";
|
||||||
|
|
||||||
export type LightPushProtocolOptions = ISendOptions & {
|
export type LightPushProtocolOptions = ISendOptions & {
|
||||||
/**
|
/**
|
||||||
@ -15,8 +17,40 @@ export type LightPushProtocolOptions = ISendOptions & {
|
|||||||
numPeersToUse?: number;
|
numPeersToUse?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ILightPush = ISender & {
|
export type ILightPush = {
|
||||||
readonly multicodec: string;
|
readonly multicodec: string[];
|
||||||
start: () => void;
|
start: () => void;
|
||||||
stop: () => void;
|
stop: () => void;
|
||||||
|
send: (
|
||||||
|
encoder: IEncoder,
|
||||||
|
message: IMessage,
|
||||||
|
options?: ISendOptions
|
||||||
|
) => Promise<LightPushSDKResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum LightPushStatusCode {
|
||||||
|
SUCCESS = 200,
|
||||||
|
BAD_REQUEST = 400,
|
||||||
|
PAYLOAD_TOO_LARGE = 413,
|
||||||
|
INVALID_MESSAGE = 420,
|
||||||
|
UNSUPPORTED_TOPIC = 421,
|
||||||
|
TOO_MANY_REQUESTS = 429,
|
||||||
|
INTERNAL_ERROR = 500,
|
||||||
|
UNAVAILABLE = 503,
|
||||||
|
NO_RLN_PROOF = 504,
|
||||||
|
NO_PEERS = 505
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatusDescriptions: Record<LightPushStatusCode, string> = {
|
||||||
|
[LightPushStatusCode.SUCCESS]: "Message sent successfully",
|
||||||
|
[LightPushStatusCode.BAD_REQUEST]: "Bad request format",
|
||||||
|
[LightPushStatusCode.PAYLOAD_TOO_LARGE]:
|
||||||
|
"Message payload exceeds maximum size",
|
||||||
|
[LightPushStatusCode.INVALID_MESSAGE]: "Message validation failed",
|
||||||
|
[LightPushStatusCode.UNSUPPORTED_TOPIC]: "Unsupported pubsub topic",
|
||||||
|
[LightPushStatusCode.TOO_MANY_REQUESTS]: "Rate limit exceeded",
|
||||||
|
[LightPushStatusCode.INTERNAL_ERROR]: "Internal server error",
|
||||||
|
[LightPushStatusCode.UNAVAILABLE]: "Service temporarily unavailable",
|
||||||
|
[LightPushStatusCode.NO_RLN_PROOF]: "RLN proof generation failed",
|
||||||
|
[LightPushStatusCode.NO_PEERS]: "No relay peers available"
|
||||||
};
|
};
|
||||||
|
|||||||
@ -130,117 +130,123 @@ export type Callback<T extends IDecodedMessage> = (
|
|||||||
msg: T
|
msg: T
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
|
||||||
export enum ProtocolError {
|
export enum LightPushError {
|
||||||
//
|
|
||||||
// GENERAL ERRORS SECTION
|
|
||||||
//
|
|
||||||
/**
|
|
||||||
* Could not determine the origin of the fault. Best to check connectivity and try again
|
|
||||||
* */
|
|
||||||
GENERIC_FAIL = "Generic error",
|
GENERIC_FAIL = "Generic error",
|
||||||
|
|
||||||
/**
|
|
||||||
* The remote peer rejected the message. Information provided by the remote peer
|
|
||||||
* is logged. Review message validity, or mitigation for `NO_PEER_AVAILABLE`
|
|
||||||
* or `DECODE_FAILED` can be used.
|
|
||||||
*/
|
|
||||||
REMOTE_PEER_REJECTED = "Remote peer rejected",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Failure to protobuf decode the message. May be due to a remote peer issue,
|
|
||||||
* ensuring that messages are sent via several peer enable mitigation of this error.
|
|
||||||
*/
|
|
||||||
DECODE_FAILED = "Failed to decode",
|
DECODE_FAILED = "Failed to decode",
|
||||||
|
|
||||||
/**
|
|
||||||
* Failure to find a peer with suitable protocols. This may due to a connection issue.
|
|
||||||
* Mitigation can be: retrying after a given time period, display connectivity issue
|
|
||||||
* to user or listening for `peer:connected:bootstrap` or `peer:connected:peer-exchange`
|
|
||||||
* on the connection manager before retrying.
|
|
||||||
*/
|
|
||||||
NO_PEER_AVAILABLE = "No peer available",
|
NO_PEER_AVAILABLE = "No peer available",
|
||||||
|
|
||||||
/**
|
|
||||||
* Failure to find a stream to the peer. This may be because the connection with the peer is not still alive.
|
|
||||||
* Mitigation can be: retrying after a given time period, or mitigation for `NO_PEER_AVAILABLE` can be used.
|
|
||||||
*/
|
|
||||||
NO_STREAM_AVAILABLE = "No stream available",
|
NO_STREAM_AVAILABLE = "No stream available",
|
||||||
|
|
||||||
/**
|
|
||||||
* The remote peer did not behave as expected. Mitigation for `NO_PEER_AVAILABLE`
|
|
||||||
* or `DECODE_FAILED` can be used.
|
|
||||||
*/
|
|
||||||
NO_RESPONSE = "No response received",
|
NO_RESPONSE = "No response received",
|
||||||
|
|
||||||
//
|
|
||||||
// SEND ERRORS SECTION
|
|
||||||
//
|
|
||||||
/**
|
|
||||||
* Failure to protobuf encode the message. This is not recoverable and needs
|
|
||||||
* further investigation.
|
|
||||||
*/
|
|
||||||
ENCODE_FAILED = "Failed to encode",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The message payload is empty, making the message invalid. Ensure that a non-empty
|
|
||||||
* payload is set on the outgoing message.
|
|
||||||
*/
|
|
||||||
EMPTY_PAYLOAD = "Payload is empty",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The message size is above the maximum message size allowed on the Waku Network.
|
|
||||||
* Compressing the message or using an alternative strategy for large messages is recommended.
|
|
||||||
*/
|
|
||||||
SIZE_TOO_BIG = "Size is too big",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The PubsubTopic passed to the send function is not configured on the Waku node.
|
|
||||||
* Please ensure that the PubsubTopic is used when initializing the Waku node.
|
|
||||||
*/
|
|
||||||
TOPIC_NOT_CONFIGURED = "Topic not configured",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fails when
|
|
||||||
*/
|
|
||||||
STREAM_ABORTED = "Stream aborted",
|
STREAM_ABORTED = "Stream aborted",
|
||||||
|
|
||||||
/**
|
ENCODE_FAILED = "Failed to encode",
|
||||||
* General proof generation error message.
|
EMPTY_PAYLOAD = "Payload is empty",
|
||||||
* nwaku: https://github.com/waku-org/nwaku/blob/c3cb06ac6c03f0f382d3941ea53b330f6a8dd127/waku/waku_rln_relay/group_manager/group_manager_base.nim#L201C19-L201C42
|
SIZE_TOO_BIG = "Size is too big",
|
||||||
*/
|
TOPIC_NOT_CONFIGURED = "Topic not configured",
|
||||||
RLN_PROOF_GENERATION = "Proof generation failed",
|
RLN_PROOF_GENERATION = "Proof generation failed",
|
||||||
|
REMOTE_PEER_REJECTED = "Remote peer rejected",
|
||||||
|
|
||||||
//
|
BAD_REQUEST = "Bad request format",
|
||||||
// RECEIVE ERRORS SECTION
|
PAYLOAD_TOO_LARGE = "Message payload exceeds maximum size",
|
||||||
//
|
INVALID_MESSAGE = "Message validation failed",
|
||||||
/**
|
UNSUPPORTED_TOPIC = "Unsupported pubsub topic",
|
||||||
* The pubsub topic configured on the decoder does not match the pubsub topic setup on the protocol.
|
TOO_MANY_REQUESTS = "Rate limit exceeded",
|
||||||
* Ensure that the pubsub topic used for decoder creation is the same as the one used for protocol.
|
INTERNAL_ERROR = "Internal server error",
|
||||||
*/
|
UNAVAILABLE = "Service temporarily unavailable",
|
||||||
TOPIC_DECODER_MISMATCH = "Topic decoder mismatch",
|
NO_RLN_PROOF = "RLN proof generation failed",
|
||||||
|
NO_PEERS = "No relay peers available"
|
||||||
/**
|
|
||||||
* The topics passed in the decoders do not match each other, or don't exist at all.
|
|
||||||
* Ensure that all the pubsub topics used in the decoders are valid and match each other.
|
|
||||||
*/
|
|
||||||
INVALID_DECODER_TOPICS = "Invalid decoder topics"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Failure {
|
export enum FilterError {
|
||||||
error: ProtocolError;
|
// General errors
|
||||||
|
GENERIC_FAIL = "Generic error",
|
||||||
|
DECODE_FAILED = "Failed to decode",
|
||||||
|
NO_PEER_AVAILABLE = "No peer available",
|
||||||
|
NO_STREAM_AVAILABLE = "No stream available",
|
||||||
|
NO_RESPONSE = "No response received",
|
||||||
|
STREAM_ABORTED = "Stream aborted",
|
||||||
|
|
||||||
|
// Filter specific errors
|
||||||
|
REMOTE_PEER_REJECTED = "Remote peer rejected",
|
||||||
|
TOPIC_NOT_CONFIGURED = "Topic not configured",
|
||||||
|
SUBSCRIPTION_FAILED = "Subscription failed",
|
||||||
|
UNSUBSCRIBE_FAILED = "Unsubscribe failed",
|
||||||
|
PING_FAILED = "Ping failed",
|
||||||
|
TOPIC_DECODER_MISMATCH = "Topic decoder mismatch",
|
||||||
|
INVALID_DECODER_TOPICS = "Invalid decoder topics",
|
||||||
|
SUBSCRIPTION_LIMIT_EXCEEDED = "Subscription limit exceeded",
|
||||||
|
INVALID_CONTENT_TOPIC = "Invalid content topic",
|
||||||
|
PUSH_MESSAGE_FAILED = "Push message failed",
|
||||||
|
EMPTY_MESSAGE = "Empty message received",
|
||||||
|
MISSING_PUBSUB_TOPIC = "Pubsub topic missing from push message"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LightPushFailure {
|
||||||
|
error: LightPushError;
|
||||||
peerId?: PeerId;
|
peerId?: PeerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CoreProtocolResult = ThisOrThat<
|
export interface FilterFailure {
|
||||||
|
error: FilterError;
|
||||||
|
peerId?: PeerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LightPushCoreResult = ThisOrThat<
|
||||||
"success",
|
"success",
|
||||||
PeerId,
|
PeerId,
|
||||||
"failure",
|
"failure",
|
||||||
Failure
|
LightPushFailure
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type FilterCoreResult = ThisOrThat<
|
||||||
|
"success",
|
||||||
|
PeerId,
|
||||||
|
"failure",
|
||||||
|
FilterFailure
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type LightPushSDKResult = ThisAndThat<
|
||||||
|
"successes",
|
||||||
|
PeerId[],
|
||||||
|
"failures",
|
||||||
|
LightPushFailure[]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type FilterSDKResult = ThisAndThat<
|
||||||
|
"successes",
|
||||||
|
PeerId[],
|
||||||
|
"failures",
|
||||||
|
FilterFailure[]
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated replace usage by specific result types
|
||||||
|
*/
|
||||||
export type SDKProtocolResult = ThisAndThat<
|
export type SDKProtocolResult = ThisAndThat<
|
||||||
"successes",
|
"successes",
|
||||||
PeerId[],
|
PeerId[],
|
||||||
"failures",
|
"failures",
|
||||||
Failure[]
|
Array<{
|
||||||
|
error: ProtocolError;
|
||||||
|
peerId?: PeerId;
|
||||||
|
}>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated replace usage by specific result types
|
||||||
|
*/
|
||||||
|
export enum ProtocolError {
|
||||||
|
GENERIC_FAIL = "Generic error",
|
||||||
|
REMOTE_PEER_REJECTED = "Remote peer rejected",
|
||||||
|
DECODE_FAILED = "Failed to decode",
|
||||||
|
NO_PEER_AVAILABLE = "No peer available",
|
||||||
|
NO_STREAM_AVAILABLE = "No stream available",
|
||||||
|
NO_RESPONSE = "No response received",
|
||||||
|
ENCODE_FAILED = "Failed to encode",
|
||||||
|
EMPTY_PAYLOAD = "Payload is empty",
|
||||||
|
SIZE_TOO_BIG = "Size is too big",
|
||||||
|
TOPIC_NOT_CONFIGURED = "Topic not configured",
|
||||||
|
STREAM_ABORTED = "Stream aborted",
|
||||||
|
RLN_PROOF_GENERATION = "Proof generation failed",
|
||||||
|
TOPIC_DECODER_MISMATCH = "Topic decoder mismatch",
|
||||||
|
INVALID_DECODER_TOPICS = "Invalid decoder topics"
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { IEncoder, IMessage } from "./message.js";
|
import type { IEncoder, IMessage } from "./message.js";
|
||||||
import { SDKProtocolResult } from "./protocols.js";
|
import { LightPushSDKResult } from "./protocols.js";
|
||||||
|
|
||||||
export type ISendOptions = {
|
export type ISendOptions = {
|
||||||
/**
|
/**
|
||||||
@ -13,6 +13,13 @@ export type ISendOptions = {
|
|||||||
* @default 3
|
* @default 3
|
||||||
*/
|
*/
|
||||||
maxAttempts?: number;
|
maxAttempts?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use v2 of the light push protocol.
|
||||||
|
* This parameter will be removed in the future.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
useLegacy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ISender {
|
export interface ISender {
|
||||||
@ -20,5 +27,5 @@ export interface ISender {
|
|||||||
encoder: IEncoder,
|
encoder: IEncoder,
|
||||||
message: IMessage,
|
message: IMessage,
|
||||||
sendOptions?: ISendOptions
|
sendOptions?: ISendOptions
|
||||||
) => Promise<SDKProtocolResult>;
|
) => Promise<LightPushSDKResult>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,6 +101,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* @waku/interfaces bumped from 0.0.27 to 0.0.28
|
* @waku/interfaces bumped from 0.0.27 to 0.0.28
|
||||||
* @waku/utils bumped from 0.0.20 to 0.0.21
|
* @waku/utils bumped from 0.0.20 to 0.0.21
|
||||||
|
|
||||||
|
## [0.0.37](https://github.com/waku-org/js-waku/compare/message-encryption-v0.0.36...message-encryption-v0.0.37) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/core bumped from 0.0.38 to 0.0.39
|
||||||
|
* @waku/interfaces bumped from 0.0.33 to 0.0.34
|
||||||
|
* @waku/proto bumped from 0.0.13 to 0.0.14
|
||||||
|
* @waku/utils bumped from 0.0.26 to 0.0.27
|
||||||
|
|
||||||
## [0.0.36](https://github.com/waku-org/js-waku/compare/message-encryption-v0.0.35...message-encryption-v0.0.36) (2025-08-14)
|
## [0.0.36](https://github.com/waku-org/js-waku/compare/message-encryption-v0.0.35...message-encryption-v0.0.36) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/message-encryption",
|
"name": "@waku/message-encryption",
|
||||||
"version": "0.0.36",
|
"version": "0.0.37",
|
||||||
"description": "Waku Message Payload Encryption",
|
"description": "Waku Message Payload Encryption",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@ -76,10 +76,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/secp256k1": "^1.7.1",
|
"@noble/secp256k1": "^1.7.1",
|
||||||
"@waku/core": "0.0.38",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/proto": "0.0.13",
|
"@waku/proto": "0.0.14",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"js-sha3": "^0.9.2",
|
"js-sha3": "^0.9.2",
|
||||||
"uint8arrays": "^5.0.1"
|
"uint8arrays": "^5.0.1"
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.0.14](https://github.com/waku-org/js-waku/compare/proto-v0.0.13...proto-v0.0.14) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Implement lp-v3 error codes with backwards compatibility ([#2501](https://github.com/waku-org/js-waku/issues/2501)) ([1625302](https://github.com/waku-org/js-waku/commit/16253026c6e30052d87d9975b58480951de469d8))
|
||||||
|
|
||||||
## [0.0.13](https://github.com/waku-org/js-waku/compare/proto-v0.0.12...proto-v0.0.13) (2025-08-14)
|
## [0.0.13](https://github.com/waku-org/js-waku/compare/proto-v0.0.12...proto-v0.0.13) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/proto",
|
"name": "@waku/proto",
|
||||||
"version": "0.0.13",
|
"version": "0.0.14",
|
||||||
"description": "Protobuf definitions for Waku",
|
"description": "Protobuf definitions for Waku",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
|||||||
@ -84,7 +84,7 @@ export interface SdsMessage {
|
|||||||
senderId: string
|
senderId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
channelId: string
|
channelId: string
|
||||||
lamportTimestamp?: number
|
lamportTimestamp?: bigint
|
||||||
causalHistory: HistoryEntry[]
|
causalHistory: HistoryEntry[]
|
||||||
bloomFilter?: Uint8Array
|
bloomFilter?: Uint8Array
|
||||||
content?: Uint8Array
|
content?: Uint8Array
|
||||||
@ -117,7 +117,7 @@ export namespace SdsMessage {
|
|||||||
|
|
||||||
if (obj.lamportTimestamp != null) {
|
if (obj.lamportTimestamp != null) {
|
||||||
w.uint32(80)
|
w.uint32(80)
|
||||||
w.int32(obj.lamportTimestamp)
|
w.uint64(obj.lamportTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.causalHistory != null) {
|
if (obj.causalHistory != null) {
|
||||||
@ -167,7 +167,7 @@ export namespace SdsMessage {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 10: {
|
case 10: {
|
||||||
obj.lamportTimestamp = reader.int32()
|
obj.lamportTimestamp = reader.uint64()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 11: {
|
case 11: {
|
||||||
|
|||||||
@ -39,4 +39,4 @@ message LightPushResponseV3 {
|
|||||||
uint32 status_code = 10;
|
uint32 status_code = 10;
|
||||||
optional string status_desc = 11;
|
optional string status_desc = 11;
|
||||||
optional uint32 relay_peer_count = 12;
|
optional uint32 relay_peer_count = 12;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ message SdsMessage {
|
|||||||
string sender_id = 1; // Participant ID of the message sender
|
string sender_id = 1; // Participant ID of the message sender
|
||||||
string message_id = 2; // Unique identifier of the message
|
string message_id = 2; // Unique identifier of the message
|
||||||
string channel_id = 3; // Identifier of the channel to which the message belongs
|
string channel_id = 3; // Identifier of the channel to which the message belongs
|
||||||
optional int32 lamport_timestamp = 10; // Logical timestamp for causal ordering in channel
|
optional uint64 lamport_timestamp = 10; // Logical timestamp for causal ordering in channel
|
||||||
repeated HistoryEntry causal_history = 11; // List of preceding message IDs that this message causally depends on. Generally 2 or 3 message IDs are included.
|
repeated HistoryEntry causal_history = 11; // List of preceding message IDs that this message causally depends on. Generally 2 or 3 message IDs are included.
|
||||||
optional bytes bloom_filter = 12; // Bloom filter representing received message IDs in channel
|
optional bytes bloom_filter = 12; // Bloom filter representing received message IDs in channel
|
||||||
optional bytes content = 20; // Actual content of the message
|
optional bytes content = 20; // Actual content of the message
|
||||||
|
|||||||
@ -25,6 +25,25 @@
|
|||||||
* @waku/interfaces bumped from 0.0.16 to 0.0.17
|
* @waku/interfaces bumped from 0.0.16 to 0.0.17
|
||||||
* @waku/utils bumped from 0.0.9 to 0.0.10
|
* @waku/utils bumped from 0.0.9 to 0.0.10
|
||||||
|
|
||||||
|
## [0.0.22](https://github.com/waku-org/js-waku/compare/relay-v0.0.21...relay-v0.0.22) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Expose message hash from IDecodedMessage ([#2578](https://github.com/waku-org/js-waku/issues/2578)) ([836d6b8](https://github.com/waku-org/js-waku/commit/836d6b8793a5124747684f6ea76b6dd47c73048b))
|
||||||
|
* Implement lp-v3 error codes with backwards compatibility ([#2501](https://github.com/waku-org/js-waku/issues/2501)) ([1625302](https://github.com/waku-org/js-waku/commit/16253026c6e30052d87d9975b58480951de469d8))
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/core bumped from 0.0.38 to 0.0.39
|
||||||
|
* @waku/sdk bumped from 0.0.34 to 0.0.35
|
||||||
|
* @waku/interfaces bumped from 0.0.33 to 0.0.34
|
||||||
|
* @waku/proto bumped from 0.0.13 to 0.0.14
|
||||||
|
* @waku/utils bumped from 0.0.26 to 0.0.27
|
||||||
|
|
||||||
## [0.0.21](https://github.com/waku-org/js-waku/compare/relay-v0.0.20...relay-v0.0.21) (2025-08-14)
|
## [0.0.21](https://github.com/waku-org/js-waku/compare/relay-v0.0.20...relay-v0.0.21) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/relay",
|
"name": "@waku/relay",
|
||||||
"version": "0.0.21",
|
"version": "0.0.22",
|
||||||
"description": "Relay Protocol for Waku",
|
"description": "Relay Protocol for Waku",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@ -51,11 +51,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chainsafe/libp2p-gossipsub": "14.1.1",
|
"@chainsafe/libp2p-gossipsub": "14.1.1",
|
||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
"@waku/core": "0.0.38",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/sdk": "0.0.34",
|
"@waku/sdk": "0.0.35",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/proto": "0.0.13",
|
"@waku/proto": "0.0.14",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/utils": "0.0.27",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-check": "^3.19.0",
|
"fast-check": "^3.19.0",
|
||||||
|
|||||||
@ -19,9 +19,9 @@ import {
|
|||||||
IRelay,
|
IRelay,
|
||||||
type IRoutingInfo,
|
type IRoutingInfo,
|
||||||
Libp2p,
|
Libp2p,
|
||||||
ProtocolError,
|
LightPushError,
|
||||||
PubsubTopic,
|
LightPushSDKResult,
|
||||||
SDKProtocolResult
|
PubsubTopic
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { isWireSizeUnderCap, toAsyncIterator } from "@waku/utils";
|
import { isWireSizeUnderCap, toAsyncIterator } from "@waku/utils";
|
||||||
import { pushOrInitMapSet } from "@waku/utils";
|
import { pushOrInitMapSet } from "@waku/utils";
|
||||||
@ -127,7 +127,7 @@ export class Relay implements IRelay {
|
|||||||
public async send(
|
public async send(
|
||||||
encoder: IEncoder,
|
encoder: IEncoder,
|
||||||
message: IMessage
|
message: IMessage
|
||||||
): Promise<SDKProtocolResult> {
|
): Promise<LightPushSDKResult> {
|
||||||
const { pubsubTopic } = encoder;
|
const { pubsubTopic } = encoder;
|
||||||
if (!this.pubsubTopics.has(pubsubTopic)) {
|
if (!this.pubsubTopics.has(pubsubTopic)) {
|
||||||
log.error("Failed to send waku relay: topic not configured");
|
log.error("Failed to send waku relay: topic not configured");
|
||||||
@ -135,7 +135,7 @@ export class Relay implements IRelay {
|
|||||||
successes: [],
|
successes: [],
|
||||||
failures: [
|
failures: [
|
||||||
{
|
{
|
||||||
error: ProtocolError.TOPIC_NOT_CONFIGURED
|
error: LightPushError.TOPIC_NOT_CONFIGURED
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -148,7 +148,7 @@ export class Relay implements IRelay {
|
|||||||
successes: [],
|
successes: [],
|
||||||
failures: [
|
failures: [
|
||||||
{
|
{
|
||||||
error: ProtocolError.ENCODE_FAILED
|
error: LightPushError.ENCODE_FAILED
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -160,7 +160,7 @@ export class Relay implements IRelay {
|
|||||||
successes: [],
|
successes: [],
|
||||||
failures: [
|
failures: [
|
||||||
{
|
{
|
||||||
error: ProtocolError.SIZE_TOO_BIG
|
error: LightPushError.SIZE_TOO_BIG
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,23 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.9](https://github.com/waku-org/js-waku/compare/rln-v0.1.8...rln-v0.1.9) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Expose message hash from IDecodedMessage ([#2578](https://github.com/waku-org/js-waku/issues/2578)) ([836d6b8](https://github.com/waku-org/js-waku/commit/836d6b8793a5124747684f6ea76b6dd47c73048b))
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/core bumped from ^0.0.38 to ^0.0.39
|
||||||
|
* @waku/utils bumped from ^0.0.26 to ^0.0.27
|
||||||
|
* devDependencies
|
||||||
|
* @waku/interfaces bumped from 0.0.33 to 0.0.34
|
||||||
|
* @waku/message-encryption bumped from ^0.0.36 to ^0.0.37
|
||||||
|
|
||||||
## [0.1.8](https://github.com/waku-org/js-waku/compare/rln-v0.1.7...rln-v0.1.8) (2025-08-14)
|
## [0.1.8](https://github.com/waku-org/js-waku/compare/rln-v0.1.7...rln-v0.1.8) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/rln",
|
"name": "@waku/rln",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"description": "RLN (Rate Limiting Nullifier) implementation for Waku",
|
"description": "RLN (Rate Limiting Nullifier) implementation for Waku",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@ -54,12 +54,12 @@
|
|||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@types/chai": "^5.0.1",
|
"@types/chai": "^5.0.1",
|
||||||
"@types/chai-spies": "^1.0.6",
|
"@types/chai-spies": "^1.0.6",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@types/deep-equal-in-any-order": "^1.0.4",
|
"@types/deep-equal-in-any-order": "^1.0.4",
|
||||||
"@types/lodash": "^4.17.15",
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@waku/build-utils": "^1.0.0",
|
"@waku/build-utils": "^1.0.0",
|
||||||
"@waku/message-encryption": "^0.0.36",
|
"@waku/message-encryption": "^0.0.37",
|
||||||
"deep-equal-in-any-order": "^2.0.6",
|
"deep-equal-in-any-order": "^2.0.6",
|
||||||
"fast-check": "^3.23.2",
|
"fast-check": "^3.23.2",
|
||||||
"rollup-plugin-copy": "^3.5.0"
|
"rollup-plugin-copy": "^3.5.0"
|
||||||
@ -76,10 +76,10 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chainsafe/bls-keystore": "3.0.0",
|
"@chainsafe/bls-keystore": "3.0.0",
|
||||||
"@waku/core": "^0.0.38",
|
"@waku/core": "^0.0.39",
|
||||||
"@waku/utils": "^0.0.26",
|
"@waku/utils": "^0.0.27",
|
||||||
"@noble/hashes": "^1.2.0",
|
"@noble/hashes": "^1.2.0",
|
||||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
"@waku/zerokit-rln-wasm": "^0.2.1",
|
||||||
"ethereum-cryptography": "^3.1.0",
|
"ethereum-cryptography": "^3.1.0",
|
||||||
"ethers": "^5.7.2",
|
"ethers": "^5.7.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|||||||
@ -1,363 +0,0 @@
|
|||||||
import { createDecoder, createEncoder } from "@waku/core/lib/message/version_0";
|
|
||||||
import { IDecodedMessage } from "@waku/interfaces";
|
|
||||||
import {
|
|
||||||
generatePrivateKey,
|
|
||||||
generateSymmetricKey,
|
|
||||||
getPublicKey
|
|
||||||
} from "@waku/message-encryption";
|
|
||||||
import {
|
|
||||||
createDecoder as createAsymDecoder,
|
|
||||||
createEncoder as createAsymEncoder
|
|
||||||
} from "@waku/message-encryption/ecies";
|
|
||||||
import {
|
|
||||||
createDecoder as createSymDecoder,
|
|
||||||
createEncoder as createSymEncoder
|
|
||||||
} from "@waku/message-encryption/symmetric";
|
|
||||||
import { expect } from "chai";
|
|
||||||
|
|
||||||
import {
|
|
||||||
createRLNDecoder,
|
|
||||||
createRLNEncoder,
|
|
||||||
RLNDecoder,
|
|
||||||
RLNEncoder
|
|
||||||
} from "./codec.js";
|
|
||||||
import {
|
|
||||||
createTestMetaSetter,
|
|
||||||
createTestRLNCodecSetup,
|
|
||||||
EMPTY_PROTO_MESSAGE,
|
|
||||||
TEST_CONSTANTS,
|
|
||||||
verifyRLNMessage
|
|
||||||
} from "./codec.test-utils.js";
|
|
||||||
import { RlnMessage } from "./message.js";
|
|
||||||
import { epochBytesToInt } from "./utils/index.js";
|
|
||||||
|
|
||||||
describe("RLN codec with version 0", () => {
|
|
||||||
it("toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
|
|
||||||
const rlnEncoder = createRLNEncoder({
|
|
||||||
encoder: createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
});
|
|
||||||
const rlnDecoder = createRLNDecoder({
|
|
||||||
rlnInstance,
|
|
||||||
decoder: createDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
const bytes = await rlnEncoder.toWire({ payload });
|
|
||||||
|
|
||||||
expect(bytes).to.not.be.undefined;
|
|
||||||
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
|
||||||
expect(protoResult).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
)) as RlnMessage<IDecodedMessage>;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("RLN codec with version 1", () => {
|
|
||||||
it("Symmetric, toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const symKey = generateSymmetricKey();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createSymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
symKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createSymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
symKey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const bytes = await rlnEncoder.toWire({ payload });
|
|
||||||
|
|
||||||
expect(bytes).to.not.be.undefined;
|
|
||||||
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
|
||||||
expect(protoResult).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Symmetric, toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const symKey = generateSymmetricKey();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createSymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
symKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createSymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
symKey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
);
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Asymmetric, toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const privateKey = generatePrivateKey();
|
|
||||||
const publicKey = getPublicKey(privateKey);
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createAsymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
publicKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createAsymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
privateKey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const bytes = await rlnEncoder.toWire({ payload });
|
|
||||||
|
|
||||||
expect(bytes).to.not.be.undefined;
|
|
||||||
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
|
||||||
expect(protoResult).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Asymmetric, toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const privateKey = generatePrivateKey();
|
|
||||||
const publicKey = getPublicKey(privateKey);
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createAsymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
publicKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createAsymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
privateKey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
);
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("RLN Codec - epoch", () => {
|
|
||||||
it("toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
)) as RlnMessage<IDecodedMessage>;
|
|
||||||
|
|
||||||
const epochBytes = proto!.rateLimitProof!.epoch;
|
|
||||||
const epoch = epochBytesToInt(epochBytes);
|
|
||||||
|
|
||||||
expect(msg.epoch!.toString(10).length).to.eq(9);
|
|
||||||
expect(msg.epoch).to.eq(epoch);
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("RLN codec with version 0 and meta setter", () => {
|
|
||||||
it("toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const metaSetter = createTestMetaSetter();
|
|
||||||
|
|
||||||
const rlnEncoder = createRLNEncoder({
|
|
||||||
encoder: createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
metaSetter
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
});
|
|
||||||
const rlnDecoder = createRLNDecoder({
|
|
||||||
rlnInstance,
|
|
||||||
decoder: createDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
const bytes = await rlnEncoder.toWire({ payload });
|
|
||||||
|
|
||||||
expect(bytes).to.not.be.undefined;
|
|
||||||
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
|
||||||
expect(protoResult).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
const expectedMeta = metaSetter({
|
|
||||||
...EMPTY_PROTO_MESSAGE,
|
|
||||||
payload: protoResult!.payload
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(msg!.meta).to.deep.eq(expectedMeta);
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const metaSetter = createTestMetaSetter();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
metaSetter
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
)) as RlnMessage<IDecodedMessage>;
|
|
||||||
|
|
||||||
const expectedMeta = metaSetter({
|
|
||||||
...EMPTY_PROTO_MESSAGE,
|
|
||||||
payload: msg!.payload
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(msg!.meta).to.deep.eq(expectedMeta);
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import type { IProtoMessage } from "@waku/interfaces";
|
|
||||||
import { createRoutingInfo } from "@waku/utils";
|
|
||||||
import { expect } from "chai";
|
|
||||||
|
|
||||||
import { createRLN } from "./create.js";
|
|
||||||
import type { IdentityCredential } from "./identity.js";
|
|
||||||
|
|
||||||
export interface TestRLNCodecSetup {
|
|
||||||
rlnInstance: any;
|
|
||||||
credential: IdentityCredential;
|
|
||||||
index: number;
|
|
||||||
payload: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TEST_CONSTANTS = {
|
|
||||||
contentTopic: "/test/1/waku-message/utf8",
|
|
||||||
emptyPubsubTopic: "",
|
|
||||||
defaultIndex: 0,
|
|
||||||
defaultPayload: new Uint8Array([1, 2, 3, 4, 5]),
|
|
||||||
routingInfo: createRoutingInfo(
|
|
||||||
{
|
|
||||||
clusterId: 0,
|
|
||||||
numShardsInCluster: 2
|
|
||||||
},
|
|
||||||
{ contentTopic: "/test/1/waku-message/utf8" }
|
|
||||||
)
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const EMPTY_PROTO_MESSAGE = {
|
|
||||||
timestamp: undefined,
|
|
||||||
contentTopic: "",
|
|
||||||
ephemeral: undefined,
|
|
||||||
meta: undefined,
|
|
||||||
rateLimitProof: undefined,
|
|
||||||
version: undefined
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a basic RLN setup for codec tests
|
|
||||||
*/
|
|
||||||
export async function createTestRLNCodecSetup(): Promise<TestRLNCodecSetup> {
|
|
||||||
const rlnInstance = await createRLN();
|
|
||||||
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
|
||||||
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rlnInstance,
|
|
||||||
credential,
|
|
||||||
index: TEST_CONSTANTS.defaultIndex,
|
|
||||||
payload: TEST_CONSTANTS.defaultPayload
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a meta setter function for testing
|
|
||||||
*/
|
|
||||||
export function createTestMetaSetter(): (
|
|
||||||
msg: IProtoMessage & { meta: undefined }
|
|
||||||
) => Uint8Array {
|
|
||||||
return (msg: IProtoMessage & { meta: undefined }): Uint8Array => {
|
|
||||||
const buffer = new ArrayBuffer(4);
|
|
||||||
const view = new DataView(buffer);
|
|
||||||
view.setUint32(0, msg.payload.length, false);
|
|
||||||
return new Uint8Array(buffer);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies common RLN message properties
|
|
||||||
*/
|
|
||||||
export function verifyRLNMessage(
|
|
||||||
msg: any,
|
|
||||||
payload: Uint8Array,
|
|
||||||
contentTopic: string,
|
|
||||||
version: number,
|
|
||||||
rlnInstance: any
|
|
||||||
): void {
|
|
||||||
expect(msg.rateLimitProof).to.not.be.undefined;
|
|
||||||
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
|
||||||
expect(msg.verifyNoRoot()).to.be.true;
|
|
||||||
expect(msg.epoch).to.not.be.undefined;
|
|
||||||
expect(msg.epoch).to.be.gt(0);
|
|
||||||
|
|
||||||
expect(msg.contentTopic).to.eq(contentTopic);
|
|
||||||
expect(msg.msg.version).to.eq(version);
|
|
||||||
expect(msg.payload).to.deep.eq(payload);
|
|
||||||
expect(msg.timestamp).to.not.be.undefined;
|
|
||||||
}
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import type {
|
|
||||||
IDecodedMessage,
|
|
||||||
IDecoder,
|
|
||||||
IEncoder,
|
|
||||||
IMessage,
|
|
||||||
IProtoMessage,
|
|
||||||
IRateLimitProof,
|
|
||||||
IRoutingInfo
|
|
||||||
} from "@waku/interfaces";
|
|
||||||
import { Logger } from "@waku/utils";
|
|
||||||
|
|
||||||
import type { IdentityCredential } from "./identity.js";
|
|
||||||
import { RlnMessage, toRLNSignal } from "./message.js";
|
|
||||||
import { RLNInstance } from "./rln.js";
|
|
||||||
|
|
||||||
const log = new Logger("rln:encoder");
|
|
||||||
|
|
||||||
export class RLNEncoder implements IEncoder {
|
|
||||||
private readonly idSecretHash: Uint8Array;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private readonly encoder: IEncoder,
|
|
||||||
private readonly rlnInstance: RLNInstance,
|
|
||||||
private readonly index: number,
|
|
||||||
identityCredential: IdentityCredential
|
|
||||||
) {
|
|
||||||
if (index < 0) throw new Error("Invalid membership index");
|
|
||||||
this.idSecretHash = identityCredential.IDSecretHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async toWire(message: IMessage): Promise<Uint8Array | undefined> {
|
|
||||||
message.rateLimitProof = await this.generateProof(message);
|
|
||||||
log.info("Proof generated", message.rateLimitProof);
|
|
||||||
return this.encoder.toWire(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async toProtoObj(
|
|
||||||
message: IMessage
|
|
||||||
): Promise<IProtoMessage | undefined> {
|
|
||||||
const protoMessage = await this.encoder.toProtoObj(message);
|
|
||||||
if (!protoMessage) return;
|
|
||||||
|
|
||||||
protoMessage.contentTopic = this.contentTopic;
|
|
||||||
protoMessage.rateLimitProof = await this.generateProof(message);
|
|
||||||
log.info("Proof generated", protoMessage.rateLimitProof);
|
|
||||||
return protoMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generateProof(message: IMessage): Promise<IRateLimitProof> {
|
|
||||||
const signal = toRLNSignal(this.contentTopic, message);
|
|
||||||
return this.rlnInstance.zerokit.generateRLNProof(
|
|
||||||
signal,
|
|
||||||
this.index,
|
|
||||||
message.timestamp,
|
|
||||||
this.idSecretHash
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get pubsubTopic(): string {
|
|
||||||
return this.encoder.pubsubTopic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get routingInfo(): IRoutingInfo {
|
|
||||||
return this.encoder.routingInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get contentTopic(): string {
|
|
||||||
return this.encoder.contentTopic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get ephemeral(): boolean {
|
|
||||||
return this.encoder.ephemeral;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RLNEncoderOptions = {
|
|
||||||
encoder: IEncoder;
|
|
||||||
rlnInstance: RLNInstance;
|
|
||||||
index: number;
|
|
||||||
credential: IdentityCredential;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => {
|
|
||||||
return new RLNEncoder(
|
|
||||||
options.encoder,
|
|
||||||
options.rlnInstance,
|
|
||||||
options.index,
|
|
||||||
options.credential
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RLNDecoder<T extends IDecodedMessage>
|
|
||||||
implements IDecoder<RlnMessage<T>>
|
|
||||||
{
|
|
||||||
public constructor(
|
|
||||||
private readonly rlnInstance: RLNInstance,
|
|
||||||
private readonly decoder: IDecoder<T>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public get pubsubTopic(): string {
|
|
||||||
return this.decoder.pubsubTopic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get contentTopic(): string {
|
|
||||||
return this.decoder.contentTopic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public fromWireToProtoObj(
|
|
||||||
bytes: Uint8Array
|
|
||||||
): Promise<IProtoMessage | undefined> {
|
|
||||||
const protoMessage = this.decoder.fromWireToProtoObj(bytes);
|
|
||||||
log.info("Message decoded", protoMessage);
|
|
||||||
return Promise.resolve(protoMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async fromProtoObj(
|
|
||||||
pubsubTopic: string,
|
|
||||||
proto: IProtoMessage
|
|
||||||
): Promise<RlnMessage<T> | undefined> {
|
|
||||||
const msg: T | undefined = await this.decoder.fromProtoObj(
|
|
||||||
pubsubTopic,
|
|
||||||
proto
|
|
||||||
);
|
|
||||||
if (!msg) return;
|
|
||||||
return new RlnMessage(this.rlnInstance, msg, proto.rateLimitProof);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RLNDecoderOptions<T extends IDecodedMessage> = {
|
|
||||||
decoder: IDecoder<T>;
|
|
||||||
rlnInstance: RLNInstance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createRLNDecoder = <T extends IDecodedMessage>(
|
|
||||||
options: RLNDecoderOptions<T>
|
|
||||||
): RLNDecoder<T> => {
|
|
||||||
return new RLNDecoder(options.rlnInstance, options.decoder);
|
|
||||||
};
|
|
||||||
@ -19,26 +19,16 @@ export const PRICE_CALCULATOR_CONTRACT = {
|
|||||||
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
|
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
|
||||||
*/
|
*/
|
||||||
export const RATE_LIMIT_TIERS = {
|
export const RATE_LIMIT_TIERS = {
|
||||||
LOW: 20, // Suggested minimum rate - 20 messages per epoch
|
STANDARD: 300,
|
||||||
MEDIUM: 200,
|
MAX: 600
|
||||||
HIGH: 600 // Suggested maximum rate - 600 messages per epoch
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Global rate limit parameters
|
// Global rate limit parameters
|
||||||
export const RATE_LIMIT_PARAMS = {
|
export const RATE_LIMIT_PARAMS = {
|
||||||
MIN_RATE: RATE_LIMIT_TIERS.LOW,
|
MIN_RATE: RATE_LIMIT_TIERS.STANDARD,
|
||||||
MAX_RATE: RATE_LIMIT_TIERS.HIGH,
|
MAX_RATE: RATE_LIMIT_TIERS.MAX,
|
||||||
MAX_TOTAL_RATE: 160_000, // Maximum total rate limit across all memberships
|
MAX_TOTAL_RATE: 160_000,
|
||||||
EPOCH_LENGTH: 600 // Epoch length in seconds (10 minutes)
|
EPOCH_LENGTH: 600
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Q value for the RLN contract
|
|
||||||
* This is the upper bound for the ID commitment
|
|
||||||
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
|
|
||||||
*/
|
|
||||||
export const RLN_Q = BigInt(
|
|
||||||
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DEFAULT_RATE_LIMIT = RATE_LIMIT_PARAMS.MAX_RATE;
|
export const DEFAULT_RATE_LIMIT = RATE_LIMIT_PARAMS.MAX_RATE;
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export { RLNContract } from "./rln_contract.js";
|
|
||||||
export * from "./constants.js";
|
export * from "./constants.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { ethers } from "ethers";
|
|||||||
|
|
||||||
import { IdentityCredential } from "../identity.js";
|
import { IdentityCredential } from "../identity.js";
|
||||||
import { DecryptedCredentials } from "../keystore/types.js";
|
import { DecryptedCredentials } from "../keystore/types.js";
|
||||||
import { BytesUtils } from "../utils/bytes.js";
|
|
||||||
|
|
||||||
import { RLN_ABI } from "./abi/rln.js";
|
import { RLN_ABI } from "./abi/rln.js";
|
||||||
import {
|
import {
|
||||||
@ -632,7 +631,7 @@ export class RLNBaseContract {
|
|||||||
permit.v,
|
permit.v,
|
||||||
permit.r,
|
permit.r,
|
||||||
permit.s,
|
permit.s,
|
||||||
BytesUtils.buildBigIntFromUint8ArrayBE(identity.IDCommitment),
|
identity.IDCommitmentBigInt,
|
||||||
this.rateLimit,
|
this.rateLimit,
|
||||||
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
|
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { expect, use } from "chai";
|
|
||||||
import chaiAsPromised from "chai-as-promised";
|
|
||||||
import * as ethers from "ethers";
|
|
||||||
import sinon, { SinonSandbox } from "sinon";
|
|
||||||
|
|
||||||
import { createTestRLNInstance, initializeRLNContract } from "./test_setup.js";
|
|
||||||
import {
|
|
||||||
createMockRegistryContract,
|
|
||||||
createRegisterStub,
|
|
||||||
mockRLNRegisteredEvent,
|
|
||||||
verifyRegistration
|
|
||||||
} from "./test_utils.js";
|
|
||||||
|
|
||||||
use(chaiAsPromised);
|
|
||||||
|
|
||||||
describe("RLN Contract abstraction - RLN", () => {
|
|
||||||
let sandbox: SinonSandbox;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
sandbox = sinon.createSandbox();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Member Registration", () => {
|
|
||||||
it("should fetch members from events and store them in the RLN instance", async () => {
|
|
||||||
const { rlnInstance, insertMemberSpy } = await createTestRLNInstance();
|
|
||||||
const membershipRegisteredEvent = mockRLNRegisteredEvent();
|
|
||||||
const queryFilterStub = sinon.stub().returns([membershipRegisteredEvent]);
|
|
||||||
|
|
||||||
const mockedRegistryContract = createMockRegistryContract({
|
|
||||||
queryFilter: queryFilterStub
|
|
||||||
});
|
|
||||||
|
|
||||||
const rlnContract = await initializeRLNContract(
|
|
||||||
rlnInstance,
|
|
||||||
mockedRegistryContract
|
|
||||||
);
|
|
||||||
|
|
||||||
await rlnContract.fetchMembers({
|
|
||||||
fromBlock: 0,
|
|
||||||
fetchRange: 1000,
|
|
||||||
fetchChunks: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
insertMemberSpy.calledWith(
|
|
||||||
ethers.utils.zeroPad(
|
|
||||||
hexToBytes(membershipRegisteredEvent.args!.idCommitment),
|
|
||||||
32
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).to.be.true;
|
|
||||||
expect(queryFilterStub.called).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should register a member", async () => {
|
|
||||||
const { rlnInstance, identity, insertMemberSpy } =
|
|
||||||
await createTestRLNInstance();
|
|
||||||
|
|
||||||
const registerStub = createRegisterStub(identity);
|
|
||||||
const mockedRegistryContract = createMockRegistryContract({
|
|
||||||
register: registerStub,
|
|
||||||
queryFilter: () => []
|
|
||||||
});
|
|
||||||
|
|
||||||
const rlnContract = await initializeRLNContract(
|
|
||||||
rlnInstance,
|
|
||||||
mockedRegistryContract
|
|
||||||
);
|
|
||||||
|
|
||||||
const decryptedCredentials =
|
|
||||||
await rlnContract.registerWithIdentity(identity);
|
|
||||||
|
|
||||||
if (!decryptedCredentials) {
|
|
||||||
throw new Error("Failed to retrieve credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyRegistration(
|
|
||||||
decryptedCredentials,
|
|
||||||
identity,
|
|
||||||
registerStub,
|
|
||||||
insertMemberSpy
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
import { Logger } from "@waku/utils";
|
|
||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { ethers } from "ethers";
|
|
||||||
|
|
||||||
import type { RLNInstance } from "../rln.js";
|
|
||||||
import { MerkleRootTracker } from "../root_tracker.js";
|
|
||||||
import { BytesUtils } from "../utils/bytes.js";
|
|
||||||
|
|
||||||
import { RLNBaseContract } from "./rln_base_contract.js";
|
|
||||||
import { RLNContractInitOptions } from "./types.js";
|
|
||||||
|
|
||||||
const log = new Logger("rln:contract");
|
|
||||||
|
|
||||||
export class RLNContract extends RLNBaseContract {
|
|
||||||
private instance: RLNInstance;
|
|
||||||
private merkleRootTracker: MerkleRootTracker;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous initializer for RLNContract.
|
|
||||||
* Allows injecting a mocked contract for testing purposes.
|
|
||||||
*/
|
|
||||||
public static async init(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
options: RLNContractInitOptions
|
|
||||||
): Promise<RLNContract> {
|
|
||||||
const rlnContract = new RLNContract(rlnInstance, options);
|
|
||||||
|
|
||||||
return rlnContract;
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
options: RLNContractInitOptions
|
|
||||||
) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this.instance = rlnInstance;
|
|
||||||
|
|
||||||
const initialRoot = rlnInstance.zerokit.getMerkleRoot();
|
|
||||||
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override processEvents(events: ethers.Event[]): void {
|
|
||||||
const toRemoveTable = new Map<number, number[]>();
|
|
||||||
const toInsertTable = new Map<number, ethers.Event[]>();
|
|
||||||
|
|
||||||
events.forEach((evt) => {
|
|
||||||
if (!evt.args) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
evt.event === "MembershipErased" ||
|
|
||||||
evt.event === "MembershipExpired"
|
|
||||||
) {
|
|
||||||
let index = evt.args.index;
|
|
||||||
|
|
||||||
if (!index) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof index === "number" || typeof index === "string") {
|
|
||||||
index = ethers.BigNumber.from(index);
|
|
||||||
} else {
|
|
||||||
log.error("Index is not a number or string", {
|
|
||||||
index,
|
|
||||||
event: evt
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toRemoveVal = toRemoveTable.get(evt.blockNumber);
|
|
||||||
if (toRemoveVal != undefined) {
|
|
||||||
toRemoveVal.push(index.toNumber());
|
|
||||||
toRemoveTable.set(evt.blockNumber, toRemoveVal);
|
|
||||||
} else {
|
|
||||||
toRemoveTable.set(evt.blockNumber, [index.toNumber()]);
|
|
||||||
}
|
|
||||||
} else if (evt.event === "MembershipRegistered") {
|
|
||||||
let eventsPerBlock = toInsertTable.get(evt.blockNumber);
|
|
||||||
if (eventsPerBlock == undefined) {
|
|
||||||
eventsPerBlock = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
eventsPerBlock.push(evt);
|
|
||||||
toInsertTable.set(evt.blockNumber, eventsPerBlock);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.removeMembers(this.instance, toRemoveTable);
|
|
||||||
this.insertMembers(this.instance, toInsertTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private insertMembers(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
toInsert: Map<number, ethers.Event[]>
|
|
||||||
): void {
|
|
||||||
toInsert.forEach((events: ethers.Event[], blockNumber: number) => {
|
|
||||||
events.forEach((evt) => {
|
|
||||||
if (!evt.args) return;
|
|
||||||
|
|
||||||
const _idCommitment = evt.args.idCommitment as string;
|
|
||||||
let index = evt.args.index;
|
|
||||||
|
|
||||||
if (!_idCommitment || !index) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof index === "number" || typeof index === "string") {
|
|
||||||
index = ethers.BigNumber.from(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
const idCommitment = BytesUtils.zeroPadLE(
|
|
||||||
hexToBytes(_idCommitment),
|
|
||||||
32
|
|
||||||
);
|
|
||||||
rlnInstance.zerokit.insertMember(idCommitment);
|
|
||||||
|
|
||||||
const numericIndex = index.toNumber();
|
|
||||||
this._members.set(numericIndex, {
|
|
||||||
index,
|
|
||||||
idCommitment: _idCommitment
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRoot = rlnInstance.zerokit.getMerkleRoot();
|
|
||||||
this.merkleRootTracker.pushRoot(blockNumber, currentRoot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeMembers(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
toRemove: Map<number, number[]>
|
|
||||||
): void {
|
|
||||||
const removeDescending = new Map([...toRemove].reverse());
|
|
||||||
removeDescending.forEach((indexes: number[], blockNumber: number) => {
|
|
||||||
indexes.forEach((index) => {
|
|
||||||
if (this._members.has(index)) {
|
|
||||||
this._members.delete(index);
|
|
||||||
rlnInstance.zerokit.deleteMember(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.merkleRootTracker.backFill(blockNumber);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { ethers } from "ethers";
|
|
||||||
import sinon from "sinon";
|
|
||||||
|
|
||||||
import { createRLN } from "../create.js";
|
|
||||||
import type { IdentityCredential } from "../identity.js";
|
|
||||||
|
|
||||||
import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js";
|
|
||||||
import { RLNContract } from "./rln_contract.js";
|
|
||||||
|
|
||||||
export interface TestRLNInstance {
|
|
||||||
rlnInstance: any;
|
|
||||||
identity: IdentityCredential;
|
|
||||||
insertMemberSpy: sinon.SinonStub;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a test RLN instance with basic setup
|
|
||||||
*/
|
|
||||||
export async function createTestRLNInstance(): Promise<TestRLNInstance> {
|
|
||||||
const rlnInstance = await createRLN();
|
|
||||||
const insertMemberSpy = sinon.stub();
|
|
||||||
rlnInstance.zerokit.insertMember = insertMemberSpy;
|
|
||||||
|
|
||||||
const mockSignature =
|
|
||||||
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
|
|
||||||
const identity =
|
|
||||||
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rlnInstance,
|
|
||||||
identity,
|
|
||||||
insertMemberSpy
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes an RLN contract with the given registry contract
|
|
||||||
*/
|
|
||||||
export async function initializeRLNContract(
|
|
||||||
rlnInstance: any,
|
|
||||||
mockedRegistryContract: ethers.Contract
|
|
||||||
): Promise<RLNContract> {
|
|
||||||
const provider = new ethers.providers.JsonRpcProvider();
|
|
||||||
const voidSigner = new ethers.VoidSigner(RLN_CONTRACT.address, provider);
|
|
||||||
|
|
||||||
const originalRegister = mockedRegistryContract.register;
|
|
||||||
(mockedRegistryContract as any).register = function (...args: any[]) {
|
|
||||||
const result = originalRegister.apply(this, args);
|
|
||||||
|
|
||||||
if (args[0] && rlnInstance.zerokit) {
|
|
||||||
const idCommitmentBigInt = args[0];
|
|
||||||
const idCommitmentHex =
|
|
||||||
"0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
|
|
||||||
const idCommitment = ethers.utils.zeroPad(
|
|
||||||
hexToBytes(idCommitmentHex),
|
|
||||||
32
|
|
||||||
);
|
|
||||||
rlnInstance.zerokit.insertMember(idCommitment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const contract = await RLNContract.init(rlnInstance, {
|
|
||||||
address: RLN_CONTRACT.address,
|
|
||||||
signer: voidSigner,
|
|
||||||
rateLimit: DEFAULT_RATE_LIMIT,
|
|
||||||
contract: mockedRegistryContract
|
|
||||||
});
|
|
||||||
|
|
||||||
return contract;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common test message data
|
|
||||||
*/
|
|
||||||
export const TEST_DATA = {
|
|
||||||
contentTopic: "/test/1/waku-message/utf8",
|
|
||||||
emptyPubsubTopic: "",
|
|
||||||
testMessage: Uint8Array.from(
|
|
||||||
"Hello World".split("").map((x) => x.charCodeAt(0))
|
|
||||||
),
|
|
||||||
mockSignature:
|
|
||||||
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c"
|
|
||||||
};
|
|
||||||
@ -1,179 +0,0 @@
|
|||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { expect } from "chai";
|
|
||||||
import * as ethers from "ethers";
|
|
||||||
import sinon from "sinon";
|
|
||||||
|
|
||||||
import type { IdentityCredential } from "../identity.js";
|
|
||||||
|
|
||||||
import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js";
|
|
||||||
|
|
||||||
export const mockRateLimits = {
|
|
||||||
minRate: 20,
|
|
||||||
maxRate: 600,
|
|
||||||
maxTotalRate: 1200,
|
|
||||||
currentTotalRate: 500
|
|
||||||
};
|
|
||||||
|
|
||||||
type MockProvider = {
|
|
||||||
getLogs: () => never[];
|
|
||||||
getBlockNumber: () => Promise<number>;
|
|
||||||
getNetwork: () => Promise<{ chainId: number }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MockFilters = {
|
|
||||||
MembershipRegistered: () => { address: string };
|
|
||||||
MembershipErased: () => { address: string };
|
|
||||||
MembershipExpired: () => { address: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createMockProvider(): MockProvider {
|
|
||||||
return {
|
|
||||||
getLogs: () => [],
|
|
||||||
getBlockNumber: () => Promise.resolve(1000),
|
|
||||||
getNetwork: () => Promise.resolve({ chainId: 11155111 })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMockFilters(): MockFilters {
|
|
||||||
return {
|
|
||||||
MembershipRegistered: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipExpired: () => ({ address: RLN_CONTRACT.address })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContractOverrides = Partial<{
|
|
||||||
filters: Record<string, unknown>;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export function createMockRegistryContract(
|
|
||||||
overrides: ContractOverrides = {}
|
|
||||||
): ethers.Contract {
|
|
||||||
const filters = {
|
|
||||||
MembershipRegistered: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipExpired: () => ({ address: RLN_CONTRACT.address })
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseContract = {
|
|
||||||
minMembershipRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.minRate)),
|
|
||||||
maxMembershipRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxRate)),
|
|
||||||
maxTotalRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxTotalRate)),
|
|
||||||
currentTotalRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.currentTotalRate)),
|
|
||||||
queryFilter: () => [],
|
|
||||||
provider: createMockProvider(),
|
|
||||||
filters,
|
|
||||||
on: () => ({}),
|
|
||||||
removeAllListeners: () => ({}),
|
|
||||||
register: () => ({
|
|
||||||
wait: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
events: [mockRLNRegisteredEvent()]
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
estimateGas: {
|
|
||||||
register: () => Promise.resolve(ethers.BigNumber.from(100000))
|
|
||||||
},
|
|
||||||
functions: {
|
|
||||||
register: () => Promise.resolve()
|
|
||||||
},
|
|
||||||
getMemberIndex: () => Promise.resolve(null),
|
|
||||||
interface: {
|
|
||||||
getEvent: (eventName: string) => ({
|
|
||||||
name: eventName,
|
|
||||||
format: () => {}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
address: RLN_CONTRACT.address
|
|
||||||
};
|
|
||||||
|
|
||||||
// Merge overrides while preserving filters
|
|
||||||
const merged = {
|
|
||||||
...baseContract,
|
|
||||||
...overrides,
|
|
||||||
filters: { ...filters, ...(overrides.filters || {}) }
|
|
||||||
};
|
|
||||||
|
|
||||||
return merged as unknown as ethers.Contract;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mockRLNRegisteredEvent(idCommitment?: string): ethers.Event {
|
|
||||||
return {
|
|
||||||
args: {
|
|
||||||
idCommitment:
|
|
||||||
idCommitment ||
|
|
||||||
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
||||||
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
|
|
||||||
index: ethers.BigNumber.from(1)
|
|
||||||
},
|
|
||||||
event: "MembershipRegistered"
|
|
||||||
} as unknown as ethers.Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatIdCommitment(idCommitmentBigInt: bigint): string {
|
|
||||||
return "0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRegisterStub(
|
|
||||||
identity: IdentityCredential
|
|
||||||
): sinon.SinonStub {
|
|
||||||
return sinon.stub().callsFake(() => ({
|
|
||||||
wait: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
events: [
|
|
||||||
{
|
|
||||||
event: "MembershipRegistered",
|
|
||||||
args: {
|
|
||||||
idCommitment: formatIdCommitment(identity.IDCommitmentBigInt),
|
|
||||||
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
|
|
||||||
index: ethers.BigNumber.from(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyRegistration(
|
|
||||||
decryptedCredentials: any,
|
|
||||||
identity: IdentityCredential,
|
|
||||||
registerStub: sinon.SinonStub,
|
|
||||||
insertMemberSpy: sinon.SinonStub
|
|
||||||
): void {
|
|
||||||
if (!decryptedCredentials) {
|
|
||||||
throw new Error("Decrypted credentials should not be undefined");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify registration call
|
|
||||||
expect(
|
|
||||||
registerStub.calledWith(
|
|
||||||
sinon.match.same(identity.IDCommitmentBigInt),
|
|
||||||
sinon.match.same(DEFAULT_RATE_LIMIT),
|
|
||||||
sinon.match.array,
|
|
||||||
sinon.match.object
|
|
||||||
)
|
|
||||||
).to.be.true;
|
|
||||||
|
|
||||||
// Verify credential properties
|
|
||||||
expect(decryptedCredentials).to.have.property("identity");
|
|
||||||
expect(decryptedCredentials).to.have.property("membership");
|
|
||||||
expect(decryptedCredentials.membership).to.include({
|
|
||||||
address: RLN_CONTRACT.address,
|
|
||||||
treeIndex: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify member insertion
|
|
||||||
const expectedIdCommitment = ethers.utils.zeroPad(
|
|
||||||
hexToBytes(formatIdCommitment(identity.IDCommitmentBigInt)),
|
|
||||||
32
|
|
||||||
);
|
|
||||||
expect(insertMemberSpy.callCount).to.equal(1);
|
|
||||||
expect(insertMemberSpy.getCall(0).args[0]).to.deep.equal(
|
|
||||||
expectedIdCommitment
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
import { assert, expect } from "chai";
|
|
||||||
|
|
||||||
import { createRLN } from "./create.js";
|
|
||||||
|
|
||||||
describe("js-rln", () => {
|
|
||||||
it("should verify a proof", async function () {
|
|
||||||
const rlnInstance = await createRLN();
|
|
||||||
|
|
||||||
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
|
||||||
|
|
||||||
//peer's index in the Merkle Tree
|
|
||||||
const index = 5;
|
|
||||||
|
|
||||||
// Create a Merkle tree with random members
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
if (i == index) {
|
|
||||||
// insert the current peer's pk
|
|
||||||
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
|
||||||
} else {
|
|
||||||
// create a new key pair
|
|
||||||
rlnInstance.zerokit.insertMember(
|
|
||||||
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare the message
|
|
||||||
const uint8Msg = Uint8Array.from(
|
|
||||||
"Hello World".split("").map((x) => x.charCodeAt(0))
|
|
||||||
);
|
|
||||||
|
|
||||||
// setting up the epoch
|
|
||||||
const epoch = new Date();
|
|
||||||
|
|
||||||
// generating proof
|
|
||||||
const proof = await rlnInstance.zerokit.generateRLNProof(
|
|
||||||
uint8Msg,
|
|
||||||
index,
|
|
||||||
epoch,
|
|
||||||
credential.IDSecretHash
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// verify the proof
|
|
||||||
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
|
||||||
expect(verifResult).to.be.true;
|
|
||||||
} catch (err) {
|
|
||||||
assert.fail(0, 1, "should not have failed proof verification");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Modifying the signal so it's invalid
|
|
||||||
uint8Msg[4] = 4;
|
|
||||||
// verify the proof
|
|
||||||
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
|
||||||
expect(verifResult).to.be.false;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
it("should verify a proof with a seeded membership key generation", async function () {
|
|
||||||
const rlnInstance = await createRLN();
|
|
||||||
const seed = "This is a test seed";
|
|
||||||
const credential =
|
|
||||||
rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
|
||||||
|
|
||||||
//peer's index in the Merkle Tree
|
|
||||||
const index = 5;
|
|
||||||
|
|
||||||
// Create a Merkle tree with random members
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
if (i == index) {
|
|
||||||
// insert the current peer's pk
|
|
||||||
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
|
||||||
} else {
|
|
||||||
// create a new key pair
|
|
||||||
rlnInstance.zerokit.insertMember(
|
|
||||||
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare the message
|
|
||||||
const uint8Msg = Uint8Array.from(
|
|
||||||
"Hello World".split("").map((x) => x.charCodeAt(0))
|
|
||||||
);
|
|
||||||
|
|
||||||
// setting up the epoch
|
|
||||||
const epoch = new Date();
|
|
||||||
|
|
||||||
// generating proof
|
|
||||||
const proof = await rlnInstance.zerokit.generateRLNProof(
|
|
||||||
uint8Msg,
|
|
||||||
index,
|
|
||||||
epoch,
|
|
||||||
credential.IDSecretHash
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// verify the proof
|
|
||||||
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
|
||||||
expect(verifResult).to.be.true;
|
|
||||||
} catch (err) {
|
|
||||||
assert.fail(0, 1, "should not have failed proof verification");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Modifying the signal so it's invalid
|
|
||||||
uint8Msg[4] = 4;
|
|
||||||
// verify the proof
|
|
||||||
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
|
||||||
expect(verifResult).to.be.false;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should generate the same membership key if the same seed is provided", async function () {
|
|
||||||
const rlnInstance = await createRLN();
|
|
||||||
const seed = "This is a test seed";
|
|
||||||
const memKeys1 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
|
||||||
const memKeys2 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
|
||||||
|
|
||||||
memKeys1.IDCommitment.forEach((element, index) => {
|
|
||||||
expect(element).to.equal(memKeys2.IDCommitment[index]);
|
|
||||||
});
|
|
||||||
memKeys1.IDNullifier.forEach((element, index) => {
|
|
||||||
expect(element).to.equal(memKeys2.IDNullifier[index]);
|
|
||||||
});
|
|
||||||
memKeys1.IDSecretHash.forEach((element, index) => {
|
|
||||||
expect(element).to.equal(memKeys2.IDSecretHash[index]);
|
|
||||||
});
|
|
||||||
memKeys1.IDTrapdoor.forEach((element, index) => {
|
|
||||||
expect(element).to.equal(memKeys2.IDTrapdoor[index]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,11 +1,8 @@
|
|||||||
import { hmac } from "@noble/hashes/hmac";
|
|
||||||
import { sha256 } from "@noble/hashes/sha2";
|
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
|
|
||||||
import { RLN_CONTRACT, RLN_Q } from "./contract/constants.js";
|
import { RLN_CONTRACT } from "./contract/constants.js";
|
||||||
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
||||||
import { IdentityCredential } from "./identity.js";
|
|
||||||
import { Keystore } from "./keystore/index.js";
|
import { Keystore } from "./keystore/index.js";
|
||||||
import type {
|
import type {
|
||||||
DecryptedCredentials,
|
DecryptedCredentials,
|
||||||
@ -13,7 +10,6 @@ import type {
|
|||||||
} from "./keystore/index.js";
|
} from "./keystore/index.js";
|
||||||
import { KeystoreEntity, Password } from "./keystore/types.js";
|
import { KeystoreEntity, Password } from "./keystore/types.js";
|
||||||
import { RegisterMembershipOptions, StartRLNOptions } from "./types.js";
|
import { RegisterMembershipOptions, StartRLNOptions } from "./types.js";
|
||||||
import { BytesUtils } from "./utils/bytes.js";
|
|
||||||
import { extractMetaMaskSigner } from "./utils/index.js";
|
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||||
import { Zerokit } from "./zerokit.js";
|
import { Zerokit } from "./zerokit.js";
|
||||||
|
|
||||||
@ -21,7 +17,6 @@ const log = new Logger("rln:credentials");
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages credentials for RLN
|
* Manages credentials for RLN
|
||||||
* This is a lightweight implementation of the RLN contract that doesn't require Zerokit
|
|
||||||
* It is used to register membership and generate identity credentials
|
* It is used to register membership and generate identity credentials
|
||||||
*/
|
*/
|
||||||
export class RLNCredentialsManager {
|
export class RLNCredentialsManager {
|
||||||
@ -34,9 +29,9 @@ export class RLNCredentialsManager {
|
|||||||
protected keystore = Keystore.create();
|
protected keystore = Keystore.create();
|
||||||
public credentials: undefined | DecryptedCredentials;
|
public credentials: undefined | DecryptedCredentials;
|
||||||
|
|
||||||
public zerokit: undefined | Zerokit;
|
public zerokit: Zerokit;
|
||||||
|
|
||||||
public constructor(zerokit?: Zerokit) {
|
public constructor(zerokit: Zerokit) {
|
||||||
log.info("RLNCredentialsManager initialized");
|
log.info("RLNCredentialsManager initialized");
|
||||||
this.zerokit = zerokit;
|
this.zerokit = zerokit;
|
||||||
}
|
}
|
||||||
@ -81,7 +76,7 @@ export class RLNCredentialsManager {
|
|||||||
this.contract = await RLNBaseContract.create({
|
this.contract = await RLNBaseContract.create({
|
||||||
address: address!,
|
address: address!,
|
||||||
signer: signer!,
|
signer: signer!,
|
||||||
rateLimit: rateLimit ?? this.zerokit?.rateLimit
|
rateLimit: rateLimit ?? this.zerokit.rateLimit
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("RLNCredentialsManager successfully started");
|
log.info("RLNCredentialsManager successfully started");
|
||||||
@ -106,18 +101,10 @@ export class RLNCredentialsManager {
|
|||||||
let identity = "identity" in options && options.identity;
|
let identity = "identity" in options && options.identity;
|
||||||
|
|
||||||
if ("signature" in options) {
|
if ("signature" in options) {
|
||||||
log.info("Generating identity from signature");
|
log.info("Using Zerokit to generate identity");
|
||||||
if (this.zerokit) {
|
identity = this.zerokit.generateSeededIdentityCredential(
|
||||||
log.info("Using Zerokit to generate identity");
|
options.signature
|
||||||
identity = this.zerokit.generateSeededIdentityCredential(
|
);
|
||||||
options.signature
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
log.info("Using local implementation to generate identity");
|
|
||||||
identity = await this.generateSeededIdentityCredential(
|
|
||||||
options.signature
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@ -242,55 +229,4 @@ export class RLNCredentialsManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates an identity credential from a seed string
|
|
||||||
* This is a pure implementation that doesn't rely on Zerokit
|
|
||||||
* @param seed A string seed to generate the identity from
|
|
||||||
* @returns IdentityCredential
|
|
||||||
*/
|
|
||||||
private async generateSeededIdentityCredential(
|
|
||||||
seed: string
|
|
||||||
): Promise<IdentityCredential> {
|
|
||||||
log.info("Generating seeded identity credential");
|
|
||||||
// Convert the seed to bytes
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const seedBytes = encoder.encode(seed);
|
|
||||||
|
|
||||||
// Generate deterministic values using HMAC-SHA256
|
|
||||||
// We use different context strings for each component to ensure they're different
|
|
||||||
const idTrapdoorBE = hmac(sha256, seedBytes, encoder.encode("IDTrapdoor"));
|
|
||||||
const idNullifierBE = hmac(
|
|
||||||
sha256,
|
|
||||||
seedBytes,
|
|
||||||
encoder.encode("IDNullifier")
|
|
||||||
);
|
|
||||||
|
|
||||||
const combinedBytes = new Uint8Array([...idTrapdoorBE, ...idNullifierBE]);
|
|
||||||
const idSecretHashBE = sha256(combinedBytes);
|
|
||||||
|
|
||||||
const idCommitmentRawBE = sha256(idSecretHashBE);
|
|
||||||
const idCommitmentBE = this.reduceIdCommitment(idCommitmentRawBE);
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Successfully generated identity credential, storing in Big Endian format"
|
|
||||||
);
|
|
||||||
return new IdentityCredential(
|
|
||||||
idTrapdoorBE,
|
|
||||||
idNullifierBE,
|
|
||||||
idSecretHashBE,
|
|
||||||
idCommitmentBE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: take 32-byte BE, reduce mod Q, return 32-byte BE
|
|
||||||
*/
|
|
||||||
private reduceIdCommitment(
|
|
||||||
bytesBE: Uint8Array,
|
|
||||||
limit: bigint = RLN_Q
|
|
||||||
): Uint8Array {
|
|
||||||
const nBE = BytesUtils.buildBigIntFromUint8ArrayBE(bytesBE);
|
|
||||||
return BytesUtils.bigIntToUint8Array32BE(nBE % limit);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,7 @@ export class IdentityCredential {
|
|||||||
public readonly IDSecretHash: Uint8Array,
|
public readonly IDSecretHash: Uint8Array,
|
||||||
public readonly IDCommitment: Uint8Array
|
public readonly IDCommitment: Uint8Array
|
||||||
) {
|
) {
|
||||||
this.IDCommitmentBigInt =
|
this.IDCommitmentBigInt = BytesUtils.toBigInt(IDCommitment);
|
||||||
BytesUtils.buildBigIntFromUint8ArrayBE(IDCommitment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromBytes(memKeys: Uint8Array): IdentityCredential {
|
public static fromBytes(memKeys: Uint8Array): IdentityCredential {
|
||||||
|
|||||||
@ -1,28 +1,18 @@
|
|||||||
import { RLNDecoder, RLNEncoder } from "./codec.js";
|
|
||||||
import { RLN_ABI } from "./contract/abi/rln.js";
|
import { RLN_ABI } from "./contract/abi/rln.js";
|
||||||
import { RLN_CONTRACT, RLNContract } from "./contract/index.js";
|
import { RLN_CONTRACT } from "./contract/index.js";
|
||||||
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
||||||
import { createRLN } from "./create.js";
|
import { createRLN } from "./create.js";
|
||||||
import { RLNCredentialsManager } from "./credentials_manager.js";
|
|
||||||
import { IdentityCredential } from "./identity.js";
|
import { IdentityCredential } from "./identity.js";
|
||||||
import { Keystore } from "./keystore/index.js";
|
import { Keystore } from "./keystore/index.js";
|
||||||
import { Proof } from "./proof.js";
|
|
||||||
import { RLNInstance } from "./rln.js";
|
import { RLNInstance } from "./rln.js";
|
||||||
import { MerkleRootTracker } from "./root_tracker.js";
|
|
||||||
import { extractMetaMaskSigner } from "./utils/index.js";
|
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
RLNCredentialsManager,
|
|
||||||
RLNBaseContract,
|
RLNBaseContract,
|
||||||
createRLN,
|
createRLN,
|
||||||
Keystore,
|
Keystore,
|
||||||
RLNInstance,
|
RLNInstance,
|
||||||
IdentityCredential,
|
IdentityCredential,
|
||||||
Proof,
|
|
||||||
RLNEncoder,
|
|
||||||
RLNDecoder,
|
|
||||||
MerkleRootTracker,
|
|
||||||
RLNContract,
|
|
||||||
RLN_CONTRACT,
|
RLN_CONTRACT,
|
||||||
extractMetaMaskSigner,
|
extractMetaMaskSigner,
|
||||||
RLN_ABI
|
RLN_ABI
|
||||||
|
|||||||
@ -222,9 +222,7 @@ describe("Keystore", () => {
|
|||||||
])
|
])
|
||||||
} as unknown as IdentityCredential;
|
} as unknown as IdentityCredential;
|
||||||
// Add the missing property for test correctness
|
// Add the missing property for test correctness
|
||||||
identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE(
|
identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment);
|
||||||
identity.IDCommitment
|
|
||||||
);
|
|
||||||
const membership = {
|
const membership = {
|
||||||
chainId: "0xAA36A7",
|
chainId: "0xAA36A7",
|
||||||
treeIndex: 8,
|
treeIndex: 8,
|
||||||
@ -276,9 +274,7 @@ describe("Keystore", () => {
|
|||||||
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15
|
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15
|
||||||
]
|
]
|
||||||
} as unknown as IdentityCredential;
|
} as unknown as IdentityCredential;
|
||||||
identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE(
|
identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment);
|
||||||
identity.IDCommitment
|
|
||||||
);
|
|
||||||
const membership = {
|
const membership = {
|
||||||
chainId: "0xAA36A7",
|
chainId: "0xAA36A7",
|
||||||
treeIndex: 8,
|
treeIndex: 8,
|
||||||
|
|||||||
@ -264,20 +264,14 @@ export class Keystore {
|
|||||||
_.get(obj, "identityCredential.idSecretHash", [])
|
_.get(obj, "identityCredential.idSecretHash", [])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Big Endian
|
const idCommitmentBigInt = BytesUtils.toBigInt(idCommitmentLE);
|
||||||
const idCommitmentBE = BytesUtils.switchEndianness(idCommitmentLE);
|
|
||||||
const idTrapdoorBE = BytesUtils.switchEndianness(idTrapdoorLE);
|
|
||||||
const idNullifierBE = BytesUtils.switchEndianness(idNullifierLE);
|
|
||||||
const idSecretHashBE = BytesUtils.switchEndianness(idSecretHashLE);
|
|
||||||
const idCommitmentBigInt =
|
|
||||||
BytesUtils.buildBigIntFromUint8ArrayBE(idCommitmentBE);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identity: {
|
identity: {
|
||||||
IDCommitment: idCommitmentBE,
|
IDCommitment: idCommitmentLE,
|
||||||
IDTrapdoor: idTrapdoorBE,
|
IDTrapdoor: idTrapdoorLE,
|
||||||
IDNullifier: idNullifierBE,
|
IDNullifier: idNullifierLE,
|
||||||
IDSecretHash: idSecretHashBE,
|
IDSecretHash: idSecretHashLE,
|
||||||
IDCommitmentBigInt: idCommitmentBigInt
|
IDCommitmentBigInt: idCommitmentBigInt
|
||||||
},
|
},
|
||||||
membership: {
|
membership: {
|
||||||
@ -329,35 +323,18 @@ export class Keystore {
|
|||||||
|
|
||||||
// follows nwaku implementation
|
// follows nwaku implementation
|
||||||
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
|
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
|
||||||
// IdentityCredential is stored in Big Endian format => switch to Little Endian
|
|
||||||
private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array {
|
private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array {
|
||||||
const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } =
|
const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } =
|
||||||
options.identity;
|
options.identity;
|
||||||
const idCommitmentLE = BytesUtils.switchEndianness(IDCommitment);
|
|
||||||
const idNullifierLE = BytesUtils.switchEndianness(IDNullifier);
|
|
||||||
const idSecretHashLE = BytesUtils.switchEndianness(IDSecretHash);
|
|
||||||
const idTrapdoorLE = BytesUtils.switchEndianness(IDTrapdoor);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log({
|
|
||||||
idCommitmentBE: IDCommitment,
|
|
||||||
idCommitmentLE,
|
|
||||||
idNullifierBE: IDNullifier,
|
|
||||||
idNullifierLE,
|
|
||||||
idSecretHashBE: IDSecretHash,
|
|
||||||
idSecretHashLE,
|
|
||||||
idTrapdoorBE: IDTrapdoor,
|
|
||||||
idTrapdoorLE
|
|
||||||
});
|
|
||||||
|
|
||||||
return utf8ToBytes(
|
return utf8ToBytes(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
treeIndex: options.membership.treeIndex,
|
treeIndex: options.membership.treeIndex,
|
||||||
identityCredential: {
|
identityCredential: {
|
||||||
idCommitment: Array.from(idCommitmentLE),
|
idCommitment: Array.from(IDCommitment),
|
||||||
idNullifier: Array.from(idNullifierLE),
|
idNullifier: Array.from(IDNullifier),
|
||||||
idSecretHash: Array.from(idSecretHashLE),
|
idSecretHash: Array.from(IDSecretHash),
|
||||||
idTrapdoor: Array.from(idTrapdoorLE)
|
idTrapdoor: Array.from(IDTrapdoor)
|
||||||
},
|
},
|
||||||
membershipContract: {
|
membershipContract: {
|
||||||
chainId: options.membership.chainId,
|
chainId: options.membership.chainId,
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
import { message } from "@waku/core";
|
|
||||||
import type {
|
|
||||||
IDecodedMessage,
|
|
||||||
IMessage,
|
|
||||||
IRateLimitProof,
|
|
||||||
IRlnMessage
|
|
||||||
} from "@waku/interfaces";
|
|
||||||
import * as utils from "@waku/utils/bytes";
|
|
||||||
|
|
||||||
import { RLNInstance } from "./rln.js";
|
|
||||||
import { epochBytesToInt } from "./utils/index.js";
|
|
||||||
|
|
||||||
export function toRLNSignal(contentTopic: string, msg: IMessage): Uint8Array {
|
|
||||||
const contentTopicBytes = utils.utf8ToBytes(contentTopic ?? "");
|
|
||||||
return new Uint8Array([...(msg.payload ?? []), ...contentTopicBytes]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RlnMessage<T extends IDecodedMessage> implements IRlnMessage {
|
|
||||||
public pubsubTopic = "";
|
|
||||||
public version = message.version_0.Version;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private rlnInstance: RLNInstance,
|
|
||||||
private msg: T,
|
|
||||||
public rateLimitProof: IRateLimitProof | undefined
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public verify(roots: Uint8Array[]): boolean | undefined {
|
|
||||||
return this.rateLimitProof
|
|
||||||
? this.rlnInstance.zerokit.verifyWithRoots(
|
|
||||||
this.rateLimitProof,
|
|
||||||
toRLNSignal(this.msg.contentTopic, this.msg),
|
|
||||||
roots
|
|
||||||
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyNoRoot(): boolean | undefined {
|
|
||||||
return this.rateLimitProof
|
|
||||||
? this.rlnInstance.zerokit.verifyWithNoRoot(
|
|
||||||
this.rateLimitProof,
|
|
||||||
toRLNSignal(this.msg.contentTopic, this.msg)
|
|
||||||
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get payload(): Uint8Array {
|
|
||||||
return this.msg.payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hash(): Uint8Array {
|
|
||||||
return this.msg.hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hashStr(): string {
|
|
||||||
return this.msg.hashStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get contentTopic(): string {
|
|
||||||
return this.msg.contentTopic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get timestamp(): Date | undefined {
|
|
||||||
return this.msg.timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get ephemeral(): boolean | undefined {
|
|
||||||
return this.msg.ephemeral;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get meta(): Uint8Array | undefined {
|
|
||||||
return this.msg.meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get epoch(): number | undefined {
|
|
||||||
const bytes = this.rateLimitProof?.epoch;
|
|
||||||
if (!bytes) return undefined;
|
|
||||||
|
|
||||||
return epochBytesToInt(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import type { IRateLimitProof } from "@waku/interfaces";
|
|
||||||
|
|
||||||
import { BytesUtils, poseidonHash } from "./utils/index.js";
|
|
||||||
|
|
||||||
const proofOffset = 128;
|
|
||||||
const rootOffset = proofOffset + 32;
|
|
||||||
const epochOffset = rootOffset + 32;
|
|
||||||
const shareXOffset = epochOffset + 32;
|
|
||||||
const shareYOffset = shareXOffset + 32;
|
|
||||||
const nullifierOffset = shareYOffset + 32;
|
|
||||||
const rlnIdentifierOffset = nullifierOffset + 32;
|
|
||||||
|
|
||||||
class ProofMetadata {
|
|
||||||
public constructor(
|
|
||||||
public readonly nullifier: Uint8Array,
|
|
||||||
public readonly shareX: Uint8Array,
|
|
||||||
public readonly shareY: Uint8Array,
|
|
||||||
public readonly externalNullifier: Uint8Array
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Proof implements IRateLimitProof {
|
|
||||||
public readonly proof: Uint8Array;
|
|
||||||
public readonly merkleRoot: Uint8Array;
|
|
||||||
public readonly epoch: Uint8Array;
|
|
||||||
public readonly shareX: Uint8Array;
|
|
||||||
public readonly shareY: Uint8Array;
|
|
||||||
public readonly nullifier: Uint8Array;
|
|
||||||
public readonly rlnIdentifier: Uint8Array;
|
|
||||||
|
|
||||||
public constructor(proofBytes: Uint8Array) {
|
|
||||||
if (proofBytes.length < rlnIdentifierOffset) {
|
|
||||||
throw new Error("invalid proof");
|
|
||||||
}
|
|
||||||
// parse the proof as proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32>
|
|
||||||
this.proof = proofBytes.subarray(0, proofOffset);
|
|
||||||
this.merkleRoot = proofBytes.subarray(proofOffset, rootOffset);
|
|
||||||
this.epoch = proofBytes.subarray(rootOffset, epochOffset);
|
|
||||||
this.shareX = proofBytes.subarray(epochOffset, shareXOffset);
|
|
||||||
this.shareY = proofBytes.subarray(shareXOffset, shareYOffset);
|
|
||||||
this.nullifier = proofBytes.subarray(shareYOffset, nullifierOffset);
|
|
||||||
this.rlnIdentifier = proofBytes.subarray(
|
|
||||||
nullifierOffset,
|
|
||||||
rlnIdentifierOffset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public extractMetadata(): ProofMetadata {
|
|
||||||
const externalNullifier = poseidonHash(this.epoch, this.rlnIdentifier);
|
|
||||||
return new ProofMetadata(
|
|
||||||
this.nullifier,
|
|
||||||
this.shareX,
|
|
||||||
this.shareY,
|
|
||||||
externalNullifier
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function proofToBytes(p: IRateLimitProof): Uint8Array {
|
|
||||||
return BytesUtils.concatenate(
|
|
||||||
p.proof,
|
|
||||||
p.merkleRoot,
|
|
||||||
p.epoch,
|
|
||||||
p.shareX,
|
|
||||||
p.shareY,
|
|
||||||
p.nullifier,
|
|
||||||
p.rlnIdentifier
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
13
packages/rln/src/resources/verification_key.d.ts
vendored
13
packages/rln/src/resources/verification_key.d.ts
vendored
@ -1,13 +0,0 @@
|
|||||||
declare const verificationKey: {
|
|
||||||
protocol: string;
|
|
||||||
curve: string;
|
|
||||||
nPublic: number;
|
|
||||||
vk_alpha_1: string[];
|
|
||||||
vk_beta_2: string[][];
|
|
||||||
vk_gamma_2: string[][];
|
|
||||||
vk_delta_2: string[][];
|
|
||||||
vk_alphabeta_12: string[][][];
|
|
||||||
IC: string[][];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default verificationKey;
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
const verificationKey = {
|
|
||||||
protocol: "groth16",
|
|
||||||
curve: "bn128",
|
|
||||||
nPublic: 6,
|
|
||||||
vk_alpha_1: [
|
|
||||||
"20124996762962216725442980738609010303800849578410091356605067053491763969391",
|
|
||||||
"9118593021526896828671519912099489027245924097793322973632351264852174143923",
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
vk_beta_2: [
|
|
||||||
[
|
|
||||||
"4693952934005375501364248788849686435240706020501681709396105298107971354382",
|
|
||||||
"14346958885444710485362620645446987998958218205939139994511461437152241966681"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"16851772916911573982706166384196538392731905827088356034885868448550849804972",
|
|
||||||
"823612331030938060799959717749043047845343400798220427319188951998582076532"
|
|
||||||
],
|
|
||||||
["1", "0"]
|
|
||||||
],
|
|
||||||
vk_gamma_2: [
|
|
||||||
[
|
|
||||||
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
|
|
||||||
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
|
|
||||||
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
|
|
||||||
],
|
|
||||||
["1", "0"]
|
|
||||||
],
|
|
||||||
vk_delta_2: [
|
|
||||||
[
|
|
||||||
"8353516066399360694538747105302262515182301251524941126222712285088022964076",
|
|
||||||
"9329524012539638256356482961742014315122377605267454801030953882967973561832"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"16805391589556134376869247619848130874761233086443465978238468412168162326401",
|
|
||||||
"10111259694977636294287802909665108497237922060047080343914303287629927847739"
|
|
||||||
],
|
|
||||||
["1", "0"]
|
|
||||||
],
|
|
||||||
vk_alphabeta_12: [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"12608968655665301215455851857466367636344427685631271961542642719683786103711",
|
|
||||||
"9849575605876329747382930567422916152871921500826003490242628251047652318086"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"6322029441245076030714726551623552073612922718416871603535535085523083939021",
|
|
||||||
"8700115492541474338049149013125102281865518624059015445617546140629435818912"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"10674973475340072635573101639867487770811074181475255667220644196793546640210",
|
|
||||||
"2926286967251299230490668407790788696102889214647256022788211245826267484824"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[
|
|
||||||
"9660441540778523475944706619139394922744328902833875392144658911530830074820",
|
|
||||||
"19548113127774514328631808547691096362144426239827206966690021428110281506546"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"1870837942477655969123169532603615788122896469891695773961478956740992497097",
|
|
||||||
"12536105729661705698805725105036536744930776470051238187456307227425796690780"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"21811903352654147452884857281720047789720483752548991551595462057142824037334",
|
|
||||||
"19021616763967199151052893283384285352200445499680068407023236283004353578353"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
IC: [
|
|
||||||
[
|
|
||||||
"11992897507809711711025355300535923222599547639134311050809253678876341466909",
|
|
||||||
"17181525095924075896332561978747020491074338784673526378866503154966799128110",
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"17018665030246167677911144513385572506766200776123272044534328594850561667818",
|
|
||||||
"18601114175490465275436712413925513066546725461375425769709566180981674884464",
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"18799470100699658367834559797874857804183288553462108031963980039244731716542",
|
|
||||||
"13064227487174191981628537974951887429496059857753101852163607049188825592007",
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"17432501889058124609368103715904104425610382063762621017593209214189134571156",
|
|
||||||
"13406815149699834788256141097399354592751313348962590382887503595131085938635",
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"10320964835612716439094703312987075811498239445882526576970512041988148264481",
|
|
||||||
"9024164961646353611176283204118089412001502110138072989569118393359029324867",
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"718355081067365548229685160476620267257521491773976402837645005858953849298",
|
|
||||||
"14635482993933988261008156660773180150752190597753512086153001683711587601974",
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"11777720285956632126519898515392071627539405001940313098390150593689568177535",
|
|
||||||
"8483603647274280691250972408211651407952870456587066148445913156086740744515",
|
|
||||||
"1"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export default verificationKey;
|
|
||||||
@ -1,11 +1,25 @@
|
|||||||
export async function builder(
|
export const builder: (
|
||||||
code: Uint8Array,
|
code: Uint8Array,
|
||||||
sanityCheck: boolean
|
sanityCheck?: boolean
|
||||||
): Promise<WitnessCalculator>;
|
) => Promise<WitnessCalculator>;
|
||||||
|
|
||||||
export class WitnessCalculator {
|
export class WitnessCalculator {
|
||||||
public calculateWitness(
|
constructor(instance: any, sanityCheck?: boolean);
|
||||||
input: unknown,
|
|
||||||
sanityCheck: boolean
|
circom_version(): number;
|
||||||
): Promise<Array<bigint>>;
|
|
||||||
|
calculateWitness(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
sanityCheck?: boolean
|
||||||
|
): Promise<bigint[]>;
|
||||||
|
|
||||||
|
calculateBinWitness(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
sanityCheck?: boolean
|
||||||
|
): Promise<Uint8Array>;
|
||||||
|
|
||||||
|
calculateWTNSBin(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
sanityCheck?: boolean
|
||||||
|
): Promise<Uint8Array>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// File generated with https://github.com/iden3/circom
|
// File generated with https://github.com/iden3/circom
|
||||||
// following the instructions from:
|
// following the instructions from:
|
||||||
// https://github.com/vacp2p/zerokit/tree/master/rln#compiling-circuits
|
// https://github.com/vacp2p/zerokit/tree/master/rln#advanced-custom-circuit-compilation
|
||||||
|
|
||||||
export async function builder(code, options) {
|
export async function builder(code, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|||||||
@ -1,37 +1,14 @@
|
|||||||
import { createDecoder, createEncoder } from "@waku/core";
|
|
||||||
import type {
|
|
||||||
ContentTopic,
|
|
||||||
IDecodedMessage,
|
|
||||||
IRoutingInfo,
|
|
||||||
EncoderOptions as WakuEncoderOptions
|
|
||||||
} from "@waku/interfaces";
|
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
import init from "@waku/zerokit-rln-wasm";
|
import init, * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
|
||||||
|
|
||||||
import {
|
|
||||||
createRLNDecoder,
|
|
||||||
createRLNEncoder,
|
|
||||||
type RLNDecoder,
|
|
||||||
type RLNEncoder
|
|
||||||
} from "./codec.js";
|
|
||||||
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
||||||
import { RLNCredentialsManager } from "./credentials_manager.js";
|
import { RLNCredentialsManager } from "./credentials_manager.js";
|
||||||
import type {
|
|
||||||
DecryptedCredentials,
|
|
||||||
EncryptedCredentials
|
|
||||||
} from "./keystore/index.js";
|
|
||||||
import verificationKey from "./resources/verification_key";
|
|
||||||
import * as wc from "./resources/witness_calculator";
|
import * as wc from "./resources/witness_calculator";
|
||||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||||
import { Zerokit } from "./zerokit.js";
|
import { Zerokit } from "./zerokit.js";
|
||||||
|
|
||||||
const log = new Logger("rln");
|
const log = new Logger("rln");
|
||||||
|
|
||||||
type WakuRLNEncoderOptions = WakuEncoderOptions & {
|
|
||||||
credentials: EncryptedCredentials | DecryptedCredentials;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RLNInstance extends RLNCredentialsManager {
|
export class RLNInstance extends RLNCredentialsManager {
|
||||||
/**
|
/**
|
||||||
* Create an instance of RLN
|
* Create an instance of RLN
|
||||||
@ -39,18 +16,13 @@ export class RLNInstance extends RLNCredentialsManager {
|
|||||||
*/
|
*/
|
||||||
public static async create(): Promise<RLNInstance> {
|
public static async create(): Promise<RLNInstance> {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
await init();
|
||||||
await (init as any)?.();
|
zerokitRLN.initPanicHook();
|
||||||
zerokitRLN.init_panic_hook();
|
|
||||||
|
|
||||||
const witnessCalculator = await RLNInstance.loadWitnessCalculator();
|
const witnessCalculator = await RLNInstance.loadWitnessCalculator();
|
||||||
const zkey = await RLNInstance.loadZkey();
|
const zkey = await RLNInstance.loadZkey();
|
||||||
|
|
||||||
const stringEncoder = new TextEncoder();
|
const zkRLN = zerokitRLN.newRLN(zkey);
|
||||||
const vkey = stringEncoder.encode(JSON.stringify(verificationKey));
|
|
||||||
|
|
||||||
const DEPTH = 20;
|
|
||||||
const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey);
|
|
||||||
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
|
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
|
||||||
|
|
||||||
return new RLNInstance(zerokit);
|
return new RLNInstance(zerokit);
|
||||||
@ -64,39 +36,6 @@ export class RLNInstance extends RLNCredentialsManager {
|
|||||||
super(zerokit);
|
super(zerokit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createEncoder(
|
|
||||||
options: WakuRLNEncoderOptions
|
|
||||||
): Promise<RLNEncoder> {
|
|
||||||
const { credentials: decryptedCredentials } =
|
|
||||||
await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
|
|
||||||
const credentials = decryptedCredentials || this.credentials;
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
throw Error(
|
|
||||||
"Failed to create Encoder: missing RLN credentials. Use createRLNEncoder directly."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.verifyCredentialsAgainstContract(credentials);
|
|
||||||
|
|
||||||
return createRLNEncoder({
|
|
||||||
encoder: createEncoder(options),
|
|
||||||
rlnInstance: this,
|
|
||||||
index: credentials.membership.treeIndex,
|
|
||||||
credential: credentials.identity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public createDecoder(
|
|
||||||
contentTopic: ContentTopic,
|
|
||||||
routingInfo: IRoutingInfo
|
|
||||||
): RLNDecoder<IDecodedMessage> {
|
|
||||||
return createRLNDecoder({
|
|
||||||
rlnInstance: this,
|
|
||||||
decoder: createDecoder(contentTopic, routingInfo)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async loadWitnessCalculator(): Promise<WitnessCalculator> {
|
public static async loadWitnessCalculator(): Promise<WitnessCalculator> {
|
||||||
try {
|
try {
|
||||||
const url = new URL("./resources/rln.wasm", import.meta.url);
|
const url = new URL("./resources/rln.wasm", import.meta.url);
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
import { assert, expect } from "chai";
|
|
||||||
|
|
||||||
import { MerkleRootTracker } from "./root_tracker.js";
|
|
||||||
|
|
||||||
describe("js-rln", () => {
|
|
||||||
it("should track merkle roots and backfill from block number", async function () {
|
|
||||||
const acceptableRootWindow = 3;
|
|
||||||
|
|
||||||
const tracker = new MerkleRootTracker(
|
|
||||||
acceptableRootWindow,
|
|
||||||
new Uint8Array([0, 0, 0, 0])
|
|
||||||
);
|
|
||||||
expect(tracker.roots()).to.have.length(1);
|
|
||||||
expect(tracker.buffer()).to.have.length(0);
|
|
||||||
expect(tracker.roots()[0]).to.deep.equal(new Uint8Array([0, 0, 0, 0]));
|
|
||||||
|
|
||||||
for (let i = 1; i <= 30; i++) {
|
|
||||||
tracker.pushRoot(i, new Uint8Array([0, 0, 0, i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(tracker.roots()).to.have.length(acceptableRootWindow);
|
|
||||||
expect(tracker.buffer()).to.have.length(20);
|
|
||||||
assert.sameDeepMembers(tracker.roots(), [
|
|
||||||
new Uint8Array([0, 0, 0, 30]),
|
|
||||||
new Uint8Array([0, 0, 0, 29]),
|
|
||||||
new Uint8Array([0, 0, 0, 28])
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Buffer should keep track of 20 blocks previous to the current valid merkle root window
|
|
||||||
expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8]));
|
|
||||||
expect(tracker.buffer()[19]).to.be.eql(new Uint8Array([0, 0, 0, 27]));
|
|
||||||
|
|
||||||
// Remove roots 29 and 30
|
|
||||||
tracker.backFill(29);
|
|
||||||
assert.sameDeepMembers(tracker.roots(), [
|
|
||||||
new Uint8Array([0, 0, 0, 28]),
|
|
||||||
new Uint8Array([0, 0, 0, 27]),
|
|
||||||
new Uint8Array([0, 0, 0, 26])
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(tracker.buffer()).to.have.length(18);
|
|
||||||
expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8]));
|
|
||||||
expect(tracker.buffer()[17]).to.be.eql(new Uint8Array([0, 0, 0, 25]));
|
|
||||||
|
|
||||||
// Remove roots from block 15 onwards. These blocks exists within the buffer
|
|
||||||
tracker.backFill(15);
|
|
||||||
assert.sameDeepMembers(tracker.roots(), [
|
|
||||||
new Uint8Array([0, 0, 0, 14]),
|
|
||||||
new Uint8Array([0, 0, 0, 13]),
|
|
||||||
new Uint8Array([0, 0, 0, 12])
|
|
||||||
]);
|
|
||||||
expect(tracker.buffer()).to.have.length(4);
|
|
||||||
expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8]));
|
|
||||||
expect(tracker.buffer()[3]).to.be.eql(new Uint8Array([0, 0, 0, 11]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
class RootPerBlock {
|
|
||||||
public constructor(
|
|
||||||
public root: Uint8Array,
|
|
||||||
public blockNumber: number
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxBufferSize = 20;
|
|
||||||
|
|
||||||
export class MerkleRootTracker {
|
|
||||||
private validMerkleRoots: Array<RootPerBlock> = new Array<RootPerBlock>();
|
|
||||||
private merkleRootBuffer: Array<RootPerBlock> = new Array<RootPerBlock>();
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private acceptableRootWindowSize: number,
|
|
||||||
initialRoot: Uint8Array
|
|
||||||
) {
|
|
||||||
this.pushRoot(0, initialRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
public backFill(fromBlockNumber: number): void {
|
|
||||||
if (this.validMerkleRoots.length == 0) return;
|
|
||||||
|
|
||||||
let numBlocks = 0;
|
|
||||||
for (let i = this.validMerkleRoots.length - 1; i >= 0; i--) {
|
|
||||||
if (this.validMerkleRoots[i].blockNumber >= fromBlockNumber) {
|
|
||||||
numBlocks++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numBlocks == 0) return;
|
|
||||||
|
|
||||||
const olderBlock = fromBlockNumber < this.validMerkleRoots[0].blockNumber;
|
|
||||||
|
|
||||||
// Remove last roots
|
|
||||||
let rootsToPop = numBlocks;
|
|
||||||
if (this.validMerkleRoots.length < rootsToPop) {
|
|
||||||
rootsToPop = this.validMerkleRoots.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.validMerkleRoots = this.validMerkleRoots.slice(
|
|
||||||
0,
|
|
||||||
this.validMerkleRoots.length - rootsToPop
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.merkleRootBuffer.length == 0) return;
|
|
||||||
|
|
||||||
if (olderBlock) {
|
|
||||||
const idx = this.merkleRootBuffer.findIndex(
|
|
||||||
(x) => x.blockNumber == fromBlockNumber
|
|
||||||
);
|
|
||||||
if (idx > -1) {
|
|
||||||
this.merkleRootBuffer = this.merkleRootBuffer.slice(0, idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backfill the tree's acceptable roots
|
|
||||||
let rootsToRestore =
|
|
||||||
this.acceptableRootWindowSize - this.validMerkleRoots.length;
|
|
||||||
if (this.merkleRootBuffer.length < rootsToRestore) {
|
|
||||||
rootsToRestore = this.merkleRootBuffer.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < rootsToRestore; i++) {
|
|
||||||
const x = this.merkleRootBuffer.pop();
|
|
||||||
if (x) this.validMerkleRoots.unshift(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public pushRoot(blockNumber: number, root: Uint8Array): void {
|
|
||||||
this.validMerkleRoots.push(new RootPerBlock(root, blockNumber));
|
|
||||||
|
|
||||||
// Maintain valid merkle root window
|
|
||||||
if (this.validMerkleRoots.length > this.acceptableRootWindowSize) {
|
|
||||||
const x = this.validMerkleRoots.shift();
|
|
||||||
if (x) this.merkleRootBuffer.push(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maintain merkle root buffer
|
|
||||||
if (this.merkleRootBuffer.length > maxBufferSize) {
|
|
||||||
this.merkleRootBuffer.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public roots(): Array<Uint8Array> {
|
|
||||||
return this.validMerkleRoots.map((x) => x.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
public buffer(): Array<Uint8Array> {
|
|
||||||
return this.merkleRootBuffer.map((x) => x.root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +1,52 @@
|
|||||||
export class BytesUtils {
|
export class BytesUtils {
|
||||||
/**
|
/**
|
||||||
* Switches endianness of a byte array
|
* Concatenate Uint8Arrays
|
||||||
|
* @param input
|
||||||
|
* @returns concatenation of all Uint8Array received as input
|
||||||
*/
|
*/
|
||||||
public static switchEndianness(bytes: Uint8Array): Uint8Array {
|
public static concatenate(...input: Uint8Array[]): Uint8Array {
|
||||||
return new Uint8Array([...bytes].reverse());
|
let totalLength = 0;
|
||||||
}
|
for (const arr of input) {
|
||||||
|
totalLength += arr.length;
|
||||||
/**
|
}
|
||||||
* Builds a BigInt from a big-endian Uint8Array
|
const result = new Uint8Array(totalLength);
|
||||||
* @param bytes The big-endian bytes to convert
|
let offset = 0;
|
||||||
* @returns The resulting BigInt in big-endian format
|
for (const arr of input) {
|
||||||
*/
|
result.set(arr, offset);
|
||||||
public static buildBigIntFromUint8ArrayBE(bytes: Uint8Array): bigint {
|
offset += arr.length;
|
||||||
let result = 0n;
|
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
|
||||||
result = (result << 8n) + BigInt(bytes[i]);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches endianness of a bigint value
|
* Convert a Uint8Array to a BigInt with configurable input endianness
|
||||||
* @param value The bigint value to switch endianness for
|
* @param bytes - The byte array to convert
|
||||||
* @returns The bigint value with reversed endianness
|
* @param inputEndianness - Endianness of the input bytes ('big' or 'little')
|
||||||
|
* @returns BigInt representation of the bytes
|
||||||
*/
|
*/
|
||||||
public static switchEndiannessBigInt(value: bigint): bigint {
|
public static toBigInt(
|
||||||
// Convert bigint to byte array
|
bytes: Uint8Array,
|
||||||
const bytes = [];
|
inputEndianness: "big" | "little" = "little"
|
||||||
let tempValue = value;
|
): bigint {
|
||||||
while (tempValue > 0n) {
|
if (bytes.length === 0) {
|
||||||
bytes.push(Number(tempValue & 0xffn));
|
return 0n;
|
||||||
tempValue >>= 8n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reverse bytes and convert back to bigint
|
// Create a copy to avoid modifying the original array
|
||||||
return bytes
|
const workingBytes = new Uint8Array(bytes);
|
||||||
.reverse()
|
|
||||||
.reduce((acc, byte) => (acc << 8n) + BigInt(byte), 0n);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Reverse bytes if input is little-endian to work with big-endian internally
|
||||||
* Converts a big-endian bigint to a 32-byte big-endian Uint8Array
|
if (inputEndianness === "little") {
|
||||||
* @param value The big-endian bigint to convert
|
workingBytes.reverse();
|
||||||
* @returns A 32-byte big-endian Uint8Array
|
|
||||||
*/
|
|
||||||
public static bigIntToUint8Array32BE(value: bigint): Uint8Array {
|
|
||||||
const bytes = new Uint8Array(32);
|
|
||||||
for (let i = 31; i >= 0; i--) {
|
|
||||||
bytes[i] = Number(value & 0xffn);
|
|
||||||
value >>= 8n;
|
|
||||||
}
|
}
|
||||||
return bytes;
|
|
||||||
|
// Convert to BigInt
|
||||||
|
let result = 0n;
|
||||||
|
for (let i = 0; i < workingBytes.length; i++) {
|
||||||
|
result = (result << 8n) | BigInt(workingBytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,20 +77,6 @@ export class BytesUtils {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills with zeros to set length
|
|
||||||
* @param array little endian Uint8Array
|
|
||||||
* @param length amount to pad
|
|
||||||
* @returns little endian Uint8Array padded with zeros to set length
|
|
||||||
*/
|
|
||||||
public static zeroPadLE(array: Uint8Array, length: number): Uint8Array {
|
|
||||||
const result = new Uint8Array(length);
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result[i] = array[i] || 0;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adapted from https://github.com/feross/buffer
|
// Adapted from https://github.com/feross/buffer
|
||||||
public static checkInt(
|
public static checkInt(
|
||||||
buf: Uint8Array,
|
buf: Uint8Array,
|
||||||
@ -108,23 +90,4 @@ export class BytesUtils {
|
|||||||
throw new RangeError('"value" argument is out of bounds');
|
throw new RangeError('"value" argument is out of bounds');
|
||||||
if (offset + ext > buf.length) throw new RangeError("Index out of range");
|
if (offset + ext > buf.length) throw new RangeError("Index out of range");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Concatenate Uint8Arrays
|
|
||||||
* @param input
|
|
||||||
* @returns concatenation of all Uint8Array received as input
|
|
||||||
*/
|
|
||||||
public static concatenate(...input: Uint8Array[]): Uint8Array {
|
|
||||||
let totalLength = 0;
|
|
||||||
for (const arr of input) {
|
|
||||||
totalLength += arr.length;
|
|
||||||
}
|
|
||||||
const result = new Uint8Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const arr of input) {
|
|
||||||
result.set(arr, offset);
|
|
||||||
offset += arr.length;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
packages/rln/src/zerokit.spec.ts
Normal file
26
packages/rln/src/zerokit.spec.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { RLNInstance } from "./rln.js";
|
||||||
|
|
||||||
|
describe("@waku/rln", () => {
|
||||||
|
it("should generate the same membership key if the same seed is provided", async function () {
|
||||||
|
const rlnInstance = await RLNInstance.create();
|
||||||
|
|
||||||
|
const seed = "This is a test seed";
|
||||||
|
const memKeys1 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
||||||
|
const memKeys2 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
||||||
|
|
||||||
|
memKeys1.IDCommitment.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDCommitment[index]);
|
||||||
|
});
|
||||||
|
memKeys1.IDNullifier.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDNullifier[index]);
|
||||||
|
});
|
||||||
|
memKeys1.IDSecretHash.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDSecretHash[index]);
|
||||||
|
});
|
||||||
|
memKeys1.IDTrapdoor.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDTrapdoor[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,11 +1,8 @@
|
|||||||
import type { IRateLimitProof } from "@waku/interfaces";
|
|
||||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||||
|
|
||||||
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./contract/constants.js";
|
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
||||||
import { IdentityCredential } from "./identity.js";
|
import { IdentityCredential } from "./identity.js";
|
||||||
import { Proof, proofToBytes } from "./proof.js";
|
|
||||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||||
import { BytesUtils, dateToEpoch, epochIntToBytes } from "./utils/index.js";
|
|
||||||
|
|
||||||
export class Zerokit {
|
export class Zerokit {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -26,226 +23,13 @@ export class Zerokit {
|
|||||||
return this._rateLimit;
|
return this._rateLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateIdentityCredentials(): IdentityCredential {
|
|
||||||
const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm
|
|
||||||
return IdentityCredential.fromBytes(memKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateSeededIdentityCredential(seed: string): IdentityCredential {
|
public generateSeededIdentityCredential(seed: string): IdentityCredential {
|
||||||
const stringEncoder = new TextEncoder();
|
const stringEncoder = new TextEncoder();
|
||||||
const seedBytes = stringEncoder.encode(seed);
|
const seedBytes = stringEncoder.encode(seed);
|
||||||
// TODO: rename this function in zerokit rln-wasm
|
|
||||||
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
|
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
|
||||||
this.zkRLN,
|
this.zkRLN,
|
||||||
seedBytes
|
seedBytes
|
||||||
);
|
);
|
||||||
return IdentityCredential.fromBytes(memKeys);
|
return IdentityCredential.fromBytes(memKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public insertMember(idCommitment: Uint8Array): void {
|
|
||||||
zerokitRLN.insertMember(this.zkRLN, idCommitment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public insertMembers(
|
|
||||||
index: number,
|
|
||||||
...idCommitments: Array<Uint8Array>
|
|
||||||
): void {
|
|
||||||
// serializes a seq of IDCommitments to a byte seq
|
|
||||||
// the order of serialization is |id_commitment_len<8>|id_commitment<var>|
|
|
||||||
const idCommitmentLen = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
idCommitments.length,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
const idCommitmentBytes = BytesUtils.concatenate(
|
|
||||||
idCommitmentLen,
|
|
||||||
...idCommitments
|
|
||||||
);
|
|
||||||
zerokitRLN.setLeavesFrom(this.zkRLN, index, idCommitmentBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteMember(index: number): void {
|
|
||||||
zerokitRLN.deleteLeaf(this.zkRLN, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMerkleRoot(): Uint8Array {
|
|
||||||
return zerokitRLN.getRoot(this.zkRLN);
|
|
||||||
}
|
|
||||||
|
|
||||||
public serializeMessage(
|
|
||||||
uint8Msg: Uint8Array,
|
|
||||||
memIndex: number,
|
|
||||||
epoch: Uint8Array,
|
|
||||||
idKey: Uint8Array,
|
|
||||||
rateLimit?: number
|
|
||||||
): Uint8Array {
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
uint8Msg.length,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
const memIndexBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
memIndex,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
// [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> | rate_limit<8> ]
|
|
||||||
return BytesUtils.concatenate(
|
|
||||||
idKey,
|
|
||||||
memIndexBytes,
|
|
||||||
epoch,
|
|
||||||
msgLen,
|
|
||||||
uint8Msg,
|
|
||||||
rateLimitBytes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async generateRLNProof(
|
|
||||||
msg: Uint8Array,
|
|
||||||
index: number,
|
|
||||||
epoch: Uint8Array | Date | undefined,
|
|
||||||
idSecretHash: Uint8Array,
|
|
||||||
rateLimit?: number
|
|
||||||
): Promise<IRateLimitProof> {
|
|
||||||
if (epoch === undefined) {
|
|
||||||
epoch = epochIntToBytes(dateToEpoch(new Date()));
|
|
||||||
} else if (epoch instanceof Date) {
|
|
||||||
epoch = epochIntToBytes(dateToEpoch(epoch));
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectiveRateLimit = rateLimit ?? this.rateLimit;
|
|
||||||
|
|
||||||
if (epoch.length !== 32) throw new Error("invalid epoch");
|
|
||||||
if (idSecretHash.length !== 32) throw new Error("invalid id secret hash");
|
|
||||||
if (index < 0) throw new Error("index must be >= 0");
|
|
||||||
if (
|
|
||||||
effectiveRateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
|
||||||
effectiveRateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serialized_msg = this.serializeMessage(
|
|
||||||
msg,
|
|
||||||
index,
|
|
||||||
epoch,
|
|
||||||
idSecretHash,
|
|
||||||
effectiveRateLimit
|
|
||||||
);
|
|
||||||
const rlnWitness = zerokitRLN.getSerializedRLNWitness(
|
|
||||||
this.zkRLN,
|
|
||||||
serialized_msg
|
|
||||||
);
|
|
||||||
const inputs = zerokitRLN.RLNWitnessToJson(this.zkRLN, rlnWitness);
|
|
||||||
const calculatedWitness = await this.witnessCalculator.calculateWitness(
|
|
||||||
inputs,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const proofBytes = zerokitRLN.generate_rln_proof_with_witness(
|
|
||||||
this.zkRLN,
|
|
||||||
calculatedWitness,
|
|
||||||
rlnWitness
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Proof(proofBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyRLNProof(
|
|
||||||
proof: IRateLimitProof | Uint8Array,
|
|
||||||
msg: Uint8Array,
|
|
||||||
rateLimit?: number
|
|
||||||
): boolean {
|
|
||||||
let pBytes: Uint8Array;
|
|
||||||
if (proof instanceof Uint8Array) {
|
|
||||||
pBytes = proof;
|
|
||||||
} else {
|
|
||||||
pBytes = proofToBytes(proof);
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
return zerokitRLN.verifyRLNProof(
|
|
||||||
this.zkRLN,
|
|
||||||
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyWithRoots(
|
|
||||||
proof: IRateLimitProof | Uint8Array,
|
|
||||||
msg: Uint8Array,
|
|
||||||
roots: Array<Uint8Array>,
|
|
||||||
rateLimit?: number
|
|
||||||
): boolean {
|
|
||||||
let pBytes: Uint8Array;
|
|
||||||
if (proof instanceof Uint8Array) {
|
|
||||||
pBytes = proof;
|
|
||||||
} else {
|
|
||||||
pBytes = proofToBytes(proof);
|
|
||||||
}
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
const rootsBytes = BytesUtils.concatenate(...roots);
|
|
||||||
|
|
||||||
return zerokitRLN.verifyWithRoots(
|
|
||||||
this.zkRLN,
|
|
||||||
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes),
|
|
||||||
rootsBytes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyWithNoRoot(
|
|
||||||
proof: IRateLimitProof | Uint8Array,
|
|
||||||
msg: Uint8Array,
|
|
||||||
rateLimit?: number
|
|
||||||
): boolean {
|
|
||||||
let pBytes: Uint8Array;
|
|
||||||
if (proof instanceof Uint8Array) {
|
|
||||||
pBytes = proof;
|
|
||||||
} else {
|
|
||||||
pBytes = proofToBytes(proof);
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
return zerokitRLN.verifyWithRoots(
|
|
||||||
this.zkRLN,
|
|
||||||
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes),
|
|
||||||
new Uint8Array()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* @waku/interfaces bumped from 0.0.19 to 0.0.20
|
* @waku/interfaces bumped from 0.0.19 to 0.0.20
|
||||||
* @waku/peer-exchange bumped from ^0.0.17 to ^0.0.18
|
* @waku/peer-exchange bumped from ^0.0.17 to ^0.0.18
|
||||||
|
|
||||||
|
## [0.0.35](https://github.com/waku-org/js-waku/compare/sdk-v0.0.34...sdk-v0.0.35) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add debounce to health indicator ([#2594](https://github.com/waku-org/js-waku/issues/2594)) ([a7f30b1](https://github.com/waku-org/js-waku/commit/a7f30b121143454340aa7b3aeb4f55470905c54d))
|
||||||
|
* Add start/stop to filter ([#2592](https://github.com/waku-org/js-waku/issues/2592)) ([2fba052](https://github.com/waku-org/js-waku/commit/2fba052b8b98cb64f6383de95d01b33beb771448))
|
||||||
|
* Expose message hash from IDecodedMessage ([#2578](https://github.com/waku-org/js-waku/issues/2578)) ([836d6b8](https://github.com/waku-org/js-waku/commit/836d6b8793a5124747684f6ea76b6dd47c73048b))
|
||||||
|
* Implement lp-v3 error codes with backwards compatibility ([#2501](https://github.com/waku-org/js-waku/issues/2501)) ([1625302](https://github.com/waku-org/js-waku/commit/16253026c6e30052d87d9975b58480951de469d8))
|
||||||
|
* Implement peer-store re-bootstrapping ([#2641](https://github.com/waku-org/js-waku/issues/2641)) ([11d84ad](https://github.com/waku-org/js-waku/commit/11d84ad342fe45158ef0734f9ca070f14704503f))
|
||||||
|
* Introduce reliable channels ([#2526](https://github.com/waku-org/js-waku/issues/2526)) ([4d5c152](https://github.com/waku-org/js-waku/commit/4d5c152f5b1b1c241bbe7bb96d13d927a6f7550e))
|
||||||
|
* Query on connect ([#2602](https://github.com/waku-org/js-waku/issues/2602)) ([8542d04](https://github.com/waku-org/js-waku/commit/8542d04bf5c9472f955ef8c9e5bc9e89c70f4738))
|
||||||
|
* StoreConnect events ([#2601](https://github.com/waku-org/js-waku/issues/2601)) ([0dfbcf6](https://github.com/waku-org/js-waku/commit/0dfbcf6b6bd9225dcb0dec540aeb1eb2703c8397))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* (sds) ensure incoming messages have their retrieval hint stored ([#2604](https://github.com/waku-org/js-waku/issues/2604)) ([914beb6](https://github.com/waku-org/js-waku/commit/914beb6531a84f8c11ca951721225d47f9e6c285))
|
||||||
|
* Make health events emission consistent ([#2570](https://github.com/waku-org/js-waku/issues/2570)) ([c8dfdb1](https://github.com/waku-org/js-waku/commit/c8dfdb1ace8f0f8f668d8f2bb6e0eaed90041782))
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/core bumped from 0.0.38 to 0.0.39
|
||||||
|
* @waku/discovery bumped from 0.0.11 to 0.0.12
|
||||||
|
* @waku/interfaces bumped from 0.0.33 to 0.0.34
|
||||||
|
* @waku/proto bumped from ^0.0.13 to ^0.0.14
|
||||||
|
* @waku/sds bumped from ^0.0.6 to ^0.0.7
|
||||||
|
* @waku/utils bumped from 0.0.26 to 0.0.27
|
||||||
|
* devDependencies
|
||||||
|
* @waku/message-encryption bumped from ^0.0.36 to ^0.0.37
|
||||||
|
|
||||||
## [0.0.34](https://github.com/waku-org/js-waku/compare/sdk-v0.0.33...sdk-v0.0.34) (2025-08-14)
|
## [0.0.34](https://github.com/waku-org/js-waku/compare/sdk-v0.0.33...sdk-v0.0.34) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@waku/sdk",
|
"name": "@waku/sdk",
|
||||||
"version": "0.0.34",
|
"version": "0.0.35",
|
||||||
"description": "A unified SDK for easy creation and management of js-waku nodes.",
|
"description": "A unified SDK for easy creation and management of js-waku nodes.",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@ -67,22 +67,26 @@
|
|||||||
"@libp2p/ping": "2.0.35",
|
"@libp2p/ping": "2.0.35",
|
||||||
"@libp2p/websockets": "9.2.16",
|
"@libp2p/websockets": "9.2.16",
|
||||||
"@noble/hashes": "^1.3.3",
|
"@noble/hashes": "^1.3.3",
|
||||||
"@waku/core": "0.0.38",
|
"@types/lodash.debounce": "^4.0.9",
|
||||||
"@waku/discovery": "0.0.11",
|
"@waku/core": "0.0.39",
|
||||||
"@waku/interfaces": "0.0.33",
|
"@waku/discovery": "0.0.12",
|
||||||
"@waku/proto": "^0.0.13",
|
"@waku/interfaces": "0.0.34",
|
||||||
"@waku/utils": "0.0.26",
|
"@waku/proto": "^0.0.14",
|
||||||
"libp2p": "2.8.11"
|
"@waku/sds": "^0.0.7",
|
||||||
|
"@waku/utils": "0.0.27",
|
||||||
|
"libp2p": "2.8.11",
|
||||||
|
"lodash.debounce": "^4.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@libp2p/interface": "2.10.4",
|
"@libp2p/interface": "2.10.4",
|
||||||
"@types/chai": "^4.3.11",
|
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
"@rollup/plugin-json": "^6.0.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-replace": "^5.0.5",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
|
"@types/chai": "^4.3.11",
|
||||||
"@types/mocha": "^10.0.9",
|
"@types/mocha": "^10.0.9",
|
||||||
"@waku/build-utils": "*",
|
"@waku/build-utils": "*",
|
||||||
|
"@waku/message-encryption": "^0.0.37",
|
||||||
"chai": "^5.1.1",
|
"chai": "^5.1.1",
|
||||||
"cspell": "^8.6.1",
|
"cspell": "^8.6.1",
|
||||||
"interface-datastore": "8.3.2",
|
"interface-datastore": "8.3.2",
|
||||||
@ -104,4 +108,4 @@
|
|||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md"
|
"README.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,14 @@ export class Filter implements IFilter {
|
|||||||
return this.protocol.multicodec;
|
return this.protocol.multicodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
await this.protocol.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
await this.protocol.stop();
|
||||||
|
}
|
||||||
|
|
||||||
public unsubscribeAll(): void {
|
public unsubscribeAll(): void {
|
||||||
for (const subscription of this.subscriptions.values()) {
|
for (const subscription of this.subscriptions.values()) {
|
||||||
subscription.stop();
|
subscription.stop();
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
WakuEvent
|
WakuEvent
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
type PeerEvent<T> = (_event: CustomEvent<T>) => void;
|
type PeerEvent<T> = (_event: CustomEvent<T>) => void;
|
||||||
|
|
||||||
@ -24,10 +25,13 @@ interface IHealthIndicator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class HealthIndicator implements IHealthIndicator {
|
export class HealthIndicator implements IHealthIndicator {
|
||||||
|
private isStarted = false;
|
||||||
|
|
||||||
private readonly libp2p: Libp2p;
|
private readonly libp2p: Libp2p;
|
||||||
private readonly events: IWakuEventEmitter;
|
private readonly events: IWakuEventEmitter;
|
||||||
|
|
||||||
private value: HealthStatus = HealthStatus.Unhealthy;
|
private value: HealthStatus = HealthStatus.Unhealthy;
|
||||||
|
private readonly debouncedAssessHealth: ReturnType<typeof debounce>;
|
||||||
|
|
||||||
public constructor(params: HealthIndicatorParams) {
|
public constructor(params: HealthIndicatorParams) {
|
||||||
this.libp2p = params.libp2p;
|
this.libp2p = params.libp2p;
|
||||||
@ -35,9 +39,18 @@ export class HealthIndicator implements IHealthIndicator {
|
|||||||
|
|
||||||
this.onPeerIdentify = this.onPeerIdentify.bind(this);
|
this.onPeerIdentify = this.onPeerIdentify.bind(this);
|
||||||
this.onPeerDisconnected = this.onPeerDisconnected.bind(this);
|
this.onPeerDisconnected = this.onPeerDisconnected.bind(this);
|
||||||
|
|
||||||
|
this.debouncedAssessHealth = debounce(() => {
|
||||||
|
void this.assessHealth();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
|
if (this.isStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isStarted = true;
|
||||||
log.info("start: adding listeners to libp2p");
|
log.info("start: adding listeners to libp2p");
|
||||||
|
|
||||||
this.libp2p.addEventListener(
|
this.libp2p.addEventListener(
|
||||||
@ -49,10 +62,15 @@ export class HealthIndicator implements IHealthIndicator {
|
|||||||
this.onPeerDisconnected as PeerEvent<PeerId>
|
this.onPeerDisconnected as PeerEvent<PeerId>
|
||||||
);
|
);
|
||||||
|
|
||||||
void this.assessHealth();
|
this.debouncedAssessHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
|
if (!this.isStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isStarted = false;
|
||||||
log.info("stop: removing listeners to libp2p");
|
log.info("stop: removing listeners to libp2p");
|
||||||
|
|
||||||
this.libp2p.removeEventListener(
|
this.libp2p.removeEventListener(
|
||||||
@ -63,22 +81,22 @@ export class HealthIndicator implements IHealthIndicator {
|
|||||||
"peer:disconnect",
|
"peer:disconnect",
|
||||||
this.onPeerDisconnected as PeerEvent<PeerId>
|
this.onPeerDisconnected as PeerEvent<PeerId>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.debouncedAssessHealth.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toValue(): HealthStatus {
|
public toValue(): HealthStatus {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onPeerDisconnected(_event: CustomEvent<PeerId>): Promise<void> {
|
private onPeerDisconnected(_event: CustomEvent<PeerId>): void {
|
||||||
log.info(`onPeerDisconnected: received libp2p event`);
|
log.info(`onPeerDisconnected: received libp2p event`);
|
||||||
await this.assessHealth();
|
this.debouncedAssessHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onPeerIdentify(
|
private onPeerIdentify(_event: CustomEvent<IdentifyResult>): void {
|
||||||
_event: CustomEvent<IdentifyResult>
|
|
||||||
): Promise<void> {
|
|
||||||
log.info(`onPeerIdentify: received libp2p event`);
|
log.info(`onPeerIdentify: received libp2p event`);
|
||||||
await this.assessHealth();
|
this.debouncedAssessHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async assessHealth(): Promise<void> {
|
private async assessHealth(): Promise<void> {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export {
|
|||||||
export { LightPush } from "./light_push/index.js";
|
export { LightPush } from "./light_push/index.js";
|
||||||
export { Filter } from "./filter/index.js";
|
export { Filter } from "./filter/index.js";
|
||||||
export { Store } from "./store/index.js";
|
export { Store } from "./store/index.js";
|
||||||
|
export * from "./reliable_channel/index.js";
|
||||||
|
|
||||||
export * as waku from "@waku/core";
|
export * as waku from "@waku/core";
|
||||||
export * as utils from "@waku/utils";
|
export * as utils from "@waku/utils";
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { Peer, PeerId } from "@libp2p/interface";
|
import { Peer, PeerId } from "@libp2p/interface";
|
||||||
import { createEncoder, Encoder, LightPushCodec } from "@waku/core";
|
import {
|
||||||
import { Libp2p, ProtocolError } from "@waku/interfaces";
|
createEncoder,
|
||||||
|
Encoder,
|
||||||
|
LightPushCodec,
|
||||||
|
LightPushCodecV2
|
||||||
|
} from "@waku/core";
|
||||||
|
import { Libp2p, LightPushError, LightPushStatusCode } from "@waku/interfaces";
|
||||||
import { createRoutingInfo } from "@waku/utils";
|
import { createRoutingInfo } from "@waku/utils";
|
||||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
import { utf8ToBytes } from "@waku/utils/bytes";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
@ -40,8 +45,8 @@ describe("LightPush SDK", () => {
|
|||||||
const failures = result.failures ?? [];
|
const failures = result.failures ?? [];
|
||||||
|
|
||||||
expect(failures.length).to.be.eq(1);
|
expect(failures.length).to.be.eq(1);
|
||||||
expect(failures.some((v) => v.error === ProtocolError.NO_PEER_AVAILABLE)).to
|
expect(failures.some((v) => v.error === LightPushError.NO_PEER_AVAILABLE))
|
||||||
.be.true;
|
.to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send to specified number of peers of used peers", async () => {
|
it("should send to specified number of peers of used peers", async () => {
|
||||||
@ -127,6 +132,45 @@ describe("LightPush SDK", () => {
|
|||||||
expect(result.successes?.length).to.be.eq(1);
|
expect(result.successes?.length).to.be.eq(1);
|
||||||
expect(result.failures?.length).to.be.eq(1);
|
expect(result.failures?.length).to.be.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("v3 protocol support", () => {
|
||||||
|
it("should work with v3 peers", async () => {
|
||||||
|
libp2p = mockLibp2p({
|
||||||
|
peers: [mockV3Peer("1"), mockV3Peer("2")]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with mixed v2 and v3 peers", async () => {
|
||||||
|
libp2p = mockLibp2p({
|
||||||
|
peers: [mockV2AndV3Peer("1"), mockPeer("2"), mockV3Peer("3")]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock responses for different protocol versions
|
||||||
|
const v3Response = mockV3SuccessResponse(5);
|
||||||
|
const v2Response = mockV2SuccessResponse();
|
||||||
|
const v3ErrorResponse = mockV3ErrorResponse(
|
||||||
|
LightPushStatusCode.PAYLOAD_TOO_LARGE
|
||||||
|
);
|
||||||
|
const v2ErrorResponse = mockV2ErrorResponse("Message too large");
|
||||||
|
|
||||||
|
expect(v3Response.statusCode).to.eq(LightPushStatusCode.SUCCESS);
|
||||||
|
expect(v3Response.relayPeerCount).to.eq(5);
|
||||||
|
expect(v2Response.isSuccess).to.be.true;
|
||||||
|
expect(v3ErrorResponse.statusCode).to.eq(
|
||||||
|
LightPushStatusCode.PAYLOAD_TOO_LARGE
|
||||||
|
);
|
||||||
|
expect(v2ErrorResponse.isSuccess).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle v3 RLN errors", async () => {
|
||||||
|
const v3RLNError = mockV3RLNErrorResponse();
|
||||||
|
const v2RLNError = mockV2RLNErrorResponse();
|
||||||
|
|
||||||
|
expect(v3RLNError.statusCode).to.eq(LightPushStatusCode.NO_RLN_PROOF);
|
||||||
|
expect(v3RLNError.statusDesc).to.include("RLN proof generation failed");
|
||||||
|
expect(v2RLNError.info).to.include("RLN proof generation failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type MockLibp2pOptions = {
|
type MockLibp2pOptions = {
|
||||||
@ -136,7 +180,16 @@ type MockLibp2pOptions = {
|
|||||||
function mockLibp2p(options?: MockLibp2pOptions): Libp2p {
|
function mockLibp2p(options?: MockLibp2pOptions): Libp2p {
|
||||||
const peers = options?.peers || [];
|
const peers = options?.peers || [];
|
||||||
const peerStore = {
|
const peerStore = {
|
||||||
get: (id: any) => Promise.resolve(peers.find((p) => p.id === id))
|
get: (id: any) => {
|
||||||
|
const peer = peers.find((p) => p.id === id);
|
||||||
|
if (peer) {
|
||||||
|
return Promise.resolve({
|
||||||
|
...peer,
|
||||||
|
protocols: peer.protocols || [LightPushCodec]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -179,9 +232,92 @@ function mockLightPush(options: MockLightPushOptions): LightPush {
|
|||||||
return lightPush;
|
return lightPush;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockPeer(id: string): Peer {
|
function mockPeer(id: string, protocols: string[] = [LightPushCodec]): Peer {
|
||||||
return {
|
return {
|
||||||
id,
|
id: { toString: () => id } as PeerId,
|
||||||
protocols: [LightPushCodec]
|
protocols: protocols,
|
||||||
} as unknown as Peer;
|
metadata: new Map(),
|
||||||
|
addresses: [],
|
||||||
|
tags: new Map()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// V3-specific mock functions
|
||||||
|
function mockV3Peer(id: string): Peer {
|
||||||
|
return mockPeer(id, [LightPushCodec]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockV2AndV3Peer(id: string): Peer {
|
||||||
|
return mockPeer(id, [LightPushCodec, LightPushCodecV2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockV3SuccessResponse(relayPeerCount?: number): {
|
||||||
|
statusCode: LightPushStatusCode;
|
||||||
|
statusDesc: string;
|
||||||
|
relayPeerCount?: number;
|
||||||
|
isSuccess: boolean;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
statusCode: LightPushStatusCode.SUCCESS,
|
||||||
|
statusDesc: "Message sent successfully",
|
||||||
|
relayPeerCount,
|
||||||
|
isSuccess: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockV3ErrorResponse(
|
||||||
|
statusCode: LightPushStatusCode,
|
||||||
|
statusDesc?: string
|
||||||
|
): {
|
||||||
|
statusCode: LightPushStatusCode;
|
||||||
|
statusDesc: string;
|
||||||
|
isSuccess: boolean;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
statusCode,
|
||||||
|
statusDesc: statusDesc || "Error occurred",
|
||||||
|
isSuccess: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockV2SuccessResponse(): {
|
||||||
|
isSuccess: boolean;
|
||||||
|
info: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
isSuccess: true,
|
||||||
|
info: "Message sent successfully"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockV2ErrorResponse(info?: string): {
|
||||||
|
isSuccess: boolean;
|
||||||
|
info: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
isSuccess: false,
|
||||||
|
info: info || "Error occurred"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockV3RLNErrorResponse(): {
|
||||||
|
statusCode: LightPushStatusCode;
|
||||||
|
statusDesc: string;
|
||||||
|
isSuccess: boolean;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
statusCode: LightPushStatusCode.NO_RLN_PROOF,
|
||||||
|
statusDesc: "RLN proof generation failed",
|
||||||
|
isSuccess: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockV2RLNErrorResponse(): {
|
||||||
|
isSuccess: boolean;
|
||||||
|
info: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
isSuccess: false,
|
||||||
|
info: "RLN proof generation failed"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import type { PeerId } from "@libp2p/interface";
|
import type { PeerId } from "@libp2p/interface";
|
||||||
import { LightPushCore } from "@waku/core";
|
import { LightPushCore } from "@waku/core";
|
||||||
import {
|
import {
|
||||||
type CoreProtocolResult,
|
|
||||||
Failure,
|
|
||||||
type IEncoder,
|
type IEncoder,
|
||||||
ILightPush,
|
ILightPush,
|
||||||
type IMessage,
|
type IMessage,
|
||||||
type ISendOptions,
|
type ISendOptions,
|
||||||
type Libp2p,
|
type Libp2p,
|
||||||
|
LightPushCoreResult,
|
||||||
|
LightPushError,
|
||||||
|
LightPushFailure,
|
||||||
type LightPushProtocolOptions,
|
type LightPushProtocolOptions,
|
||||||
ProtocolError,
|
LightPushSDKResult,
|
||||||
Protocols,
|
Protocols
|
||||||
SDKProtocolResult
|
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export class LightPush implements ILightPush {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get multicodec(): string {
|
public get multicodec(): string[] {
|
||||||
return this.protocol.multicodec;
|
return this.protocol.multicodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,8 +71,9 @@ export class LightPush implements ILightPush {
|
|||||||
encoder: IEncoder,
|
encoder: IEncoder,
|
||||||
message: IMessage,
|
message: IMessage,
|
||||||
options: ISendOptions = {}
|
options: ISendOptions = {}
|
||||||
): Promise<SDKProtocolResult> {
|
): Promise<LightPushSDKResult> {
|
||||||
options = {
|
options = {
|
||||||
|
useLegacy: false,
|
||||||
...this.config,
|
...this.config,
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
@ -82,45 +83,48 @@ export class LightPush implements ILightPush {
|
|||||||
log.info("send: attempting to send a message to pubsubTopic:", pubsubTopic);
|
log.info("send: attempting to send a message to pubsubTopic:", pubsubTopic);
|
||||||
|
|
||||||
const peerIds = await this.peerManager.getPeers({
|
const peerIds = await this.peerManager.getPeers({
|
||||||
protocol: Protocols.LightPush,
|
protocol: options.useLegacy ? "light-push-v2" : Protocols.LightPush,
|
||||||
pubsubTopic: encoder.pubsubTopic
|
pubsubTopic: encoder.pubsubTopic
|
||||||
});
|
});
|
||||||
|
|
||||||
const coreResults: CoreProtocolResult[] =
|
const coreResults =
|
||||||
peerIds?.length > 0
|
peerIds?.length > 0
|
||||||
? await Promise.all(
|
? await Promise.all(
|
||||||
peerIds.map((peerId) =>
|
peerIds.map((peerId) =>
|
||||||
this.protocol.send(encoder, message, peerId).catch((_e) => ({
|
this.protocol
|
||||||
success: null,
|
.send(encoder, message, peerId, options.useLegacy)
|
||||||
failure: {
|
.catch((_e) => ({
|
||||||
error: ProtocolError.GENERIC_FAIL
|
success: null,
|
||||||
}
|
failure: {
|
||||||
}))
|
error: LightPushError.GENERIC_FAIL
|
||||||
|
}
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const results: SDKProtocolResult = coreResults.length
|
const results: LightPushSDKResult = coreResults.length
|
||||||
? {
|
? {
|
||||||
successes: coreResults
|
successes: coreResults
|
||||||
.filter((v) => v.success)
|
.filter((v) => v.success)
|
||||||
.map((v) => v.success) as PeerId[],
|
.map((v) => v.success) as PeerId[],
|
||||||
failures: coreResults
|
failures: coreResults
|
||||||
.filter((v) => v.failure)
|
.filter((v) => v.failure)
|
||||||
.map((v) => v.failure) as Failure[]
|
.map((v) => v.failure) as LightPushFailure[]
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
successes: [],
|
successes: [],
|
||||||
failures: [
|
failures: [
|
||||||
{
|
{
|
||||||
error: ProtocolError.NO_PEER_AVAILABLE
|
error: LightPushError.NO_PEER_AVAILABLE
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.autoRetry && results.successes.length === 0) {
|
if (options.autoRetry && results.successes.length === 0) {
|
||||||
const sendCallback = (peerId: PeerId): Promise<CoreProtocolResult> =>
|
const sendCallback = (peerId: PeerId): Promise<LightPushCoreResult> =>
|
||||||
this.protocol.send(encoder, message, peerId);
|
this.protocol.send(encoder, message, peerId, options.useLegacy);
|
||||||
|
|
||||||
this.retryManager.push(
|
this.retryManager.push(
|
||||||
sendCallback.bind(this),
|
sendCallback.bind(this),
|
||||||
options.maxAttempts || DEFAULT_MAX_ATTEMPTS,
|
options.maxAttempts || DEFAULT_MAX_ATTEMPTS,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { PeerId } from "@libp2p/interface";
|
import type { PeerId } from "@libp2p/interface";
|
||||||
import {
|
import {
|
||||||
type CoreProtocolResult,
|
type LightPushCoreResult,
|
||||||
|
LightPushError,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
Protocols
|
Protocols
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
@ -59,7 +60,7 @@ describe("RetryManager", () => {
|
|||||||
|
|
||||||
it("should process tasks in queue", async () => {
|
it("should process tasks in queue", async () => {
|
||||||
const successCallback = sinon.spy(
|
const successCallback = sinon.spy(
|
||||||
async (peerId: PeerId): Promise<CoreProtocolResult> => ({
|
async (peerId: PeerId): Promise<LightPushCoreResult> => ({
|
||||||
success: peerId,
|
success: peerId,
|
||||||
failure: null
|
failure: null
|
||||||
})
|
})
|
||||||
@ -112,9 +113,9 @@ describe("RetryManager", () => {
|
|||||||
|
|
||||||
it("should retry failed tasks", async () => {
|
it("should retry failed tasks", async () => {
|
||||||
const failingCallback = sinon.spy(
|
const failingCallback = sinon.spy(
|
||||||
async (): Promise<CoreProtocolResult> => ({
|
async (): Promise<LightPushCoreResult> => ({
|
||||||
success: null,
|
success: null,
|
||||||
failure: { error: "test error" as any }
|
failure: { error: LightPushError.GENERIC_FAIL }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -135,7 +136,7 @@ describe("RetryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should request peer renewal on specific errors", async () => {
|
it("should request peer renewal on specific errors", async () => {
|
||||||
const errorCallback = sinon.spy(async (): Promise<CoreProtocolResult> => {
|
const errorCallback = sinon.spy(async (): Promise<LightPushCoreResult> => {
|
||||||
throw new Error(ProtocolError.NO_PEER_AVAILABLE);
|
throw new Error(ProtocolError.NO_PEER_AVAILABLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,7 +156,7 @@ describe("RetryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle task timeouts", async () => {
|
it("should handle task timeouts", async () => {
|
||||||
const slowCallback = sinon.spy(async (): Promise<CoreProtocolResult> => {
|
const slowCallback = sinon.spy(async (): Promise<LightPushCoreResult> => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||||
return { success: mockPeerId, failure: null };
|
return { success: mockPeerId, failure: null };
|
||||||
});
|
});
|
||||||
@ -174,9 +175,11 @@ describe("RetryManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not execute task if max attempts is 0", async () => {
|
it("should not execute task if max attempts is 0", async () => {
|
||||||
const failingCallback = sinon.spy(async (): Promise<CoreProtocolResult> => {
|
const failingCallback = sinon.spy(
|
||||||
throw new Error("test error" as any);
|
async (): Promise<LightPushCoreResult> => {
|
||||||
});
|
throw new Error("test error" as any);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const task = {
|
const task = {
|
||||||
callback: failingCallback,
|
callback: failingCallback,
|
||||||
@ -209,7 +212,7 @@ describe("RetryManager", () => {
|
|||||||
called++;
|
called++;
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
success: null,
|
success: null,
|
||||||
failure: { error: ProtocolError.GENERIC_FAIL }
|
failure: { error: LightPushError.GENERIC_FAIL }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
retryManager.push(failCallback, 2, TestRoutingInfo);
|
retryManager.push(failCallback, 2, TestRoutingInfo);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { PeerId } from "@libp2p/interface";
|
import type { PeerId } from "@libp2p/interface";
|
||||||
import {
|
import {
|
||||||
type CoreProtocolResult,
|
|
||||||
type IRoutingInfo,
|
type IRoutingInfo,
|
||||||
|
type LightPushCoreResult,
|
||||||
Protocols
|
Protocols
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
@ -15,7 +15,7 @@ type RetryManagerConfig = {
|
|||||||
peerManager: PeerManager;
|
peerManager: PeerManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AttemptCallback = (peerId: PeerId) => Promise<CoreProtocolResult>;
|
type AttemptCallback = (peerId: PeerId) => Promise<LightPushCoreResult>;
|
||||||
|
|
||||||
export type ScheduledTask = {
|
export type ScheduledTask = {
|
||||||
maxAttempts: number;
|
maxAttempts: number;
|
||||||
@ -123,7 +123,13 @@ export class RetryManager {
|
|||||||
task.callback(peerId)
|
task.callback(peerId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (response?.failure) {
|
// If timeout resolves first, response will be void (undefined)
|
||||||
|
// In this case, we should treat it as a timeout error
|
||||||
|
if (response === undefined) {
|
||||||
|
throw new Error("Task timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.failure) {
|
||||||
throw Error(response.failure.error);
|
throw Error(response.failure.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { ProtocolError } from "@waku/interfaces";
|
import { LightPushError } from "@waku/interfaces";
|
||||||
|
|
||||||
export const shouldPeerBeChanged = (
|
export const shouldPeerBeChanged = (
|
||||||
failure: string | ProtocolError
|
failure: string | LightPushError
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const toBeChanged =
|
const toBeChanged =
|
||||||
failure === ProtocolError.REMOTE_PEER_REJECTED ||
|
failure === LightPushError.REMOTE_PEER_REJECTED ||
|
||||||
failure === ProtocolError.NO_RESPONSE ||
|
failure === LightPushError.NO_RESPONSE ||
|
||||||
failure === ProtocolError.RLN_PROOF_GENERATION ||
|
failure === LightPushError.RLN_PROOF_GENERATION ||
|
||||||
failure === ProtocolError.NO_PEER_AVAILABLE;
|
failure === LightPushError.NO_PEER_AVAILABLE;
|
||||||
|
|
||||||
if (toBeChanged) {
|
if (toBeChanged) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -85,7 +85,8 @@ describe("PeerManager", () => {
|
|||||||
_clusterId: ClusterId,
|
_clusterId: ClusterId,
|
||||||
_shardId: ShardId
|
_shardId: ShardId
|
||||||
) => true,
|
) => true,
|
||||||
isPeerOnTopic: async (_id: PeerId, _topic: string) => true
|
isPeerOnTopic: async (_id: PeerId, _topic: string) => true,
|
||||||
|
hasShardInfo: async (_id: PeerId) => true
|
||||||
} as unknown as IConnectionManager;
|
} as unknown as IConnectionManager;
|
||||||
peerManager = new PeerManager({
|
peerManager = new PeerManager({
|
||||||
libp2p,
|
libp2p,
|
||||||
|
|||||||
@ -4,7 +4,12 @@ import {
|
|||||||
PeerId,
|
PeerId,
|
||||||
TypedEventEmitter
|
TypedEventEmitter
|
||||||
} from "@libp2p/interface";
|
} from "@libp2p/interface";
|
||||||
import { FilterCodecs, LightPushCodec, StoreCodec } from "@waku/core";
|
import {
|
||||||
|
FilterCodecs,
|
||||||
|
LightPushCodec,
|
||||||
|
LightPushCodecV2,
|
||||||
|
StoreCodec
|
||||||
|
} from "@waku/core";
|
||||||
import {
|
import {
|
||||||
CONNECTION_LOCKED_TAG,
|
CONNECTION_LOCKED_TAG,
|
||||||
type IConnectionManager,
|
type IConnectionManager,
|
||||||
@ -28,8 +33,10 @@ type PeerManagerParams = {
|
|||||||
connectionManager: IConnectionManager;
|
connectionManager: IConnectionManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SupportedProtocols = Protocols | "light-push-v2";
|
||||||
|
|
||||||
type GetPeersParams = {
|
type GetPeersParams = {
|
||||||
protocol: Protocols;
|
protocol: SupportedProtocols;
|
||||||
pubsubTopic: string;
|
pubsubTopic: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +126,7 @@ export class PeerManager {
|
|||||||
|
|
||||||
for (const peer of connectedPeers) {
|
for (const peer of connectedPeers) {
|
||||||
const hasProtocol = this.hasPeerProtocol(peer, params.protocol);
|
const hasProtocol = this.hasPeerProtocol(peer, params.protocol);
|
||||||
const hasSamePubsub = await this.connectionManager.isPeerOnTopic(
|
const hasSamePubsub = await this.isPeerOnPubsub(
|
||||||
peer.id,
|
peer.id,
|
||||||
params.pubsubTopic
|
params.pubsubTopic
|
||||||
);
|
);
|
||||||
@ -204,12 +211,19 @@ export class PeerManager {
|
|||||||
|
|
||||||
private async onConnected(event: CustomEvent<IdentifyResult>): Promise<void> {
|
private async onConnected(event: CustomEvent<IdentifyResult>): Promise<void> {
|
||||||
const result = event.detail;
|
const result = event.detail;
|
||||||
if (
|
|
||||||
result.protocols.includes(this.matchProtocolToCodec(Protocols.Filter))
|
const isFilterPeer = result.protocols.includes(
|
||||||
) {
|
this.getProtocolCodecs(Protocols.Filter)
|
||||||
|
);
|
||||||
|
const isStorePeer = result.protocols.includes(
|
||||||
|
this.getProtocolCodecs(Protocols.Store)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFilterPeer) {
|
||||||
this.dispatchFilterPeerConnect(result.peerId);
|
this.dispatchFilterPeerConnect(result.peerId);
|
||||||
}
|
}
|
||||||
if (result.protocols.includes(this.matchProtocolToCodec(Protocols.Store))) {
|
|
||||||
|
if (isStorePeer) {
|
||||||
this.dispatchStorePeerConnect(result.peerId);
|
this.dispatchStorePeerConnect(result.peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,8 +244,8 @@ export class PeerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasPeerProtocol(peer: Peer, protocol: Protocols): boolean {
|
private hasPeerProtocol(peer: Peer, protocol: SupportedProtocols): boolean {
|
||||||
return peer.protocols.includes(this.matchProtocolToCodec(protocol));
|
return peer.protocols.includes(this.getProtocolCodecs(protocol));
|
||||||
}
|
}
|
||||||
|
|
||||||
private lockPeer(id: PeerId): void {
|
private lockPeer(id: PeerId): void {
|
||||||
@ -289,14 +303,18 @@ export class PeerManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private matchProtocolToCodec(protocol: Protocols): string {
|
private getProtocolCodecs(protocol: SupportedProtocols): string {
|
||||||
const protocolToCodec = {
|
if (protocol === Protocols.Relay) {
|
||||||
|
throw new Error("Relay protocol is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocolToCodecs = {
|
||||||
[Protocols.Filter]: FilterCodecs.SUBSCRIBE,
|
[Protocols.Filter]: FilterCodecs.SUBSCRIBE,
|
||||||
[Protocols.LightPush]: LightPushCodec,
|
[Protocols.LightPush]: LightPushCodec,
|
||||||
[Protocols.Store]: StoreCodec,
|
[Protocols.Store]: StoreCodec,
|
||||||
[Protocols.Relay]: ""
|
"light-push-v2": LightPushCodecV2
|
||||||
};
|
};
|
||||||
|
|
||||||
return protocolToCodec[protocol];
|
return protocolToCodecs[protocol];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,7 @@ describe("QueryOnConnect", () => {
|
|||||||
it("should create QueryOnConnect instance with all required parameters", () => {
|
it("should create QueryOnConnect instance with all required parameters", () => {
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -108,6 +109,7 @@ describe("QueryOnConnect", () => {
|
|||||||
it("should create QueryOnConnect instance without options", () => {
|
it("should create QueryOnConnect instance without options", () => {
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator
|
mockQueryGenerator
|
||||||
@ -120,6 +122,7 @@ describe("QueryOnConnect", () => {
|
|||||||
it("should accept empty decoders array", () => {
|
it("should accept empty decoders array", () => {
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
[],
|
[],
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -134,6 +137,7 @@ describe("QueryOnConnect", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -173,6 +177,7 @@ describe("QueryOnConnect", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -224,6 +229,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -276,6 +282,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -298,6 +305,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -320,6 +328,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -391,6 +400,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
const queryOnConnect = new QueryOnConnect(
|
const queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -418,6 +428,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -443,6 +454,7 @@ describe("QueryOnConnect", () => {
|
|||||||
let resolveMessageEvent: (messages: IDecodedMessage[]) => void;
|
let resolveMessageEvent: (messages: IDecodedMessage[]) => void;
|
||||||
let rejectMessageEvent: (reason: string) => void;
|
let rejectMessageEvent: (reason: string) => void;
|
||||||
let connectStoreEvent: CustomEvent<PeerId>;
|
let connectStoreEvent: CustomEvent<PeerId>;
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Create a promise that resolves when a message event is emitted
|
// Create a promise that resolves when a message event is emitted
|
||||||
@ -472,6 +484,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -482,6 +495,7 @@ describe("QueryOnConnect", () => {
|
|||||||
queryOnConnect.addEventListener(
|
queryOnConnect.addEventListener(
|
||||||
QueryOnConnectEvent.MessagesRetrieved,
|
QueryOnConnectEvent.MessagesRetrieved,
|
||||||
(event: CustomEvent<IDecodedMessage[]>) => {
|
(event: CustomEvent<IDecodedMessage[]>) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
resolveMessageEvent(event.detail);
|
resolveMessageEvent(event.detail);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -491,12 +505,16 @@ describe("QueryOnConnect", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set a timeout to reject if no message is received
|
// Set a timeout to reject if no message is received
|
||||||
setTimeout(
|
timeoutId = setTimeout(
|
||||||
() => rejectMessageEvent("No message received within timeout"),
|
() => rejectMessageEvent("No message received within timeout"),
|
||||||
500
|
500
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
});
|
||||||
|
|
||||||
it("should emit message when we just started and store connect event occurs", async () => {
|
it("should emit message when we just started and store connect event occurs", async () => {
|
||||||
const mockMessage: IDecodedMessage = {
|
const mockMessage: IDecodedMessage = {
|
||||||
hash: utf8ToBytes("1234"),
|
hash: utf8ToBytes("1234"),
|
||||||
@ -599,6 +617,7 @@ describe("QueryOnConnect", () => {
|
|||||||
|
|
||||||
queryOnConnect = new QueryOnConnect(
|
queryOnConnect = new QueryOnConnect(
|
||||||
mockDecoders,
|
mockDecoders,
|
||||||
|
() => false,
|
||||||
mockPeerManagerEventEmitter,
|
mockPeerManagerEventEmitter,
|
||||||
mockWakuEventEmitter,
|
mockWakuEventEmitter,
|
||||||
mockQueryGenerator,
|
mockQueryGenerator,
|
||||||
@ -744,6 +763,248 @@ describe("QueryOnConnect", () => {
|
|||||||
expect(mockQueryGenerator.calledTwice).to.be.true;
|
expect(mockQueryGenerator.calledTwice).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("stopIfTrue predicate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPeerManagerEventEmitter.addEventListener = sinon.stub();
|
||||||
|
mockWakuEventEmitter.addEventListener = sinon.stub();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stop query iteration when stopIfTrue returns true", async () => {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "msg1",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([1]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "stop-hash",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([2]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "msg3",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([3]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup generator to yield 3 pages, stop should occur on page 2
|
||||||
|
const mockAsyncGenerator = async function* (): AsyncGenerator<
|
||||||
|
Promise<IDecodedMessage | undefined>[]
|
||||||
|
> {
|
||||||
|
yield [Promise.resolve(messages[0])];
|
||||||
|
yield [Promise.resolve(messages[1])];
|
||||||
|
yield [Promise.resolve(messages[2])];
|
||||||
|
};
|
||||||
|
mockQueryGenerator.returns(mockAsyncGenerator());
|
||||||
|
|
||||||
|
const stopPredicate = (msg: IDecodedMessage): boolean =>
|
||||||
|
msg.hashStr === "stop-hash";
|
||||||
|
|
||||||
|
queryOnConnect = new QueryOnConnect(
|
||||||
|
mockDecoders,
|
||||||
|
stopPredicate,
|
||||||
|
mockPeerManagerEventEmitter,
|
||||||
|
mockWakuEventEmitter,
|
||||||
|
mockQueryGenerator,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const receivedMessages: IDecodedMessage[] = [];
|
||||||
|
queryOnConnect.addEventListener(
|
||||||
|
QueryOnConnectEvent.MessagesRetrieved,
|
||||||
|
(event: CustomEvent<IDecodedMessage[]>) => {
|
||||||
|
receivedMessages.push(...event.detail);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
queryOnConnect.start();
|
||||||
|
await queryOnConnect["maybeQuery"](mockPeerId);
|
||||||
|
|
||||||
|
// Should have received messages from first 2 pages only
|
||||||
|
expect(receivedMessages).to.have.length(2);
|
||||||
|
expect(receivedMessages[0].hashStr).to.equal("msg1");
|
||||||
|
expect(receivedMessages[1].hashStr).to.equal("stop-hash");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should process all pages when stopIfTrue never returns true", async () => {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "msg1",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([1]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "msg2",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([2]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "msg3",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([3]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockAsyncGenerator = async function* (): AsyncGenerator<
|
||||||
|
Promise<IDecodedMessage | undefined>[]
|
||||||
|
> {
|
||||||
|
yield [Promise.resolve(messages[0])];
|
||||||
|
yield [Promise.resolve(messages[1])];
|
||||||
|
yield [Promise.resolve(messages[2])];
|
||||||
|
};
|
||||||
|
mockQueryGenerator.returns(mockAsyncGenerator());
|
||||||
|
|
||||||
|
const stopPredicate = (): boolean => false;
|
||||||
|
|
||||||
|
queryOnConnect = new QueryOnConnect(
|
||||||
|
mockDecoders,
|
||||||
|
stopPredicate,
|
||||||
|
mockPeerManagerEventEmitter,
|
||||||
|
mockWakuEventEmitter,
|
||||||
|
mockQueryGenerator,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const receivedMessages: IDecodedMessage[] = [];
|
||||||
|
queryOnConnect.addEventListener(
|
||||||
|
QueryOnConnectEvent.MessagesRetrieved,
|
||||||
|
(event: CustomEvent<IDecodedMessage[]>) => {
|
||||||
|
receivedMessages.push(...event.detail);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
queryOnConnect.start();
|
||||||
|
await queryOnConnect["maybeQuery"](mockPeerId);
|
||||||
|
|
||||||
|
// Should have received all 3 messages
|
||||||
|
expect(receivedMessages).to.have.length(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stop on first message of a page if stopIfTrue matches", async () => {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "stop-hash",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([1]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "msg2",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([2]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: new Uint8Array(),
|
||||||
|
hashStr: "msg3",
|
||||||
|
version: 1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
contentTopic: "/test/1/content",
|
||||||
|
pubsubTopic: "/waku/2/default-waku/proto",
|
||||||
|
payload: new Uint8Array([3]),
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
ephemeral: false,
|
||||||
|
meta: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockAsyncGenerator = async function* (): AsyncGenerator<
|
||||||
|
Promise<IDecodedMessage | undefined>[]
|
||||||
|
> {
|
||||||
|
yield [
|
||||||
|
Promise.resolve(messages[0]),
|
||||||
|
Promise.resolve(messages[1]),
|
||||||
|
Promise.resolve(messages[2])
|
||||||
|
];
|
||||||
|
};
|
||||||
|
mockQueryGenerator.returns(mockAsyncGenerator());
|
||||||
|
|
||||||
|
const stopPredicate = (msg: IDecodedMessage): boolean =>
|
||||||
|
msg.hashStr === "stop-hash";
|
||||||
|
|
||||||
|
queryOnConnect = new QueryOnConnect(
|
||||||
|
mockDecoders,
|
||||||
|
stopPredicate,
|
||||||
|
mockPeerManagerEventEmitter,
|
||||||
|
mockWakuEventEmitter,
|
||||||
|
mockQueryGenerator,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const receivedMessages: IDecodedMessage[] = [];
|
||||||
|
queryOnConnect.addEventListener(
|
||||||
|
QueryOnConnectEvent.MessagesRetrieved,
|
||||||
|
(event: CustomEvent<IDecodedMessage[]>) => {
|
||||||
|
receivedMessages.push(...event.detail);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
queryOnConnect.start();
|
||||||
|
await queryOnConnect["maybeQuery"](mockPeerId);
|
||||||
|
|
||||||
|
// Should have received all 3 messages from the page, even though first matched
|
||||||
|
expect(receivedMessages).to.have.length(3);
|
||||||
|
expect(receivedMessages[0].hashStr).to.equal("stop-hash");
|
||||||
|
expect(receivedMessages[1].hashStr).to.equal("msg2");
|
||||||
|
expect(receivedMessages[2].hashStr).to.equal("msg3");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("calculateTimeRange", () => {
|
describe("calculateTimeRange", () => {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
const log = new Logger("sdk:query-on-connect");
|
const log = new Logger("sdk:query-on-connect");
|
||||||
|
|
||||||
export const DEFAULT_FORCE_QUERY_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
export const DEFAULT_FORCE_QUERY_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
export const MAX_TIME_RANGE_QUERY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
export const MAX_TIME_RANGE_QUERY_MS = 30 * 24 * 60 * 60 * 1000; // 30 days (queries are split)
|
||||||
|
|
||||||
export interface QueryOnConnectOptions {
|
export interface QueryOnConnectOptions {
|
||||||
/**
|
/**
|
||||||
@ -54,6 +54,7 @@ export class QueryOnConnect<
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public decoders: IDecoder<T>[],
|
public decoders: IDecoder<T>[],
|
||||||
|
public stopIfTrue: (msg: T) => boolean,
|
||||||
private readonly peerManagerEventEmitter: TypedEventEmitter<IPeerManagerEvents>,
|
private readonly peerManagerEventEmitter: TypedEventEmitter<IPeerManagerEvents>,
|
||||||
private readonly wakuEventEmitter: IWakuEventEmitter,
|
private readonly wakuEventEmitter: IWakuEventEmitter,
|
||||||
private readonly _queryGenerator: <T extends IDecodedMessage>(
|
private readonly _queryGenerator: <T extends IDecodedMessage>(
|
||||||
@ -125,8 +126,13 @@ export class QueryOnConnect<
|
|||||||
const messages = (await Promise.all(page)).filter(
|
const messages = (await Promise.all(page)).filter(
|
||||||
(m) => m !== undefined
|
(m) => m !== undefined
|
||||||
);
|
);
|
||||||
|
const stop = messages.some((msg: T) => this.stopIfTrue(msg));
|
||||||
// Bundle the messages to help batch process by sds
|
// Bundle the messages to help batch process by sds
|
||||||
this.dispatchMessages(messages);
|
this.dispatchMessages(messages);
|
||||||
|
|
||||||
|
if (stop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Didn't throw, so it didn't fail
|
// Didn't throw, so it didn't fail
|
||||||
|
|||||||
66
packages/sdk/src/reliable_channel/events.ts
Normal file
66
packages/sdk/src/reliable_channel/events.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { IDecodedMessage, ProtocolError } from "@waku/interfaces";
|
||||||
|
import type { HistoryEntry, MessageId } from "@waku/sds";
|
||||||
|
|
||||||
|
export const ReliableChannelEvent = {
|
||||||
|
/**
|
||||||
|
* The message is being sent over the wire.
|
||||||
|
*
|
||||||
|
* This event may be emitted several times if the retry mechanism kicks in.
|
||||||
|
*/
|
||||||
|
SendingMessage: "sending-message",
|
||||||
|
/**
|
||||||
|
* The message has been sent over the wire but has not been acknowledged by
|
||||||
|
* any other party yet.
|
||||||
|
*
|
||||||
|
* We are now waiting for acknowledgements.
|
||||||
|
*
|
||||||
|
* This event may be emitted several times if the
|
||||||
|
* several times if the retry mechanisms kicks in.
|
||||||
|
*/
|
||||||
|
MessageSent: "message-sent",
|
||||||
|
/**
|
||||||
|
* A received bloom filter seems to indicate that the messages was received
|
||||||
|
* by another party.
|
||||||
|
*
|
||||||
|
* However, this is probabilistic. The retry mechanism will wait a bit longer
|
||||||
|
* before trying to send the message again.
|
||||||
|
*/
|
||||||
|
MessagePossiblyAcknowledged: "message-possibly-acknowledged",
|
||||||
|
/**
|
||||||
|
* The message was fully acknowledged by other members of the channel
|
||||||
|
*/
|
||||||
|
MessageAcknowledged: "message-acknowledged",
|
||||||
|
/**
|
||||||
|
* It was not possible to send the messages due to a non-recoverable error,
|
||||||
|
* most likely an internal error for a developer to resolve.
|
||||||
|
*/
|
||||||
|
SendingMessageIrrecoverableError: "sending-message-irrecoverable-error",
|
||||||
|
/**
|
||||||
|
* A new message has been received.
|
||||||
|
*/
|
||||||
|
MessageReceived: "message-received",
|
||||||
|
/**
|
||||||
|
* We are aware of a missing message but failed to retrieve it successfully.
|
||||||
|
*/
|
||||||
|
IrretrievableMessage: "irretrievable-message"
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReliableChannelEvent =
|
||||||
|
(typeof ReliableChannelEvent)[keyof typeof ReliableChannelEvent];
|
||||||
|
|
||||||
|
export interface ReliableChannelEvents {
|
||||||
|
"sending-message": CustomEvent<MessageId>;
|
||||||
|
"message-sent": CustomEvent<MessageId>;
|
||||||
|
"message-possibly-acknowledged": CustomEvent<{
|
||||||
|
messageId: MessageId;
|
||||||
|
possibleAckCount: number;
|
||||||
|
}>;
|
||||||
|
"message-acknowledged": CustomEvent<MessageId>;
|
||||||
|
// TODO probably T extends IDecodedMessage?
|
||||||
|
"message-received": CustomEvent<IDecodedMessage>;
|
||||||
|
"irretrievable-message": CustomEvent<HistoryEntry>;
|
||||||
|
"sending-message-irrecoverable-error": CustomEvent<{
|
||||||
|
messageId: MessageId;
|
||||||
|
error: ProtocolError;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
2
packages/sdk/src/reliable_channel/index.ts
Normal file
2
packages/sdk/src/reliable_channel/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { ReliableChannel, ReliableChannelOptions } from "./reliable_channel.js";
|
||||||
|
export { ReliableChannelEvents, ReliableChannelEvent } from "./events.js";
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import type {
|
||||||
|
IDecodedMessage,
|
||||||
|
IDecoder,
|
||||||
|
QueryRequestParams
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import type { MessageId } from "@waku/sds";
|
||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
|
||||||
|
const log = new Logger("sdk:missing-message-retriever");
|
||||||
|
|
||||||
|
const DEFAULT_RETRIEVE_FREQUENCY_MS = 10 * 1000; // 10 seconds
|
||||||
|
|
||||||
|
export class MissingMessageRetriever<T extends IDecodedMessage> {
|
||||||
|
private retrieveInterval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
private missingMessages: Map<MessageId, Uint8Array<ArrayBufferLike>>; // Waku Message Ids
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly decoder: IDecoder<T>,
|
||||||
|
private readonly retrieveFrequencyMs: number = DEFAULT_RETRIEVE_FREQUENCY_MS,
|
||||||
|
private readonly _retrieve: <T extends IDecodedMessage>(
|
||||||
|
decoders: IDecoder<T>[],
|
||||||
|
options?: Partial<QueryRequestParams>
|
||||||
|
) => AsyncGenerator<Promise<T | undefined>[]>,
|
||||||
|
private readonly onMessageRetrieved?: (message: T) => Promise<void>
|
||||||
|
) {
|
||||||
|
this.missingMessages = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(): void {
|
||||||
|
if (this.retrieveInterval) {
|
||||||
|
clearInterval(this.retrieveInterval);
|
||||||
|
}
|
||||||
|
if (this.retrieveFrequencyMs !== 0) {
|
||||||
|
log.info(`start retrieve loop every ${this.retrieveFrequencyMs}ms`);
|
||||||
|
this.retrieveInterval = setInterval(() => {
|
||||||
|
void this.retrieveMissingMessage();
|
||||||
|
}, this.retrieveFrequencyMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
if (this.retrieveInterval) {
|
||||||
|
clearInterval(this.retrieveInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addMissingMessage(
|
||||||
|
messageId: MessageId,
|
||||||
|
retrievalHint: Uint8Array
|
||||||
|
): void {
|
||||||
|
if (!this.missingMessages.has(messageId)) {
|
||||||
|
log.info("missing message notice", messageId, retrievalHint);
|
||||||
|
this.missingMessages.set(messageId, retrievalHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeMissingMessage(messageId: MessageId): void {
|
||||||
|
if (this.missingMessages.has(messageId)) {
|
||||||
|
this.missingMessages.delete(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async retrieveMissingMessage(): Promise<void> {
|
||||||
|
if (this.missingMessages.size) {
|
||||||
|
const messageHashes = Array.from(this.missingMessages.values());
|
||||||
|
log.info("attempting to retrieve missing message", messageHashes.length);
|
||||||
|
for await (const page of this._retrieve([this.decoder], {
|
||||||
|
messageHashes
|
||||||
|
})) {
|
||||||
|
for await (const msg of page) {
|
||||||
|
if (msg && this.onMessageRetrieved) {
|
||||||
|
await this.onMessageRetrieved(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1133
packages/sdk/src/reliable_channel/reliable_channel.spec.ts
Normal file
1133
packages/sdk/src/reliable_channel/reliable_channel.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
691
packages/sdk/src/reliable_channel/reliable_channel.ts
Normal file
691
packages/sdk/src/reliable_channel/reliable_channel.ts
Normal file
@ -0,0 +1,691 @@
|
|||||||
|
import { TypedEventEmitter } from "@libp2p/interface";
|
||||||
|
import { messageHash } from "@waku/core";
|
||||||
|
import {
|
||||||
|
type Callback,
|
||||||
|
type IDecodedMessage,
|
||||||
|
type IDecoder,
|
||||||
|
type IEncoder,
|
||||||
|
type IMessage,
|
||||||
|
ISendOptions,
|
||||||
|
type IWaku,
|
||||||
|
LightPushError,
|
||||||
|
LightPushSDKResult,
|
||||||
|
QueryRequestParams
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import {
|
||||||
|
type ChannelId,
|
||||||
|
isContentMessage,
|
||||||
|
MessageChannel,
|
||||||
|
MessageChannelEvent,
|
||||||
|
type MessageChannelOptions,
|
||||||
|
Message as SdsMessage,
|
||||||
|
type SenderId,
|
||||||
|
SyncMessage
|
||||||
|
} from "@waku/sds";
|
||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
QueryOnConnect,
|
||||||
|
QueryOnConnectEvent
|
||||||
|
} from "../query_on_connect/index.js";
|
||||||
|
|
||||||
|
import { ReliableChannelEvent, ReliableChannelEvents } from "./events.js";
|
||||||
|
import { MissingMessageRetriever } from "./missing_message_retriever.js";
|
||||||
|
import { RetryManager } from "./retry_manager.js";
|
||||||
|
|
||||||
|
const log = new Logger("sdk:reliable-channel");
|
||||||
|
|
||||||
|
const DEFAULT_SYNC_MIN_INTERVAL_MS = 30 * 1000; // 30 seconds
|
||||||
|
const DEFAULT_RETRY_INTERVAL_MS = 30 * 1000; // 30 seconds
|
||||||
|
const DEFAULT_MAX_RETRY_ATTEMPTS = 10;
|
||||||
|
const DEFAULT_SWEEP_IN_BUF_INTERVAL_MS = 5 * 1000;
|
||||||
|
const DEFAULT_PROCESS_TASK_MIN_ELAPSE_MS = 1000;
|
||||||
|
|
||||||
|
const IRRECOVERABLE_SENDING_ERRORS: LightPushError[] = [
|
||||||
|
LightPushError.ENCODE_FAILED,
|
||||||
|
LightPushError.EMPTY_PAYLOAD,
|
||||||
|
LightPushError.SIZE_TOO_BIG,
|
||||||
|
LightPushError.RLN_PROOF_GENERATION
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ReliableChannelOptions = MessageChannelOptions & {
|
||||||
|
/**
|
||||||
|
* The minimum interval between 2 sync messages in the channel.
|
||||||
|
*
|
||||||
|
* Meaning, how frequently we want messages in the channel, noting that the
|
||||||
|
* responsibility of sending a sync messages is shared between participants
|
||||||
|
* of the channel.
|
||||||
|
*
|
||||||
|
* `0` means no sync messages will be sent.
|
||||||
|
*
|
||||||
|
* @default 30,000 (30 seconds) [[DEFAULT_SYNC_MIN_INTERVAL_MS]]
|
||||||
|
*/
|
||||||
|
syncMinIntervalMs?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long to wait before re-sending a message that as not acknowledged.
|
||||||
|
*
|
||||||
|
* @default 60,000 (60 seconds) [[DEFAULT_RETRY_INTERVAL_MS]]
|
||||||
|
*/
|
||||||
|
retryIntervalMs?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many times do we attempt resending messages that were not acknowledged.
|
||||||
|
*
|
||||||
|
* @default 10 [[DEFAULT_MAX_RETRY_ATTEMPTS]]
|
||||||
|
*/
|
||||||
|
maxRetryAttempts?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How often store queries are done to retrieve missing messages.
|
||||||
|
*
|
||||||
|
* @default 10,000 (10 seconds)
|
||||||
|
*/
|
||||||
|
retrieveFrequencyMs?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How often SDS message channel incoming buffer is swept.
|
||||||
|
*
|
||||||
|
* @default 5000 (every 5 seconds)
|
||||||
|
*/
|
||||||
|
sweepInBufIntervalMs?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to automatically do a store query after connection to store nodes.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
queryOnConnect?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to auto start the message channel
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoStart?: boolean;
|
||||||
|
|
||||||
|
/** The minimum elapse time between calling the underlying channel process
|
||||||
|
* task for incoming messages. This is to avoid overload when processing
|
||||||
|
* a lot of messages.
|
||||||
|
*
|
||||||
|
* @default 1000 (1 second)
|
||||||
|
*/
|
||||||
|
processTaskMinElapseMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An easy-to-use reliable channel that ensures all participants to the channel have eventual message consistency.
|
||||||
|
*
|
||||||
|
* Use events to track:
|
||||||
|
* - if your outgoing messages are sent, acknowledged or error out
|
||||||
|
* - for new incoming messages
|
||||||
|
* @emits [[ReliableChannelEvents]]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class ReliableChannel<
|
||||||
|
T extends IDecodedMessage
|
||||||
|
> extends TypedEventEmitter<ReliableChannelEvents> {
|
||||||
|
private readonly _send: (
|
||||||
|
encoder: IEncoder,
|
||||||
|
message: IMessage,
|
||||||
|
sendOptions?: ISendOptions
|
||||||
|
) => Promise<LightPushSDKResult>;
|
||||||
|
|
||||||
|
private readonly _subscribe: (
|
||||||
|
decoders: IDecoder<T> | IDecoder<T>[],
|
||||||
|
callback: Callback<T>
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
|
private readonly _retrieve?: <T extends IDecodedMessage>(
|
||||||
|
decoders: IDecoder<T>[],
|
||||||
|
options?: Partial<QueryRequestParams>
|
||||||
|
) => AsyncGenerator<Promise<T | undefined>[]>;
|
||||||
|
|
||||||
|
private readonly syncMinIntervalMs: number;
|
||||||
|
private syncTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
private sweepInBufInterval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
private readonly sweepInBufIntervalMs: number;
|
||||||
|
private processTaskTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
private readonly retryManager: RetryManager | undefined;
|
||||||
|
private readonly missingMessageRetriever?: MissingMessageRetriever<T>;
|
||||||
|
private readonly queryOnConnect?: QueryOnConnect<T>;
|
||||||
|
private readonly processTaskMinElapseMs: number;
|
||||||
|
private _started: boolean;
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
public node: IWaku,
|
||||||
|
public messageChannel: MessageChannel,
|
||||||
|
private encoder: IEncoder,
|
||||||
|
private decoder: IDecoder<T>,
|
||||||
|
options?: ReliableChannelOptions
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
if (node.lightPush) {
|
||||||
|
this._send = node.lightPush.send.bind(node.lightPush);
|
||||||
|
} else if (node.relay) {
|
||||||
|
this._send = node.relay.send.bind(node.relay);
|
||||||
|
} else {
|
||||||
|
throw "No protocol available to send messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.filter) {
|
||||||
|
this._subscribe = node.filter.subscribe.bind(node.filter);
|
||||||
|
} else if (node.relay) {
|
||||||
|
// TODO: Why do relay and filter have different interfaces?
|
||||||
|
// this._subscribe = node.relay.subscribeWithUnsubscribe;
|
||||||
|
throw "Not implemented";
|
||||||
|
} else {
|
||||||
|
throw "No protocol available to receive messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.store) {
|
||||||
|
this._retrieve = node.store.queryGenerator.bind(node.store);
|
||||||
|
const peerManagerEvents = (node as any)?.peerManager?.events;
|
||||||
|
if (
|
||||||
|
peerManagerEvents !== undefined &&
|
||||||
|
(options?.queryOnConnect ?? true)
|
||||||
|
) {
|
||||||
|
this.queryOnConnect = new QueryOnConnect(
|
||||||
|
[this.decoder],
|
||||||
|
this.isChannelMessageWithCausalHistory.bind(this),
|
||||||
|
peerManagerEvents,
|
||||||
|
node.events,
|
||||||
|
this._retrieve.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncMinIntervalMs =
|
||||||
|
options?.syncMinIntervalMs ?? DEFAULT_SYNC_MIN_INTERVAL_MS;
|
||||||
|
|
||||||
|
this.sweepInBufIntervalMs =
|
||||||
|
options?.sweepInBufIntervalMs ?? DEFAULT_SWEEP_IN_BUF_INTERVAL_MS;
|
||||||
|
|
||||||
|
const retryIntervalMs =
|
||||||
|
options?.retryIntervalMs ?? DEFAULT_RETRY_INTERVAL_MS;
|
||||||
|
const maxRetryAttempts =
|
||||||
|
options?.maxRetryAttempts ?? DEFAULT_MAX_RETRY_ATTEMPTS;
|
||||||
|
|
||||||
|
if (retryIntervalMs && maxRetryAttempts) {
|
||||||
|
// TODO: there is a lot to improve. e.g. not point retry to send if node is offline.
|
||||||
|
this.retryManager = new RetryManager(retryIntervalMs, maxRetryAttempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processTaskMinElapseMs =
|
||||||
|
options?.processTaskMinElapseMs ?? DEFAULT_PROCESS_TASK_MIN_ELAPSE_MS;
|
||||||
|
|
||||||
|
if (this._retrieve) {
|
||||||
|
this.missingMessageRetriever = new MissingMessageRetriever(
|
||||||
|
this.decoder,
|
||||||
|
options?.retrieveFrequencyMs,
|
||||||
|
this._retrieve,
|
||||||
|
async (msg: T) => {
|
||||||
|
await this.processIncomingMessage(msg);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isStarted(): boolean {
|
||||||
|
return this._started;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to identify messages, pass the payload of a message you are
|
||||||
|
* about to send to track the events for this message.
|
||||||
|
* This is pre-sds wrapping
|
||||||
|
* @param messagePayload
|
||||||
|
*/
|
||||||
|
public static getMessageId(messagePayload: Uint8Array): string {
|
||||||
|
return MessageChannel.getMessageId(messagePayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message channels. Message channels enables end-to-end
|
||||||
|
* reliability by ensuring that all messages in the channel are received
|
||||||
|
* by other users, and retrieved by this local node.
|
||||||
|
*
|
||||||
|
* emits events about outgoing messages, see [[`ReliableChannel`]] docs.
|
||||||
|
*
|
||||||
|
* Note that all participants in a message channels need to get the messages
|
||||||
|
* from the channel. Meaning:
|
||||||
|
* - all participants must be able to decrypt the messages
|
||||||
|
* - all participants must be subscribing to content topic(s) where the messages are sent
|
||||||
|
*
|
||||||
|
* @param node The waku node to use to send and receive messages
|
||||||
|
* @param channelId An id for the channel, all participants of the channel should use the same id
|
||||||
|
* @param senderId An id for the sender, to ensure acknowledgements are only valid if originating from someone else; best if persisted between sessions
|
||||||
|
* @param encoder A channel operates within a singular encryption layer, hence the same encoder is needed for all messages
|
||||||
|
* @param decoder A channel operates within a singular encryption layer, hence the same decoder is needed for all messages
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
public static async create<T extends IDecodedMessage>(
|
||||||
|
node: IWaku,
|
||||||
|
channelId: ChannelId,
|
||||||
|
senderId: SenderId,
|
||||||
|
encoder: IEncoder,
|
||||||
|
decoder: IDecoder<T>,
|
||||||
|
options?: ReliableChannelOptions
|
||||||
|
): Promise<ReliableChannel<T>> {
|
||||||
|
const sdsMessageChannel = new MessageChannel(channelId, senderId, options);
|
||||||
|
const messageChannel = new ReliableChannel(
|
||||||
|
node,
|
||||||
|
sdsMessageChannel,
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoStart = options?.autoStart ?? true;
|
||||||
|
if (autoStart) {
|
||||||
|
await messageChannel.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message in the channel, will attempt to re-send if not acknowledged
|
||||||
|
* by other participants.
|
||||||
|
*
|
||||||
|
* @param messagePayload
|
||||||
|
* @returns the message id
|
||||||
|
*/
|
||||||
|
public send(messagePayload: Uint8Array): string {
|
||||||
|
const messageId = ReliableChannel.getMessageId(messagePayload);
|
||||||
|
if (!this._started) {
|
||||||
|
this.safeSendEvent("sending-message-irrecoverable-error", {
|
||||||
|
detail: { messageId: messageId, error: "channel is not started" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const wrapAndSendBind = this._wrapAndSend.bind(this, messagePayload);
|
||||||
|
this.retryManager?.startRetries(messageId, wrapAndSendBind);
|
||||||
|
wrapAndSendBind();
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _wrapAndSend(messagePayload: Uint8Array): void {
|
||||||
|
this.messageChannel.pushOutgoingMessage(
|
||||||
|
messagePayload,
|
||||||
|
async (
|
||||||
|
sdsMessage: SdsMessage
|
||||||
|
): Promise<{ success: boolean; retrievalHint?: Uint8Array }> => {
|
||||||
|
// Callback is called once message has added to the SDS outgoing queue
|
||||||
|
// We start by trying to send the message now.
|
||||||
|
|
||||||
|
// `payload` wrapped in SDS
|
||||||
|
const sdsPayload = sdsMessage.encode();
|
||||||
|
|
||||||
|
const wakuMessage = {
|
||||||
|
payload: sdsPayload
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageId = ReliableChannel.getMessageId(messagePayload);
|
||||||
|
|
||||||
|
// TODO: should the encoder give me the message hash?
|
||||||
|
// Encoding now to fail early, used later to get message hash
|
||||||
|
const protoMessage = await this.encoder.toProtoObj(wakuMessage);
|
||||||
|
if (!protoMessage) {
|
||||||
|
this.safeSendEvent("sending-message-irrecoverable-error", {
|
||||||
|
detail: {
|
||||||
|
messageId: messageId,
|
||||||
|
error: "could not encode message"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
const retrievalHint = messageHash(
|
||||||
|
this.encoder.pubsubTopic,
|
||||||
|
protoMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
this.safeSendEvent("sending-message", {
|
||||||
|
detail: messageId
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendRes = await this._send(this.encoder, wakuMessage);
|
||||||
|
|
||||||
|
// If it's a recoverable failure, we will try again to send later
|
||||||
|
// If not, then we should error to the user now
|
||||||
|
for (const { error } of sendRes.failures) {
|
||||||
|
if (IRRECOVERABLE_SENDING_ERRORS.includes(error)) {
|
||||||
|
// Not recoverable, best to return it
|
||||||
|
log.error("Irrecoverable error, cannot send message: ", error);
|
||||||
|
this.safeSendEvent("sending-message-irrecoverable-error", {
|
||||||
|
detail: {
|
||||||
|
messageId,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { success: false, retrievalHint };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
retrievalHint
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process outgoing messages straight away
|
||||||
|
this.messageChannel
|
||||||
|
.processTasks()
|
||||||
|
.then(() => {
|
||||||
|
this.messageChannel.sweepOutgoingBuffer();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log.error("error encountered when processing sds tasks", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribe(): Promise<boolean> {
|
||||||
|
this.assertStarted();
|
||||||
|
return this._subscribe(this.decoder, async (message: T) => {
|
||||||
|
await this.processIncomingMessage(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't forget to call `this.messageChannel.sweepIncomingBuffer();` once done.
|
||||||
|
* @param msg
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async processIncomingMessage<T extends IDecodedMessage>(
|
||||||
|
msg: T
|
||||||
|
): Promise<void> {
|
||||||
|
// New message arrives, we need to unwrap it first
|
||||||
|
const sdsMessage = SdsMessage.decode(msg.payload);
|
||||||
|
|
||||||
|
if (!sdsMessage) {
|
||||||
|
log.error("could not SDS decode message", msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdsMessage.channelId !== this.messageChannel.channelId) {
|
||||||
|
log.warn(
|
||||||
|
"ignoring message with different channel id",
|
||||||
|
sdsMessage.channelId
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrievalHint = msg.hash;
|
||||||
|
log.info(`processing message ${sdsMessage.messageId}:${msg.hashStr}`);
|
||||||
|
// SDS Message decoded, let's pass it to the channel so we can learn about
|
||||||
|
// missing messages or the status of previous outgoing messages
|
||||||
|
this.messageChannel.pushIncomingMessage(sdsMessage, retrievalHint);
|
||||||
|
|
||||||
|
this.missingMessageRetriever?.removeMissingMessage(sdsMessage.messageId);
|
||||||
|
|
||||||
|
if (sdsMessage.content && sdsMessage.content.length > 0) {
|
||||||
|
// Now, process the message with callback
|
||||||
|
|
||||||
|
// Overrides msg.payload with unwrapped payload
|
||||||
|
// TODO: can we do better?
|
||||||
|
const { payload: _p, ...allButPayload } = msg;
|
||||||
|
const unwrappedMessage = Object.assign(allButPayload, {
|
||||||
|
payload: sdsMessage.content,
|
||||||
|
hash: msg.hash,
|
||||||
|
hashStr: msg.hashStr,
|
||||||
|
version: msg.version,
|
||||||
|
contentTopic: msg.contentTopic,
|
||||||
|
pubsubTopic: msg.pubsubTopic,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
rateLimitProof: msg.rateLimitProof,
|
||||||
|
ephemeral: msg.ephemeral,
|
||||||
|
meta: msg.meta
|
||||||
|
});
|
||||||
|
|
||||||
|
this.safeSendEvent("message-received", {
|
||||||
|
detail: unwrappedMessage as unknown as T
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queueProcessTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processIncomingMessages<T extends IDecodedMessage>(
|
||||||
|
messages: T[]
|
||||||
|
): Promise<void> {
|
||||||
|
for (const message of messages) {
|
||||||
|
await this.processIncomingMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: For now we only queue process tasks for incoming messages
|
||||||
|
// As this is where there is most volume
|
||||||
|
private queueProcessTasks(): void {
|
||||||
|
// If one is already queued, then we can ignore it
|
||||||
|
if (this.processTaskTimeout === undefined) {
|
||||||
|
this.processTaskTimeout = setTimeout(() => {
|
||||||
|
void this.messageChannel.processTasks().catch((err) => {
|
||||||
|
log.error("error encountered when processing sds tasks", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear timeout once triggered
|
||||||
|
clearTimeout(this.processTaskTimeout);
|
||||||
|
this.processTaskTimeout = undefined;
|
||||||
|
}, this.processTaskMinElapseMs); // we ensure that we don't call process tasks more than once per second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<boolean> {
|
||||||
|
if (this._started) return true;
|
||||||
|
this._started = true;
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.restartSync();
|
||||||
|
this.startSweepIncomingBufferLoop();
|
||||||
|
if (this._retrieve) {
|
||||||
|
this.missingMessageRetriever?.start();
|
||||||
|
this.queryOnConnect?.start();
|
||||||
|
}
|
||||||
|
return this.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
if (!this._started) return;
|
||||||
|
this._started = false;
|
||||||
|
this.stopSync();
|
||||||
|
this.stopSweepIncomingBufferLoop();
|
||||||
|
this.missingMessageRetriever?.stop();
|
||||||
|
this.queryOnConnect?.stop();
|
||||||
|
// TODO unsubscribe
|
||||||
|
// TODO unsetMessageListeners
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertStarted(): void {
|
||||||
|
if (!this._started) throw Error("Message Channel must be started");
|
||||||
|
}
|
||||||
|
|
||||||
|
private startSweepIncomingBufferLoop(): void {
|
||||||
|
this.stopSweepIncomingBufferLoop();
|
||||||
|
this.sweepInBufInterval = setInterval(() => {
|
||||||
|
log.info("sweep incoming buffer");
|
||||||
|
this.messageChannel.sweepIncomingBuffer();
|
||||||
|
}, this.sweepInBufIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopSweepIncomingBufferLoop(): void {
|
||||||
|
if (this.sweepInBufInterval) clearInterval(this.sweepInBufInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private restartSync(multiplier: number = 1): void {
|
||||||
|
if (this.syncTimeout) {
|
||||||
|
clearTimeout(this.syncTimeout);
|
||||||
|
}
|
||||||
|
if (this.syncMinIntervalMs) {
|
||||||
|
const timeoutMs = this.random() * this.syncMinIntervalMs * multiplier;
|
||||||
|
|
||||||
|
this.syncTimeout = setTimeout(() => {
|
||||||
|
void this.sendSyncMessage();
|
||||||
|
// Always restart a sync, no matter whether the message was sent.
|
||||||
|
// Set a multiplier so we wait a bit longer to not hog the conversation
|
||||||
|
void this.restartSync(2);
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopSync(): void {
|
||||||
|
if (this.syncTimeout) {
|
||||||
|
clearTimeout(this.syncTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to enable overriding when testing
|
||||||
|
private random(): number {
|
||||||
|
return Math.random();
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeSendEvent<T extends ReliableChannelEvent>(
|
||||||
|
event: T,
|
||||||
|
eventInit?: CustomEventInit
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
this.dispatchEvent(new CustomEvent(event, eventInit));
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Failed to dispatch event ${event}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendSyncMessage(): Promise<void> {
|
||||||
|
this.assertStarted();
|
||||||
|
await this.messageChannel.pushOutgoingSyncMessage(
|
||||||
|
async (syncMessage: SyncMessage): Promise<boolean> => {
|
||||||
|
// Callback is called once message has added to the SDS outgoing queue
|
||||||
|
// We start by trying to send the message now.
|
||||||
|
|
||||||
|
// `payload` wrapped in SDS
|
||||||
|
const sdsPayload = syncMessage.encode();
|
||||||
|
|
||||||
|
const wakuMessage = {
|
||||||
|
payload: sdsPayload
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendRes = await this._send(this.encoder, wakuMessage);
|
||||||
|
if (sendRes.failures.length > 0) {
|
||||||
|
log.error("Error sending sync message: ", sendRes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process outgoing messages straight away
|
||||||
|
// TODO: review and optimize
|
||||||
|
await this.messageChannel.processTasks();
|
||||||
|
this.messageChannel.sweepOutgoingBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isChannelMessageWithCausalHistory(msg: T): boolean {
|
||||||
|
// TODO: we do end-up decoding messages twice as this is used to stop store queries.
|
||||||
|
const sdsMessage = SdsMessage.decode(msg.payload);
|
||||||
|
|
||||||
|
if (!sdsMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdsMessage.channelId !== this.messageChannel.channelId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdsMessage.causalHistory && sdsMessage.causalHistory.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEventListeners(): void {
|
||||||
|
this.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutMessageSent,
|
||||||
|
(event) => {
|
||||||
|
if (event.detail.content) {
|
||||||
|
const messageId = ReliableChannel.getMessageId(event.detail.content);
|
||||||
|
this.safeSendEvent("message-sent", {
|
||||||
|
detail: messageId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutMessageAcknowledged,
|
||||||
|
(event) => {
|
||||||
|
if (event.detail) {
|
||||||
|
this.safeSendEvent("message-acknowledged", {
|
||||||
|
detail: event.detail
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stopping retries
|
||||||
|
this.retryManager?.stopRetries(event.detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutMessagePossiblyAcknowledged,
|
||||||
|
(event) => {
|
||||||
|
if (event.detail) {
|
||||||
|
this.safeSendEvent("message-possibly-acknowledged", {
|
||||||
|
detail: {
|
||||||
|
messageId: event.detail.messageId,
|
||||||
|
possibleAckCount: event.detail.count
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.InSyncReceived,
|
||||||
|
(_event) => {
|
||||||
|
// restart the timeout when a sync message has been received
|
||||||
|
this.restartSync();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.InMessageReceived,
|
||||||
|
(event) => {
|
||||||
|
// restart the timeout when a content message has been received
|
||||||
|
if (isContentMessage(event.detail)) {
|
||||||
|
// send a sync message faster to ack someone's else
|
||||||
|
this.restartSync(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutMessageSent,
|
||||||
|
(event) => {
|
||||||
|
// restart the timeout when a content message has been sent
|
||||||
|
if (isContentMessage(event.detail)) {
|
||||||
|
this.restartSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.InMessageMissing,
|
||||||
|
(event) => {
|
||||||
|
for (const { messageId, retrievalHint } of event.detail) {
|
||||||
|
if (retrievalHint && this.missingMessageRetriever) {
|
||||||
|
this.missingMessageRetriever.addMissingMessage(
|
||||||
|
messageId,
|
||||||
|
retrievalHint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.queryOnConnect) {
|
||||||
|
this.queryOnConnect.addEventListener(
|
||||||
|
QueryOnConnectEvent.MessagesRetrieved,
|
||||||
|
(event) => {
|
||||||
|
void this.processIncomingMessages(event.detail);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
packages/sdk/src/reliable_channel/reliable_channel_acks.spec.ts
Normal file
187
packages/sdk/src/reliable_channel/reliable_channel_acks.spec.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import { TypedEventEmitter } from "@libp2p/interface";
|
||||||
|
import { createDecoder, createEncoder } from "@waku/core";
|
||||||
|
import {
|
||||||
|
AutoSharding,
|
||||||
|
IDecodedMessage,
|
||||||
|
IDecoder,
|
||||||
|
IEncoder
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import {
|
||||||
|
createRoutingInfo,
|
||||||
|
delay,
|
||||||
|
MockWakuEvents,
|
||||||
|
MockWakuNode
|
||||||
|
} from "@waku/utils";
|
||||||
|
import { bytesToUtf8, utf8ToBytes } from "@waku/utils/bytes";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { beforeEach, describe } from "mocha";
|
||||||
|
|
||||||
|
import { ReliableChannel } from "./index.js";
|
||||||
|
|
||||||
|
const TEST_CONTENT_TOPIC = "/my-tests/0/topic-name/proto";
|
||||||
|
const TEST_NETWORK_CONFIG: AutoSharding = {
|
||||||
|
clusterId: 0,
|
||||||
|
numShardsInCluster: 1
|
||||||
|
};
|
||||||
|
const TEST_ROUTING_INFO = createRoutingInfo(TEST_NETWORK_CONFIG, {
|
||||||
|
contentTopic: TEST_CONTENT_TOPIC
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Reliable Channel: Acks", () => {
|
||||||
|
let encoder: IEncoder;
|
||||||
|
let decoder: IDecoder<IDecodedMessage>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
encoder = createEncoder({
|
||||||
|
contentTopic: TEST_CONTENT_TOPIC,
|
||||||
|
routingInfo: TEST_ROUTING_INFO
|
||||||
|
});
|
||||||
|
decoder = createDecoder(TEST_CONTENT_TOPIC, TEST_ROUTING_INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Outgoing message is acknowledged", async () => {
|
||||||
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
|
||||||
|
const reliableChannelAlice = await ReliableChannel.create(
|
||||||
|
mockWakuNodeAlice,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
const reliableChannelBob = await ReliableChannel.create(
|
||||||
|
mockWakuNodeBob,
|
||||||
|
"MyChannel",
|
||||||
|
"bob",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = utf8ToBytes("first message in channel");
|
||||||
|
|
||||||
|
// Alice sets up message tracking
|
||||||
|
const messageId = ReliableChannel.getMessageId(message);
|
||||||
|
|
||||||
|
let messageReceived = false;
|
||||||
|
reliableChannelBob.addEventListener("message-received", (event) => {
|
||||||
|
if (bytesToUtf8(event.detail.payload) === "first message in channel") {
|
||||||
|
messageReceived = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let messageAcknowledged = false;
|
||||||
|
reliableChannelAlice.addEventListener("message-acknowledged", (event) => {
|
||||||
|
if (event.detail === messageId) {
|
||||||
|
messageAcknowledged = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reliableChannelAlice.send(message);
|
||||||
|
|
||||||
|
// Wait for Bob to receive the message to ensure it uses it in causal history
|
||||||
|
while (!messageReceived) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
// Bobs sends a message now, it should include first one in causal history
|
||||||
|
reliableChannelBob.send(utf8ToBytes("second message in channel"));
|
||||||
|
while (!messageAcknowledged) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(messageAcknowledged).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Re-sent message is acknowledged once other parties join.", async () => {
|
||||||
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
|
|
||||||
|
// Setup, Alice first
|
||||||
|
const reliableChannelAlice = await ReliableChannel.create(
|
||||||
|
mockWakuNodeAlice,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
retryIntervalMs: 0, // disable any automation to better control the test
|
||||||
|
syncMinIntervalMs: 0,
|
||||||
|
processTaskMinElapseMs: 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob is offline, Alice sends a message, this is the message we want
|
||||||
|
// acknowledged in this test.
|
||||||
|
const message = utf8ToBytes("message to be acknowledged");
|
||||||
|
const messageId = ReliableChannel.getMessageId(message);
|
||||||
|
let messageAcknowledged = false;
|
||||||
|
reliableChannelAlice.addEventListener("message-acknowledged", (event) => {
|
||||||
|
if (event.detail === messageId) {
|
||||||
|
messageAcknowledged = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reliableChannelAlice.send(message);
|
||||||
|
|
||||||
|
// Wait a bit to ensure Bob does not receive the message
|
||||||
|
await delay(100);
|
||||||
|
|
||||||
|
// Now Bob goes online
|
||||||
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
const reliableChannelBob = await ReliableChannel.create(
|
||||||
|
mockWakuNodeBob,
|
||||||
|
"MyChannel",
|
||||||
|
"bob",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
retryIntervalMs: 0, // disable any automation to better control the test
|
||||||
|
syncMinIntervalMs: 0,
|
||||||
|
processTaskMinElapseMs: 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track when Bob receives the message
|
||||||
|
let bobReceivedMessage = false;
|
||||||
|
reliableChannelBob.addEventListener("message-received", (event) => {
|
||||||
|
if (bytesToUtf8(event.detail.payload!) === "message to be acknowledged") {
|
||||||
|
bobReceivedMessage = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Some sync messages are exchanged
|
||||||
|
await reliableChannelAlice["sendSyncMessage"]();
|
||||||
|
await reliableChannelBob["sendSyncMessage"]();
|
||||||
|
|
||||||
|
// wait a bit to ensure messages are processed
|
||||||
|
await delay(100);
|
||||||
|
|
||||||
|
// Some content messages are exchanged too
|
||||||
|
reliableChannelAlice.send(utf8ToBytes("some message"));
|
||||||
|
reliableChannelBob.send(utf8ToBytes("some other message"));
|
||||||
|
|
||||||
|
// wait a bit to ensure messages are processed
|
||||||
|
await delay(100);
|
||||||
|
|
||||||
|
// At this point, the message shouldn't be acknowledged yet as Bob
|
||||||
|
// does not have a complete log
|
||||||
|
expect(messageAcknowledged).to.be.false;
|
||||||
|
|
||||||
|
// Now Alice resends the message
|
||||||
|
reliableChannelAlice.send(message);
|
||||||
|
|
||||||
|
// Wait for Bob to receive the message
|
||||||
|
while (!bobReceivedMessage) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob receives it, and should include it in its sync
|
||||||
|
await reliableChannelBob["sendSyncMessage"]();
|
||||||
|
while (!messageAcknowledged) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sync should acknowledge the message
|
||||||
|
expect(messageAcknowledged).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,327 @@
|
|||||||
|
import { TypedEventEmitter } from "@libp2p/interface";
|
||||||
|
import {
|
||||||
|
AutoSharding,
|
||||||
|
IDecodedMessage,
|
||||||
|
IDecoder,
|
||||||
|
IEncoder,
|
||||||
|
type IMessage,
|
||||||
|
ISendOptions,
|
||||||
|
IWaku,
|
||||||
|
LightPushError,
|
||||||
|
LightPushSDKResult
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { generatePrivateKey, getPublicKey } from "@waku/message-encryption";
|
||||||
|
import {
|
||||||
|
createDecoder as createEciesDecoder,
|
||||||
|
createEncoder as createEciesEncoder
|
||||||
|
} from "@waku/message-encryption/ecies";
|
||||||
|
import {
|
||||||
|
createRoutingInfo,
|
||||||
|
delay,
|
||||||
|
MockWakuEvents,
|
||||||
|
MockWakuNode
|
||||||
|
} from "@waku/utils";
|
||||||
|
import { bytesToUtf8, utf8ToBytes } from "@waku/utils/bytes";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { beforeEach, describe } from "mocha";
|
||||||
|
|
||||||
|
import { ReliableChannel } from "./index.js";
|
||||||
|
|
||||||
|
const TEST_CONTENT_TOPIC = "/my-tests/0/topic-name/proto";
|
||||||
|
const TEST_NETWORK_CONFIG: AutoSharding = {
|
||||||
|
clusterId: 0,
|
||||||
|
numShardsInCluster: 1
|
||||||
|
};
|
||||||
|
const TEST_ROUTING_INFO = createRoutingInfo(TEST_NETWORK_CONFIG, {
|
||||||
|
contentTopic: TEST_CONTENT_TOPIC
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Reliable Channel: Encryption", () => {
|
||||||
|
let mockWakuNode: IWaku;
|
||||||
|
let encoder: IEncoder;
|
||||||
|
let decoder: IDecoder<IDecodedMessage>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockWakuNode = new MockWakuNode();
|
||||||
|
const privateKey = generatePrivateKey();
|
||||||
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
encoder = createEciesEncoder({
|
||||||
|
contentTopic: TEST_CONTENT_TOPIC,
|
||||||
|
routingInfo: TEST_ROUTING_INFO,
|
||||||
|
publicKey
|
||||||
|
});
|
||||||
|
decoder = createEciesDecoder(
|
||||||
|
TEST_CONTENT_TOPIC,
|
||||||
|
TEST_ROUTING_INFO,
|
||||||
|
privateKey
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Outgoing message is emitted as sending", async () => {
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = utf8ToBytes("message in channel");
|
||||||
|
|
||||||
|
// Setting up message tracking
|
||||||
|
const messageId = ReliableChannel.getMessageId(message);
|
||||||
|
let messageSending = false;
|
||||||
|
reliableChannel.addEventListener("sending-message", (event) => {
|
||||||
|
if (event.detail === messageId) {
|
||||||
|
messageSending = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reliableChannel.send(message);
|
||||||
|
while (!messageSending) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(messageSending).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Outgoing message is emitted as sent", async () => {
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = utf8ToBytes("message in channel");
|
||||||
|
|
||||||
|
// Setting up message tracking
|
||||||
|
const messageId = ReliableChannel.getMessageId(message);
|
||||||
|
let messageSent = false;
|
||||||
|
reliableChannel.addEventListener("message-sent", (event) => {
|
||||||
|
if (event.detail === messageId) {
|
||||||
|
messageSent = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reliableChannel.send(message);
|
||||||
|
while (!messageSent) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(messageSent).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Encoder error raises irrecoverable error", async () => {
|
||||||
|
mockWakuNode.lightPush!.send = (
|
||||||
|
_encoder: IEncoder,
|
||||||
|
_message: IMessage,
|
||||||
|
_sendOptions?: ISendOptions
|
||||||
|
): Promise<LightPushSDKResult> => {
|
||||||
|
return Promise.resolve({
|
||||||
|
failures: [{ error: LightPushError.EMPTY_PAYLOAD }],
|
||||||
|
successes: []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = utf8ToBytes("payload doesnt matter");
|
||||||
|
|
||||||
|
// Setting up message tracking
|
||||||
|
const messageId = ReliableChannel.getMessageId(message);
|
||||||
|
let irrecoverableError = false;
|
||||||
|
reliableChannel.addEventListener(
|
||||||
|
"sending-message-irrecoverable-error",
|
||||||
|
(event) => {
|
||||||
|
if (event.detail.messageId === messageId) {
|
||||||
|
irrecoverableError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
encoder.contentTopic = "...";
|
||||||
|
reliableChannel.send(message);
|
||||||
|
while (!irrecoverableError) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(irrecoverableError).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Outgoing message is not emitted as acknowledged from own outgoing messages", async () => {
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = utf8ToBytes("first message in channel");
|
||||||
|
|
||||||
|
// Setting up message tracking
|
||||||
|
const messageId = ReliableChannel.getMessageId(message);
|
||||||
|
let messageAcknowledged = false;
|
||||||
|
reliableChannel.addEventListener("message-acknowledged", (event) => {
|
||||||
|
if (event.detail === messageId) {
|
||||||
|
messageAcknowledged = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reliableChannel.send(message);
|
||||||
|
|
||||||
|
// Sending a second message from the same node should not acknowledge the first one
|
||||||
|
reliableChannel.send(utf8ToBytes("second message in channel"));
|
||||||
|
|
||||||
|
// Wait a bit to be sure no event is emitted
|
||||||
|
await delay(200);
|
||||||
|
|
||||||
|
expect(messageAcknowledged).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: https://github.com/waku-org/js-waku/issues/2648
|
||||||
|
it.skip("Outgoing message is possibly acknowledged", async () => {
|
||||||
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
|
||||||
|
const reliableChannelAlice = await ReliableChannel.create(
|
||||||
|
mockWakuNodeAlice,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
const reliableChannelBob = await ReliableChannel.create(
|
||||||
|
mockWakuNodeBob,
|
||||||
|
"MyChannel",
|
||||||
|
"bob",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
// Bob only includes one message in causal history
|
||||||
|
{ causalHistorySize: 1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = ["first", "second", "third"].map((m) => {
|
||||||
|
return utf8ToBytes(m);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alice sets up message tracking for first message
|
||||||
|
const firstMessageId = ReliableChannel.getMessageId(messages[0]);
|
||||||
|
let firstMessagePossiblyAcknowledged = false;
|
||||||
|
reliableChannelAlice.addEventListener(
|
||||||
|
"message-possibly-acknowledged",
|
||||||
|
(event) => {
|
||||||
|
if (event.detail.messageId === firstMessageId) {
|
||||||
|
firstMessagePossiblyAcknowledged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let bobMessageReceived = 0;
|
||||||
|
reliableChannelAlice.addEventListener("message-received", () => {
|
||||||
|
bobMessageReceived++;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const m of messages) {
|
||||||
|
reliableChannelAlice.send(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Bob to receive all messages to ensure filter is updated
|
||||||
|
while (bobMessageReceived < 3) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bobs sends a message now, it should include first one in bloom filter
|
||||||
|
reliableChannelBob.send(utf8ToBytes("message back"));
|
||||||
|
while (!firstMessagePossiblyAcknowledged) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(firstMessagePossiblyAcknowledged).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Outgoing message is acknowledged", async () => {
|
||||||
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
|
||||||
|
const reliableChannelAlice = await ReliableChannel.create(
|
||||||
|
mockWakuNodeAlice,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
const reliableChannelBob = await ReliableChannel.create(
|
||||||
|
mockWakuNodeBob,
|
||||||
|
"MyChannel",
|
||||||
|
"bob",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = utf8ToBytes("first message in channel");
|
||||||
|
|
||||||
|
// Alice sets up message tracking
|
||||||
|
const messageId = ReliableChannel.getMessageId(message);
|
||||||
|
let messageAcknowledged = false;
|
||||||
|
reliableChannelAlice.addEventListener("message-acknowledged", (event) => {
|
||||||
|
if (event.detail === messageId) {
|
||||||
|
messageAcknowledged = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let bobReceivedMessage = false;
|
||||||
|
reliableChannelBob.addEventListener("message-received", () => {
|
||||||
|
bobReceivedMessage = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
reliableChannelAlice.send(message);
|
||||||
|
|
||||||
|
// Wait for Bob to receive the message
|
||||||
|
while (!bobReceivedMessage) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bobs sends a message now, it should include first one in causal history
|
||||||
|
reliableChannelBob.send(utf8ToBytes("second message in channel"));
|
||||||
|
while (!messageAcknowledged) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(messageAcknowledged).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Incoming message is emitted as received", async () => {
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder
|
||||||
|
);
|
||||||
|
|
||||||
|
let receivedMessage: IDecodedMessage;
|
||||||
|
reliableChannel.addEventListener("message-received", (event) => {
|
||||||
|
receivedMessage = event.detail;
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = utf8ToBytes("message in channel");
|
||||||
|
|
||||||
|
reliableChannel.send(message);
|
||||||
|
while (!receivedMessage!) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(bytesToUtf8(receivedMessage!.payload)).to.eq(bytesToUtf8(message));
|
||||||
|
});
|
||||||
|
});
|
||||||
397
packages/sdk/src/reliable_channel/reliable_channel_sync.spec.ts
Normal file
397
packages/sdk/src/reliable_channel/reliable_channel_sync.spec.ts
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
import { TypedEventEmitter } from "@libp2p/interface";
|
||||||
|
import { createDecoder, createEncoder } from "@waku/core";
|
||||||
|
import {
|
||||||
|
AutoSharding,
|
||||||
|
IDecodedMessage,
|
||||||
|
IDecoder,
|
||||||
|
IEncoder,
|
||||||
|
IWaku
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { MessageChannelEvent } from "@waku/sds";
|
||||||
|
import {
|
||||||
|
createRoutingInfo,
|
||||||
|
delay,
|
||||||
|
MockWakuEvents,
|
||||||
|
MockWakuNode
|
||||||
|
} from "@waku/utils";
|
||||||
|
import { utf8ToBytes } from "@waku/utils/bytes";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { beforeEach, describe } from "mocha";
|
||||||
|
|
||||||
|
import { ReliableChannel } from "./index.js";
|
||||||
|
|
||||||
|
const TEST_CONTENT_TOPIC = "/my-tests/0/topic-name/proto";
|
||||||
|
const TEST_NETWORK_CONFIG: AutoSharding = {
|
||||||
|
clusterId: 0,
|
||||||
|
numShardsInCluster: 1
|
||||||
|
};
|
||||||
|
const TEST_ROUTING_INFO = createRoutingInfo(TEST_NETWORK_CONFIG, {
|
||||||
|
contentTopic: TEST_CONTENT_TOPIC
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Reliable Channel: Sync", () => {
|
||||||
|
let mockWakuNode: IWaku;
|
||||||
|
let encoder: IEncoder;
|
||||||
|
let decoder: IDecoder<IDecodedMessage>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockWakuNode = new MockWakuNode();
|
||||||
|
encoder = createEncoder({
|
||||||
|
contentTopic: TEST_CONTENT_TOPIC,
|
||||||
|
routingInfo: TEST_ROUTING_INFO
|
||||||
|
});
|
||||||
|
decoder = createDecoder(TEST_CONTENT_TOPIC, TEST_ROUTING_INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Sync message is sent within sync frequency", async () => {
|
||||||
|
const syncMinIntervalMs = 100;
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
syncMinIntervalMs
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send a message to have a history
|
||||||
|
const sentMsgId = reliableChannel.send(utf8ToBytes("some message"));
|
||||||
|
let messageSent = false;
|
||||||
|
reliableChannel.addEventListener("message-sent", (event) => {
|
||||||
|
if (event.detail === sentMsgId) {
|
||||||
|
messageSent = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!messageSent) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncMessageSent = false;
|
||||||
|
reliableChannel.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutSyncSent,
|
||||||
|
(_event) => {
|
||||||
|
syncMessageSent = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await delay(syncMinIntervalMs);
|
||||||
|
|
||||||
|
expect(syncMessageSent).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Sync message are not sent excessively within sync frequency", async () => {
|
||||||
|
const syncMinIntervalMs = 100;
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
syncMinIntervalMs
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let syncMessageSentCount = 0;
|
||||||
|
reliableChannel.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutSyncSent,
|
||||||
|
(_event) => {
|
||||||
|
syncMessageSentCount++;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await delay(syncMinIntervalMs);
|
||||||
|
|
||||||
|
// There is randomness to this, but it should not be excessive
|
||||||
|
expect(syncMessageSentCount).to.be.lessThan(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Sync message is not sent if another sync message was just received", async function () {
|
||||||
|
this.timeout(5000);
|
||||||
|
|
||||||
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
|
||||||
|
const syncMinIntervalMs = 1000;
|
||||||
|
|
||||||
|
const reliableChannelAlice = await ReliableChannel.create(
|
||||||
|
mockWakuNodeAlice,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
syncMinIntervalMs: 0, // does not send sync messages automatically
|
||||||
|
processTaskMinElapseMs: 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const reliableChannelBob = await ReliableChannel.create(
|
||||||
|
mockWakuNodeBob,
|
||||||
|
"MyChannel",
|
||||||
|
"bob",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
syncMinIntervalMs,
|
||||||
|
processTaskMinElapseMs: 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(reliableChannelBob as any).random = () => {
|
||||||
|
return 1;
|
||||||
|
}; // will wait a full second
|
||||||
|
|
||||||
|
// Send a message to have a history
|
||||||
|
const sentMsgId = reliableChannelAlice.send(utf8ToBytes("some message"));
|
||||||
|
let messageSent = false;
|
||||||
|
reliableChannelAlice.addEventListener("message-sent", (event) => {
|
||||||
|
if (event.detail === sentMsgId) {
|
||||||
|
messageSent = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!messageSent) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncMessageSent = false;
|
||||||
|
reliableChannelBob.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutSyncSent,
|
||||||
|
(_event) => {
|
||||||
|
syncMessageSent = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
while (!syncMessageSent) {
|
||||||
|
// Bob will send a sync message as soon as it started, we are waiting for this one
|
||||||
|
await delay(100);
|
||||||
|
}
|
||||||
|
// Let's reset the tracker
|
||||||
|
syncMessageSent = false;
|
||||||
|
// We should be faster than Bob as Bob will "randomly" wait a full second
|
||||||
|
await reliableChannelAlice["sendSyncMessage"]();
|
||||||
|
|
||||||
|
// Bob should be waiting a full second before sending a message after Alice
|
||||||
|
await delay(900);
|
||||||
|
|
||||||
|
// Now, let's wait Bob to send the sync message
|
||||||
|
await delay(200);
|
||||||
|
expect(syncMessageSent).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Sync message is not sent if another non-ephemeral message was just received", async function () {
|
||||||
|
this.timeout(5000);
|
||||||
|
|
||||||
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
|
||||||
|
const syncMinIntervalMs = 1000;
|
||||||
|
|
||||||
|
const reliableChannelAlice = await ReliableChannel.create(
|
||||||
|
mockWakuNodeAlice,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
syncMinIntervalMs: 0, // does not send sync messages automatically
|
||||||
|
processTaskMinElapseMs: 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const reliableChannelBob = await ReliableChannel.create(
|
||||||
|
mockWakuNodeBob,
|
||||||
|
"MyChannel",
|
||||||
|
"bob",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
syncMinIntervalMs,
|
||||||
|
processTaskMinElapseMs: 10
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(reliableChannelBob as any).random = () => {
|
||||||
|
return 1;
|
||||||
|
}; // will wait a full second
|
||||||
|
|
||||||
|
// Send a message to have a history
|
||||||
|
const sentMsgId = reliableChannelAlice.send(utf8ToBytes("some message"));
|
||||||
|
let messageSent = false;
|
||||||
|
reliableChannelAlice.addEventListener("message-sent", (event) => {
|
||||||
|
if (event.detail === sentMsgId) {
|
||||||
|
messageSent = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!messageSent) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncMessageSent = false;
|
||||||
|
reliableChannelBob.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutSyncSent,
|
||||||
|
(_event) => {
|
||||||
|
syncMessageSent = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
while (!syncMessageSent) {
|
||||||
|
// Bob will send a sync message as soon as it started, we are waiting for this one
|
||||||
|
await delay(100);
|
||||||
|
}
|
||||||
|
// Let's reset the tracker
|
||||||
|
syncMessageSent = false;
|
||||||
|
// We should be faster than Bob as Bob will "randomly" wait a full second
|
||||||
|
reliableChannelAlice.send(utf8ToBytes("some message"));
|
||||||
|
|
||||||
|
// Bob should be waiting a full second before sending a message after Alice
|
||||||
|
await delay(900);
|
||||||
|
|
||||||
|
// Now, let's wait Bob to send the sync message
|
||||||
|
await delay(200);
|
||||||
|
expect(syncMessageSent).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Sync message is not sent if another sync message was just sent", async function () {
|
||||||
|
this.timeout(5000);
|
||||||
|
const syncMinIntervalMs = 1000;
|
||||||
|
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{ syncMinIntervalMs }
|
||||||
|
);
|
||||||
|
(reliableChannel as any).random = () => {
|
||||||
|
return 1;
|
||||||
|
}; // will wait a full second
|
||||||
|
|
||||||
|
// Send a message to have a history
|
||||||
|
const sentMsgId = reliableChannel.send(utf8ToBytes("some message"));
|
||||||
|
let messageSent = false;
|
||||||
|
reliableChannel.addEventListener("message-sent", (event) => {
|
||||||
|
if (event.detail === sentMsgId) {
|
||||||
|
messageSent = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!messageSent) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncMessageSent = false;
|
||||||
|
reliableChannel.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutSyncSent,
|
||||||
|
(_event) => {
|
||||||
|
syncMessageSent = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
while (!syncMessageSent) {
|
||||||
|
// Will send a sync message as soon as it started, we are waiting for this one
|
||||||
|
await delay(100);
|
||||||
|
}
|
||||||
|
// Let's reset the tracker
|
||||||
|
syncMessageSent = false;
|
||||||
|
// We should be faster than automated sync as it will "randomly" wait a full second
|
||||||
|
await reliableChannel["sendSyncMessage"]();
|
||||||
|
|
||||||
|
// should be waiting a full second before sending a message after Alice
|
||||||
|
await delay(900);
|
||||||
|
|
||||||
|
// Now, let's wait to send the automated sync message
|
||||||
|
await delay(200);
|
||||||
|
expect(syncMessageSent).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Sync message is not sent if another non-ephemeral message was just sent", async function () {
|
||||||
|
this.timeout(5000);
|
||||||
|
const syncMinIntervalMs = 1000;
|
||||||
|
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{ syncMinIntervalMs }
|
||||||
|
);
|
||||||
|
(reliableChannel as any).random = () => {
|
||||||
|
return 1;
|
||||||
|
}; // will wait a full second
|
||||||
|
|
||||||
|
// Send a message to have a history
|
||||||
|
const sentMsgId = reliableChannel.send(utf8ToBytes("some message"));
|
||||||
|
let messageSent = false;
|
||||||
|
reliableChannel.addEventListener("message-sent", (event) => {
|
||||||
|
if (event.detail === sentMsgId) {
|
||||||
|
messageSent = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!messageSent) {
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncMessageSent = false;
|
||||||
|
reliableChannel.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutSyncSent,
|
||||||
|
(_event) => {
|
||||||
|
syncMessageSent = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
while (!syncMessageSent) {
|
||||||
|
// Will send a sync message as soon as it started, we are waiting for this one
|
||||||
|
await delay(100);
|
||||||
|
}
|
||||||
|
// Let's reset the tracker
|
||||||
|
syncMessageSent = false;
|
||||||
|
// We should be faster than automated sync as it will "randomly" wait a full second
|
||||||
|
reliableChannel.send(utf8ToBytes("non-ephemeral message"));
|
||||||
|
|
||||||
|
// should be waiting a full second before sending a message after Alice
|
||||||
|
await delay(900);
|
||||||
|
|
||||||
|
// Now, let's wait to send the automated sync message
|
||||||
|
await delay(200);
|
||||||
|
expect(syncMessageSent).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Own sync message does not acknowledge own messages", async () => {
|
||||||
|
const syncMinIntervalMs = 100;
|
||||||
|
const reliableChannel = await ReliableChannel.create(
|
||||||
|
mockWakuNode,
|
||||||
|
"MyChannel",
|
||||||
|
"alice",
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
{
|
||||||
|
syncMinIntervalMs
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const msg = utf8ToBytes("some message");
|
||||||
|
const msgId = ReliableChannel.getMessageId(msg);
|
||||||
|
|
||||||
|
let messageAcknowledged = false;
|
||||||
|
reliableChannel.messageChannel.addEventListener(
|
||||||
|
MessageChannelEvent.OutMessageAcknowledged,
|
||||||
|
(event) => {
|
||||||
|
if (event.detail === msgId) messageAcknowledged = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
reliableChannel.send(msg);
|
||||||
|
|
||||||
|
await delay(syncMinIntervalMs * 2);
|
||||||
|
|
||||||
|
// There is randomness to this, but it should not be excessive
|
||||||
|
expect(messageAcknowledged).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
48
packages/sdk/src/reliable_channel/retry_manager.spec.ts
Normal file
48
packages/sdk/src/reliable_channel/retry_manager.spec.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { delay } from "@waku/utils";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { RetryManager } from "./retry_manager.js";
|
||||||
|
|
||||||
|
describe("Retry Manager", () => {
|
||||||
|
it("Retries within given interval", async function () {
|
||||||
|
const retryManager = new RetryManager(100, 1);
|
||||||
|
|
||||||
|
let retryCount = 0;
|
||||||
|
retryManager.startRetries("1", () => {
|
||||||
|
retryCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(110);
|
||||||
|
|
||||||
|
expect(retryCount).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Retries within maximum given attempts", async function () {
|
||||||
|
const maxAttempts = 5;
|
||||||
|
const retryManager = new RetryManager(10, maxAttempts);
|
||||||
|
|
||||||
|
let retryCount = 0;
|
||||||
|
retryManager.startRetries("1", () => {
|
||||||
|
retryCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(200);
|
||||||
|
|
||||||
|
expect(retryCount).to.equal(maxAttempts);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wait given interval before re-trying", async function () {
|
||||||
|
const retryManager = new RetryManager(100, 1);
|
||||||
|
|
||||||
|
let retryCount = 0;
|
||||||
|
retryManager.startRetries("1", () => {
|
||||||
|
retryCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(90);
|
||||||
|
expect(retryCount).to.equal(0);
|
||||||
|
|
||||||
|
await delay(110);
|
||||||
|
expect(retryCount).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
51
packages/sdk/src/reliable_channel/retry_manager.ts
Normal file
51
packages/sdk/src/reliable_channel/retry_manager.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
export class RetryManager {
|
||||||
|
private timeouts: Map<string, ReturnType<typeof setTimeout>>;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
// TODO: back-off strategy
|
||||||
|
private retryIntervalMs: number,
|
||||||
|
private maxRetryNumber: number
|
||||||
|
) {
|
||||||
|
this.timeouts = new Map();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!retryIntervalMs ||
|
||||||
|
retryIntervalMs <= 0 ||
|
||||||
|
!maxRetryNumber ||
|
||||||
|
maxRetryNumber <= 0
|
||||||
|
) {
|
||||||
|
throw Error(
|
||||||
|
`Invalid retryIntervalMs ${retryIntervalMs} or maxRetryNumber ${maxRetryNumber} values`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopRetries(id: string): void {
|
||||||
|
const timeout = this.timeouts.get(id);
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public startRetries(id: string, retry: () => void | Promise<void>): void {
|
||||||
|
this.retry(id, retry, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private retry(
|
||||||
|
id: string,
|
||||||
|
retry: () => void | Promise<void>,
|
||||||
|
attemptNumber: number
|
||||||
|
): void {
|
||||||
|
clearTimeout(this.timeouts.get(id));
|
||||||
|
if (attemptNumber < this.maxRetryNumber) {
|
||||||
|
const interval = setTimeout(() => {
|
||||||
|
void retry();
|
||||||
|
|
||||||
|
// Register for next retry until we are told to stop;
|
||||||
|
this.retry(id, retry, ++attemptNumber);
|
||||||
|
}, this.retryIntervalMs);
|
||||||
|
|
||||||
|
this.timeouts.set(id, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,10 @@
|
|||||||
import type { Connection, Peer, PeerStore } from "@libp2p/interface";
|
import type { Connection, Peer, PeerStore } from "@libp2p/interface";
|
||||||
import { FilterCodecs, LightPushCodec, StoreCodec } from "@waku/core";
|
import {
|
||||||
|
FilterCodecs,
|
||||||
|
LightPushCodec,
|
||||||
|
LightPushCodecV2,
|
||||||
|
StoreCodec
|
||||||
|
} from "@waku/core";
|
||||||
import { IRelay, Protocols } from "@waku/interfaces";
|
import { IRelay, Protocols } from "@waku/interfaces";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import sinon from "sinon";
|
import sinon from "sinon";
|
||||||
@ -114,7 +119,10 @@ describe("waitForRemotePeer", () => {
|
|||||||
err = e as Error;
|
err = e as Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(addEventListenerSpy.calledOnceWith("peer:identify")).to.be.true;
|
expect(addEventListenerSpy.calledTwice).to.be.true;
|
||||||
|
addEventListenerSpy
|
||||||
|
.getCalls()
|
||||||
|
.forEach((c) => expect(c.firstArg).to.equal("peer:identify"));
|
||||||
|
|
||||||
expect(err).not.to.be.undefined;
|
expect(err).not.to.be.undefined;
|
||||||
expect(err!.message).to.be.eq("Timed out waiting for a remote peer.");
|
expect(err!.message).to.be.eq("Timed out waiting for a remote peer.");
|
||||||
@ -148,9 +156,12 @@ describe("waitForRemotePeer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should wait for LightPush peer to be connected", async () => {
|
it("should wait for LightPush peer to be connected", async () => {
|
||||||
|
let call = 0;
|
||||||
const addEventListenerSpy = sinon.spy(
|
const addEventListenerSpy = sinon.spy(
|
||||||
(_type: string, _cb: (e: any) => void) => {
|
(_type: string, _cb: (e: any) => void) => {
|
||||||
_cb({ detail: { protocols: [LightPushCodec] } });
|
const proto = call === 0 ? LightPushCodec : LightPushCodecV2;
|
||||||
|
call++;
|
||||||
|
_cb({ detail: { protocols: [proto] } });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
eventTarget.addEventListener = addEventListenerSpy;
|
eventTarget.addEventListener = addEventListenerSpy;
|
||||||
@ -174,7 +185,10 @@ describe("waitForRemotePeer", () => {
|
|||||||
err = e as Error;
|
err = e as Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(addEventListenerSpy.calledOnceWith("peer:identify")).to.be.true;
|
expect(addEventListenerSpy.calledTwice).to.be.true;
|
||||||
|
addEventListenerSpy
|
||||||
|
.getCalls()
|
||||||
|
.forEach((c) => expect(c.firstArg).to.equal("peer:identify"));
|
||||||
expect(err).to.be.undefined;
|
expect(err).to.be.undefined;
|
||||||
|
|
||||||
// check with metadata serivice
|
// check with metadata serivice
|
||||||
@ -196,8 +210,10 @@ describe("waitForRemotePeer", () => {
|
|||||||
err = e as Error;
|
err = e as Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(addEventListenerSpy.calledTwice).to.be.true;
|
expect(addEventListenerSpy.callCount).to.equal(4);
|
||||||
expect(addEventListenerSpy.lastCall.calledWith("peer:identify")).to.be.true;
|
addEventListenerSpy
|
||||||
|
.getCalls()
|
||||||
|
.forEach((c) => expect(c.firstArg).to.equal("peer:identify"));
|
||||||
expect(err).to.be.undefined;
|
expect(err).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import type { IdentifyResult } from "@libp2p/interface";
|
import type { IdentifyResult } from "@libp2p/interface";
|
||||||
import { FilterCodecs, LightPushCodec, StoreCodec } from "@waku/core";
|
import {
|
||||||
|
FilterCodecs,
|
||||||
|
LightPushCodec,
|
||||||
|
LightPushCodecV2,
|
||||||
|
StoreCodec
|
||||||
|
} from "@waku/core";
|
||||||
import type { IWaku, Libp2p } from "@waku/interfaces";
|
import type { IWaku, Libp2p } from "@waku/interfaces";
|
||||||
import { Protocols } from "@waku/interfaces";
|
import { Protocols } from "@waku/interfaces";
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
@ -82,6 +87,13 @@ export async function waitForRemotePeer(
|
|||||||
|
|
||||||
type EventListener = (_: CustomEvent<IdentifyResult>) => void;
|
type EventListener = (_: CustomEvent<IdentifyResult>) => void;
|
||||||
|
|
||||||
|
function protocolToPeerPromise(
|
||||||
|
codecs: string[],
|
||||||
|
libp2p: Libp2p
|
||||||
|
): Promise<void>[] {
|
||||||
|
return codecs.map((codec) => waitForConnectedPeer(codec, libp2p));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for required peers to be connected.
|
* Waits for required peers to be connected.
|
||||||
*/
|
*/
|
||||||
@ -96,15 +108,21 @@ async function waitForProtocols(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (waku.store && protocols.includes(Protocols.Store)) {
|
if (waku.store && protocols.includes(Protocols.Store)) {
|
||||||
promises.push(waitForConnectedPeer(StoreCodec, waku.libp2p));
|
promises.push(...protocolToPeerPromise([StoreCodec], waku.libp2p));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (waku.lightPush && protocols.includes(Protocols.LightPush)) {
|
if (waku.lightPush && protocols.includes(Protocols.LightPush)) {
|
||||||
promises.push(waitForConnectedPeer(LightPushCodec, waku.libp2p));
|
const lpPromises = protocolToPeerPromise(
|
||||||
|
[LightPushCodec, LightPushCodecV2],
|
||||||
|
waku.libp2p
|
||||||
|
);
|
||||||
|
promises.push(Promise.any(lpPromises));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (waku.filter && protocols.includes(Protocols.Filter)) {
|
if (waku.filter && protocols.includes(Protocols.Filter)) {
|
||||||
promises.push(waitForConnectedPeer(FilterCodecs.SUBSCRIBE, waku.libp2p));
|
promises.push(
|
||||||
|
...protocolToPeerPromise([FilterCodecs.SUBSCRIBE], waku.libp2p)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
@ -246,15 +264,17 @@ function getEnabledProtocols(waku: IWaku): Protocols[] {
|
|||||||
function mapProtocolsToCodecs(protocols: Protocols[]): Map<string, boolean> {
|
function mapProtocolsToCodecs(protocols: Protocols[]): Map<string, boolean> {
|
||||||
const codecs: Map<string, boolean> = new Map();
|
const codecs: Map<string, boolean> = new Map();
|
||||||
|
|
||||||
const protocolToCodec: Record<string, string> = {
|
const protocolToCodec: Record<string, string[]> = {
|
||||||
[Protocols.Filter]: FilterCodecs.SUBSCRIBE,
|
[Protocols.Filter]: [FilterCodecs.SUBSCRIBE],
|
||||||
[Protocols.LightPush]: LightPushCodec,
|
[Protocols.LightPush]: [LightPushCodec, LightPushCodecV2],
|
||||||
[Protocols.Store]: StoreCodec
|
[Protocols.Store]: [StoreCodec]
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const protocol of protocols) {
|
for (const protocol of protocols) {
|
||||||
if (protocolToCodec[protocol]) {
|
if (protocolToCodec[protocol]) {
|
||||||
codecs.set(protocolToCodec[protocol], false);
|
protocolToCodec[protocol].forEach((codec) => {
|
||||||
|
codecs.set(codec, false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -182,7 +182,7 @@ export class WakuNode implements IWaku {
|
|||||||
}
|
}
|
||||||
if (_protocols.includes(Protocols.LightPush)) {
|
if (_protocols.includes(Protocols.LightPush)) {
|
||||||
if (this.lightPush) {
|
if (this.lightPush) {
|
||||||
codecs.push(this.lightPush.multicodec);
|
codecs.push(...this.lightPush.multicodec);
|
||||||
} else {
|
} else {
|
||||||
log.error(
|
log.error(
|
||||||
"Light Push codec not included in dial codec: protocol not mounted locally"
|
"Light Push codec not included in dial codec: protocol not mounted locally"
|
||||||
@ -216,6 +216,7 @@ export class WakuNode implements IWaku {
|
|||||||
this._nodeStateLock = true;
|
this._nodeStateLock = true;
|
||||||
|
|
||||||
await this.libp2p.start();
|
await this.libp2p.start();
|
||||||
|
await this.filter?.start();
|
||||||
this.connectionManager.start();
|
this.connectionManager.start();
|
||||||
this.peerManager.start();
|
this.peerManager.start();
|
||||||
this.healthIndicator.start();
|
this.healthIndicator.start();
|
||||||
@ -231,6 +232,7 @@ export class WakuNode implements IWaku {
|
|||||||
this._nodeStateLock = true;
|
this._nodeStateLock = true;
|
||||||
|
|
||||||
this.lightPush?.stop();
|
this.lightPush?.stop();
|
||||||
|
await this.filter?.stop();
|
||||||
this.healthIndicator.stop();
|
this.healthIndicator.stop();
|
||||||
this.peerManager.stop();
|
this.peerManager.stop();
|
||||||
this.connectionManager.stop();
|
this.connectionManager.stop();
|
||||||
|
|||||||
@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.0.7](https://github.com/waku-org/js-waku/compare/sds-v0.0.6...sds-v0.0.7) (2025-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Implement peer-store re-bootstrapping ([#2641](https://github.com/waku-org/js-waku/issues/2641)) ([11d84ad](https://github.com/waku-org/js-waku/commit/11d84ad342fe45158ef0734f9ca070f14704503f))
|
||||||
|
* Introduce reliable channels ([#2526](https://github.com/waku-org/js-waku/issues/2526)) ([4d5c152](https://github.com/waku-org/js-waku/commit/4d5c152f5b1b1c241bbe7bb96d13d927a6f7550e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* (sds) ensure incoming messages have their retrieval hint stored ([#2604](https://github.com/waku-org/js-waku/issues/2604)) ([914beb6](https://github.com/waku-org/js-waku/commit/914beb6531a84f8c11ca951721225d47f9e6c285))
|
||||||
|
* Make health events emission consistent ([#2570](https://github.com/waku-org/js-waku/issues/2570)) ([c8dfdb1](https://github.com/waku-org/js-waku/commit/c8dfdb1ace8f0f8f668d8f2bb6e0eaed90041782))
|
||||||
|
* **sds:** Initialize lamport timestamp with current time ([#2610](https://github.com/waku-org/js-waku/issues/2610)) ([cb3af8c](https://github.com/waku-org/js-waku/commit/cb3af8cd4d820e20de1e342d40dbf85bea75e16d))
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* The following workspace dependencies were updated
|
||||||
|
* dependencies
|
||||||
|
* @waku/proto bumped from ^0.0.13 to ^0.0.14
|
||||||
|
* @waku/utils bumped from ^0.0.26 to ^0.0.27
|
||||||
|
|
||||||
## [0.0.6](https://github.com/waku-org/js-waku/compare/sds-v0.0.5...sds-v0.0.6) (2025-08-14)
|
## [0.0.6](https://github.com/waku-org/js-waku/compare/sds-v0.0.5...sds-v0.0.6) (2025-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user