Skip to content

Commit 49f26d8

Browse files
authored
Merge pull request #2397 from waku-org/fix/store-hash-query
fix(store): update store query validation logic to support msg hash q…
2 parents 1905558 + f649f59 commit 49f26d8

File tree

9 files changed

+282
-47
lines changed

9 files changed

+282
-47
lines changed

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/lib/message_hash/message_hash.spec.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IDecodedMessage, IProtoMessage } from "@waku/interfaces";
1+
import type { IProtoMessage } from "@waku/interfaces";
22
import { bytesToHex, hexToBytes } from "@waku/utils/bytes";
33
import { expect } from "chai";
44

@@ -93,19 +93,20 @@ describe("Message Hash: RFC Test Vectors", () => {
9393
expect(bytesToHex(hash)).to.equal(expectedHash);
9494
});
9595

96-
it("Waku message hash computation (message is IDecodedMessage)", () => {
96+
it("Waku message hash computation (message is IProtoMessage with version)", () => {
9797
const expectedHash =
9898
"3f11bc950dce0e3ffdcf205ae6414c01130bb5d9f20644869bff80407fa52c8f";
9999
const pubsubTopic = "/waku/2/default-waku/proto";
100-
const message: IDecodedMessage = {
101-
version: 0,
100+
const message: IProtoMessage = {
102101
payload: new Uint8Array(),
103-
pubsubTopic,
104102
contentTopic: "/waku/2/default-content/proto",
105103
meta: hexToBytes("0x73757065722d736563726574"),
106-
timestamp: new Date("2024-04-30T10:54:14.978Z"),
104+
timestamp:
105+
BigInt(new Date("2024-04-30T10:54:14.978Z").getTime()) *
106+
BigInt(1000000),
107107
ephemeral: undefined,
108-
rateLimitProof: undefined
108+
rateLimitProof: undefined,
109+
version: 0
109110
};
110111
const hash = messageHash(pubsubTopic, message);
111112
expect(bytesToHex(hash)).to.equal(expectedHash);
@@ -144,16 +145,17 @@ describe("messageHash and messageHashStr", () => {
144145
expect(hashStr).to.equal(hashStrFromBytes);
145146
});
146147

147-
it("messageHashStr works with IDecodedMessage", () => {
148-
const decodedMessage: IDecodedMessage = {
149-
version: 0,
148+
it("messageHashStr works with IProtoMessage", () => {
149+
const decodedMessage: IProtoMessage = {
150150
payload: new Uint8Array([1, 2, 3, 4]),
151-
pubsubTopic,
152151
contentTopic: "/waku/2/default-content/proto",
153152
meta: new Uint8Array([5, 6, 7, 8]),
154-
timestamp: new Date("2024-04-30T10:54:14.978Z"),
153+
timestamp:
154+
BigInt(new Date("2024-04-30T10:54:14.978Z").getTime()) *
155+
BigInt(1000000),
155156
ephemeral: undefined,
156-
rateLimitProof: undefined
157+
rateLimitProof: undefined,
158+
version: 0
157159
};
158160

159161
const hashStr = messageHashStr(pubsubTopic, decodedMessage);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { expect } from "chai";
2+
3+
import { StoreQueryRequest } from "./rpc.js";
4+
5+
describe("StoreQueryRequest validation", () => {
6+
it("accepts valid content-filtered query", () => {
7+
const request = StoreQueryRequest.create({
8+
pubsubTopic: "/waku/2/default-waku/proto",
9+
contentTopics: ["/test/1/content/proto"],
10+
includeData: true,
11+
paginationForward: true
12+
});
13+
expect(request).to.exist;
14+
});
15+
16+
it("rejects content-filtered query with only pubsubTopic", () => {
17+
expect(() =>
18+
StoreQueryRequest.create({
19+
pubsubTopic: "/waku/2/default-waku/proto",
20+
contentTopics: [],
21+
includeData: true,
22+
paginationForward: true
23+
})
24+
).to.throw(
25+
"Both pubsubTopic and contentTopics must be set together for content-filtered queries"
26+
);
27+
});
28+
29+
it("rejects content-filtered query with only contentTopics", () => {
30+
expect(() =>
31+
StoreQueryRequest.create({
32+
pubsubTopic: "",
33+
contentTopics: ["/test/1/content/proto"],
34+
includeData: true,
35+
paginationForward: true
36+
})
37+
).to.throw(
38+
"Both pubsubTopic and contentTopics must be set together for content-filtered queries"
39+
);
40+
});
41+
42+
it("accepts valid message hash query", () => {
43+
const request = StoreQueryRequest.create({
44+
pubsubTopic: "",
45+
contentTopics: [],
46+
messageHashes: [new Uint8Array([1, 2, 3, 4])],
47+
includeData: true,
48+
paginationForward: true
49+
});
50+
expect(request).to.exist;
51+
});
52+
53+
it("rejects hash query with content filter parameters", () => {
54+
expect(() =>
55+
StoreQueryRequest.create({
56+
messageHashes: [new Uint8Array([1, 2, 3, 4])],
57+
pubsubTopic: "/waku/2/default-waku/proto",
58+
contentTopics: ["/test/1/content/proto"],
59+
includeData: true,
60+
paginationForward: true
61+
})
62+
).to.throw(
63+
"Message hash lookup queries cannot include content filter criteria"
64+
);
65+
});
66+
67+
it("rejects hash query with time filter", () => {
68+
expect(() =>
69+
StoreQueryRequest.create({
70+
pubsubTopic: "",
71+
contentTopics: [],
72+
messageHashes: [new Uint8Array([1, 2, 3, 4])],
73+
timeStart: new Date(),
74+
includeData: true,
75+
paginationForward: true
76+
})
77+
).to.throw(
78+
"Message hash lookup queries cannot include content filter criteria"
79+
);
80+
});
81+
82+
it("accepts time-filtered query with content filter", () => {
83+
const request = StoreQueryRequest.create({
84+
pubsubTopic: "/waku/2/default-waku/proto",
85+
contentTopics: ["/test/1/content/proto"],
86+
timeStart: new Date(Date.now() - 3600000),
87+
timeEnd: new Date(),
88+
includeData: true,
89+
paginationForward: true
90+
});
91+
expect(request).to.exist;
92+
});
93+
});

packages/core/src/lib/store/rpc.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class StoreQueryRequest {
1414
public static create(params: QueryRequestParams): StoreQueryRequest {
1515
const request = new StoreQueryRequest({
1616
...params,
17+
contentTopics: params.contentTopics || [],
1718
requestId: uuid(),
1819
timeStart: params.timeStart
1920
? BigInt(params.timeStart.getTime() * ONE_MILLION)
@@ -27,26 +28,29 @@ export class StoreQueryRequest {
2728
: undefined
2829
});
2930

30-
// Validate request parameters based on RFC
31-
if (
32-
(params.pubsubTopic && !params.contentTopics) ||
33-
(!params.pubsubTopic && params.contentTopics)
34-
) {
35-
throw new Error(
36-
"Both pubsubTopic and contentTopics must be set or unset"
37-
);
38-
}
31+
const isHashQuery = params.messageHashes && params.messageHashes.length > 0;
32+
const hasContentTopics =
33+
params.contentTopics && params.contentTopics.length > 0;
34+
const hasTimeFilter = params.timeStart || params.timeEnd;
3935

40-
if (
41-
params.messageHashes &&
42-
(params.pubsubTopic ||
43-
params.contentTopics ||
44-
params.timeStart ||
45-
params.timeEnd)
46-
) {
47-
throw new Error(
48-
"Message hash lookup queries cannot include content filter criteria"
49-
);
36+
if (isHashQuery) {
37+
if (hasContentTopics || hasTimeFilter) {
38+
throw new Error(
39+
"Message hash lookup queries cannot include content filter criteria (contentTopics, timeStart, or timeEnd)"
40+
);
41+
}
42+
} else {
43+
if (
44+
(params.pubsubTopic &&
45+
(!params.contentTopics || params.contentTopics.length === 0)) ||
46+
(!params.pubsubTopic &&
47+
params.contentTopics &&
48+
params.contentTopics.length > 0)
49+
) {
50+
throw new Error(
51+
"Both pubsubTopic and contentTopics must be set together for content-filtered queries"
52+
);
53+
}
5054
}
5155

5256
return request;

packages/core/src/lib/store/store.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ export class StoreCore extends BaseProtocol implements IStoreCore {
4040
decoders: Map<string, IDecoder<T>>,
4141
peerId: PeerId
4242
): AsyncGenerator<Promise<T | undefined>[]> {
43+
// Only validate decoder content topics for content-filtered queries
44+
const isHashQuery =
45+
queryOpts.messageHashes && queryOpts.messageHashes.length > 0;
4346
if (
47+
!isHashQuery &&
48+
queryOpts.contentTopics &&
4449
queryOpts.contentTopics.toString() !==
45-
Array.from(decoders.keys()).toString()
50+
Array.from(decoders.keys()).toString()
4651
) {
4752
throw new Error(
4853
"Internal error, the decoders should match the query's content topics"
@@ -56,6 +61,13 @@ export class StoreCore extends BaseProtocol implements IStoreCore {
5661
paginationCursor: currentCursor
5762
});
5863

64+
log.info("Sending store query request:", {
65+
hasMessageHashes: !!queryOpts.messageHashes?.length,
66+
messageHashCount: queryOpts.messageHashes?.length,
67+
pubsubTopic: queryOpts.pubsubTopic,
68+
contentTopics: queryOpts.contentTopics
69+
});
70+
5971
let stream;
6072
try {
6173
stream = await this.getStream(peerId);

packages/sdk/src/store/store.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,32 @@ export class Store implements IStore {
5757
decoders: IDecoder<T>[],
5858
options?: Partial<QueryRequestParams>
5959
): AsyncGenerator<Promise<T | undefined>[]> {
60-
const { pubsubTopic, contentTopics, decodersAsMap } =
61-
this.validateDecodersAndPubsubTopic(decoders);
60+
// For message hash queries, don't validate decoders but still need decodersAsMap
61+
const isHashQuery =
62+
options?.messageHashes && options.messageHashes.length > 0;
63+
64+
let pubsubTopic: string;
65+
let contentTopics: string[];
66+
let decodersAsMap: Map<string, IDecoder<T>>;
67+
68+
if (isHashQuery) {
69+
// For hash queries, we still need decoders to decode messages
70+
// but we don't validate pubsubTopic consistency
71+
// Use pubsubTopic from options if provided, otherwise from first decoder
72+
pubsubTopic = options.pubsubTopic || decoders[0]?.pubsubTopic || "";
73+
contentTopics = [];
74+
decodersAsMap = new Map();
75+
decoders.forEach((dec) => {
76+
decodersAsMap.set(dec.contentTopic, dec);
77+
});
78+
} else {
79+
const validated = this.validateDecodersAndPubsubTopic(decoders);
80+
pubsubTopic = validated.pubsubTopic;
81+
contentTopics = validated.contentTopics;
82+
decodersAsMap = validated.decodersAsMap;
83+
}
6284

63-
const queryOpts = {
85+
const queryOpts: QueryRequestParams = {
6486
pubsubTopic,
6587
contentTopics,
6688
includeData: true,

packages/tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@waku/core": "*",
5656
"@waku/enr": "*",
5757
"@waku/interfaces": "*",
58+
"@waku/message-hash": "^0.1.17",
5859
"@waku/utils": "*",
5960
"app-root-path": "^3.1.0",
6061
"chai-as-promised": "^7.1.1",

0 commit comments

Comments
 (0)