Merge remote-tracking branch 'origin/master' into feat/sds-repair

This commit is contained in:
jm-clius 2025-10-02 18:30:46 +01:00
commit 942e040def
No known key found for this signature in database
GPG Key ID: 5FCD9D5211B952DA
115 changed files with 5455 additions and 2751 deletions

View File

@ -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",
"Привет", "Привет",

View File

@ -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
View File

@ -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"

View File

@ -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",

View File

@ -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);

View File

@ -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)

View File

@ -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",

View File

@ -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";

View File

@ -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");

View File

@ -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);
}
} }

View File

@ -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()

View File

@ -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);
};

View File

@ -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
} }
}; };

View 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;

View File

@ -1 +1,2 @@
export { LightPushCore, LightPushCodec, PushResponse } from "./light_push.js"; export { LightPushCore } from "./light_push.js";
export { LightPushCodec, LightPushCodecV2 } from "./constants.js";

View File

@ -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 };
} }
} }

View 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 });
}
}

View File

@ -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 {

View 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;
}
}

View File

@ -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}`);

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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",

View File

@ -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.

View File

@ -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"
}; };

View File

@ -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"
}

View File

@ -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>;
} }

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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",

View File

@ -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: {

View File

@ -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;
} }

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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
} }
] ]
}; };

View File

@ -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)

View File

@ -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",

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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);
};

View File

@ -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;

View File

@ -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";

View File

@ -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))
); );

View File

@ -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
);
});
});
});

View File

@ -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);
});
}
}

View File

@ -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"
};

View File

@ -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
);
}

View File

@ -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]);
});
});
});

View File

@ -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);
}
} }

View File

@ -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 {

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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>;
} }

View File

@ -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 || {};

View File

@ -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);

View File

@ -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]));
});
});

View File

@ -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);
}
}

View File

@ -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;
}
} }

View 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]);
});
});
});

View File

@ -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()
);
}
} }

View File

@ -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)

View File

@ -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"
] ]
} }

View File

@ -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();

View File

@ -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> {

View File

@ -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";

View File

@ -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"
};
} }

View File

@ -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,

View File

@ -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);

View File

@ -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);
} }

View File

@ -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;

View File

@ -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,

View File

@ -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];
} }
} }

View File

@ -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", () => {

View File

@ -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

View 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;
}>;
}

View File

@ -0,0 +1,2 @@
export { ReliableChannel, ReliableChannelOptions } from "./reliable_channel.js";
export { ReliableChannelEvents, ReliableChannelEvent } from "./events.js";

View File

@ -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);
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
);
}
}
}

View 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;
});
});

View File

@ -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));
});
});

View 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;
});
});

View 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);
});
});

View 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);
}
}
}

View File

@ -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;
}); });

View File

@ -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);
});
} }
} }

View File

@ -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();

View File

@ -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