Repository: waku-org/nwaku
Pull request #3406: fix: timestamp based validation
Author: login: darshankabariya
Created: 2025-05-13T08:58:49Z | Merged: 2025-05-26T12:26:29Z

This PR updates the message validation logic from using epoch to timestamp.
The reason for this change is that the current epoch duration is too long, which allows spammers to resend the same message multiple time to bypass gossipsub.

Note:
Message validation is only upgraded to use the timestamp; the nullifier still relies on rlnMaxEpochGap. This means that currently,  the node stores logs of every message to monitor each user’s message rate. But once the epoch gap exceeds 1, those message longer requires storing messages. To prevent excessive storage usage, we’re continuously deleting those messages from the logs.

closes #2972 

Linked issue #2972: chore(rln): move from epoch based gap to timestamp based.
### Background
We currently validate that an RLN message epoch is +- a given gap (with respect to the current one)  determined in epochs, [see](https://github.com/waku-org/nwaku/blob/master/waku/waku_rln_relay/rln_relay.nim#L209). This is good since it leaves some margins for clock jitter among nodes, propagation/processing times, etc.

However, when having long epoch sizes (eg several minutes or hours), an old message could be replayed into the future to spam the network. And since the gossipsub window just covers 2 minutes, the network will see this message as new.

To solve this issue, its proposed to:
* Make timestamp mandatory for rln messages
* Include the timestamp in the RLN signal, [here](https://github.com/waku-org/nwaku/blob/master/waku/waku_rln_relay/rln_relay.nim#L292)
* Use this timestamp as the allowed gap. Eg, +-10 seconds.
Linked issue comments: user: login: jm-clius
created_at: 2024-08-14T13:21:20Z
body: Makes sense to me. Thanks for creating this.

Commented elsewhere, but I think we may have to validate that the `timestamp` matches the `epoch` against which the proof was generated too. Since the epoch is still the unit against which a rate limit is measured, an attacker could presumably use very old/future epochs to generate proofs and just ensure that they use current timestamps if they want to spam the network?
author_association: CONTRIBUTOR
user: login: alrevuelta
created_at: 2024-08-19T11:35:56Z
body: Some things to take into account for determining the timestamp diff we allow:
* Propagation time of the message. In a very large network this can get quite high. Eg if a node generates the message + timestamp at t0 and the messages hops 5 times, the receiver will see a diff of 5*prop_time. Ofc this should be < than the gap we allow.
* Gossipsub window size (2min now). The gap can't be higher than that, otherwise we won't be protected by the gossipsub cache window.

cc @s-tikhomirov 
author_association: CONTRIBUTOR
user: login: s-tikhomirov
created_at: 2024-08-20T13:07:49Z
body: > the gossipsub window just covers 2 minutes

Is the gossipsub window you mention the same as `seen_ttl` defined [here](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.0.md)? The value of 2 minutes is mentioned there as a "reasonable default", not a strict requirement. I'd say we should then use a non-default `seen_ttl` value for our use case.

The RLN semantics require RLN Relay nodes to keep track of all messages at least within the current epoch to detect spam. If the gossipsub cache is too short for this purpose, isn't the simplest solution just make it longer, for example, equal to to the epoch length (10 mintes)? Can we do it? What are the downsides?
author_association: CONTRIBUTOR
user: login: alrevuelta
created_at: 2024-08-20T13:34:27Z
body: > The RLN semantics require RLN Relay nodes to keep track of all messages at least within the current epoch to detect spam. If the gossipsub cache is too short for this purpose, isn't the simplest solution just make it longer, for example, equal to to the epoch length (10 mintes)? Can we do it? What are the downsides?

Main downside is that your cache (in memory) will be bigger, leading to a higher memory footprint. And well, if someone deploys a network with an epoch of 1 day, then we need seen_ttl of 1 day? Said footprint ofc depends on the amount of messages being sent over that period. The advantage of that solution is that we don't need to modify the code, just tune a config parameter. One thing that is indeed interesting with this approach is that timestamps don't need to be enforced, which adds some privacy? (eg mitigate timing attacks?)

but imho its not about modifying the seen_ttl (which can be done) but about setting a reasonable window size (eg 2 min) and then enforcing it. does it make sense to relay a message that was generated at t0 in t0+9minutes? not sure that offers something valuable, but it opens a bunch of attack vectors. 
author_association: CONTRIBUTOR
user: login: s-tikhomirov
created_at: 2024-08-21T10:08:27Z
body: > does it make sense to relay a message that was generated at t0 in t0+9minutes?

It depends on the use case. I don't think it's up for the protocol level to decide. I can imagine, for example, some commit-reveal scheme (a closed-bid auction or something similar) that time-stamps bids but reveals them 9 minutes later.

> One thing that is indeed interesting with this approach is that timestamps don't need to be enforced, which adds some privacy? (eg mitigate timing attacks?)

Can we really enforce timestamps though? And what do you mean by "enforce"?

Generally speaking, I think we should define our security assumptions around timestamps. If we assume that _most_ (maybe "most" requires a more precise definition) Relay nodes have their local clocks within, say, 1 second of the real physical time, then we can define the cache size in terms of physical seconds. And even if the attacker tries to manipulate with timestamps, its message will be dropped by _most_ Relay nodes.

An analogy: in Bitcoin, timestamps partially determine block validity: a block timestamp must be higher than the median value of the past 11 blocks. Timestamps thus don't have to follow block progression (it might be that `t_n` > `t_{n+1}`), however, this rule prevents wild timestamp manipulations without adding too many trust assumptions.

We'd have to think carefully then about edge cases. Say, a message is generated at time `t1` and committed to epoch `e(t1)`. A given Relay node receives this message at time `t2>t1` during epoch `e(t2)`. Let `g` be the maximum allowed gap (`seen_ttl`). There are two independent message validity criteria now:
- `t2 - t1 > g`?
- `e(t2) == e(t1)`?

A Relay node's actions can be as follows:

|                  | `abs(t2 - t1) < g`                                                                                                                                                                          | `abs(t2 - t1) > g`                                                                                                | Comment                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `e(t2) < e(t1)`  | Keep in cache until `e(t1)` and then relay.                                                                                                                                                 | Drop the message.                                                                                                 | The message is committed to a future epoch but relayed prematurely. |
| `e(t2) == e(t1)` | Happy path: relayed on time within the right epoch. Relay.                                                                                                                                  | Relayed too early _or_ too late within the right epoch. If early: keep in cache and relay at `t1`. If late: drop. | The message is committed to the same epoch as when it is relayed.   |
| `e(t2) > e(t1)`  | Relayed just a bit too late; perhaps in the last seconds of the correct epoch. Drop. Suggestion for users: don't send in the last seconds, better re-generate the proof for the next epoch. | Relayed too late. Drop.                                                                                           | The message is committed to a past epoch.                           |

All in all, I agree that if we trust honest nodes to be in sync with "real" time anyway, then we can also define timestamp-based validity conditions on messages.
author_association: CONTRIBUTOR
user: login: jm-clius
created_at: 2024-08-21T10:52:51Z
body: > A Relay node's actions can be as follows:

I'm not sure that the complexity here makes sense. Since epoch sizes can vary wildly (think epoch of a day or longer), it doesn't make sense to include epoch comparison to determine if a message has been sent roughly within the approximately real-time environment of Relay. For example, if we cache messages in the same epoch but early timestamp, it will be easy to attack this cache by simply generating millions of messages with early timestamp but valid epoch.

In general, the epoch is useful only as a rate-limiting window, not as real-time check. The `timestamp` on its own can be used to introduce some TTL-like behaviour for messages, necessary to protect against past/future replay injection attacks. We are (and should be) very generous here. Previously we allowed up to 20 second timestamp variance in either direction. On the assumption that Relay nodes and publishers have to function roughly within this (wide) real-time window, it becomes easy to simply drop the message if `abs(t2 - t1) > g` with `g == 20`. 
author_association: CONTRIBUTOR
user: login: alrevuelta
created_at: 2024-08-21T11:23:55Z
body: > Can we really enforce timestamps though? And what do you mean by "enforce"?

Yep. no timestamp = reject message + lower score of the sender. timestamp_diff > eg 20 seconds, reject message and lower score of the sender. since the idea is to have the timestamp to be included in the signal (part of the zk proof input) that should cover it.
author_association: CONTRIBUTOR
user: login: s-tikhomirov
created_at: 2024-08-21T12:23:12Z
body: > it doesn't make sense to include epoch comparison to determine if a message has been sent roughly within the approximately real-time environment of Relay

But it is still required to compare message epoch with the Relay's node current epoch to detect rate limit violatoins, right?

> if we cache messages in the same epoch but early timestamp, it will be easy to attack this cache by simply generating millions of messages with early timestamp but valid epoch

I'm not sure I understand this.

Sorry if I get this discussion side-tracked, but I really want to get to the bottom of this :) From what I understand from the [spec](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/17/rln-relay.md#publishing):

1. `epoch` is derived deterministically from a UNIX timestamp;
2. message validity depends on it being relayed in the "current" epoch;
3. to detect rate limiting violation, a Relay node must keep a cache of messages relayed within the "current" epoch.

A hidden assumption in the RLN spec as it stands now it that it uses "current epoch" as a single term, whereas in reality there is a difference between the "current" epoch as defined by:
- the RLN proof a message is committed to;
- the time when the sender first relays the message; 
- the time when a given Relay node first sees this message.

In summary (please let me know if any of the following is based on a misunderstanding):

1. I agree that an additional timestamp-based validity condition might be helpful to prevent deliberate timestamp manipulation.
2. RLN validity is still defined in terms of epochs, therefore, a Relay node must at least be aware of all messages trom the latest epoch to detect spam attempts;
3. If we add timestamp-based validity conditions, we should address edge cases at epoch boundaries (or accept them as inevitable);
4. Long-term, it'd be great to have our assumptions around clock synchronization defined precisely as it relates to RLN proof validation.
author_association: CONTRIBUTOR
user: login: jm-clius
created_at: 2024-08-22T14:23:55Z
body: > it is still required to compare message epoch with the Relay's node current epoch to detect rate limit violatoins, right?

I don't think so. Afaiu rate limit violations are checked by caching the nullifiers generated within an epoch and ensuring that no double signalling occur. Of course, we should ensure that the caching occurs for all epochs where valid messages (with valid timestamps) could occur, but I think this is done in the implementation already. The point is that the epoch in a message does not have to be the current epoch for the message to be valid.

> message validity depends on it being relayed in the "current" epoch;

I don't understand it this way. The message is generated with the "current" epoch as seen by the publisher. Currently, this does not have to match the current epoch of the validator, as long as there's no more than `max_epoch_gap` between the published and validating epoch. The proposal is to change to a timestamp-based validation. Indeed, in implementation we should ensure that nullifiers are cached for all epochs that could conceivably fall within the valid timestamp window, but the epoch itself is not a validation requirement.

> a Relay node must keep a cache of messages relayed within the "current" epoch.

IIUC a cache of nullifiers within all epochs that could contain messages with valid timestamps

> RLN validity is still defined in terms of epochs

Only the rate-limiting is defined per epoch.

> a Relay node must at least be aware of all messages trom the latest epoch to detect spam attempts

It must be aware of all _valid_ messages from whichever epochs can contain valid messages. Valid message is one with a timestamp within an acceptable window from current time as measured by the validator. You're absolutely right that nullifiers we cache might still cover several epochs to account for boundary conditions, but this is an implementation detail and not related to message validity rules


author_association: CONTRIBUTOR
user: login: jm-clius
created_at: 2025-04-24T10:07:34Z
body: @seemenkina, as far as I remember there was no progress on this issue last year? Just double-checking that there's no branch with feature work that I might be missing. :)

What is necessary to close this issue, in summary:

**Changes to types and RLN spec:**
- `rlnMaxEpochGap` becomes `rlnMaxTimestampGap`
- `timestamp` becomes mandatory for RLN and included in the RLN `signal`

**Changes to RLN validation**
- validate that `epoch` and `timestamp` correspond
- validate that `currentTime - timestamp <= rlnMaxTimestampGap`

cc @darshankabariya @Ivansete-status 
author_association: CONTRIBUTOR
user: login: seemenkina
created_at: 2025-04-30T12:07:29Z
body: @jm-clius, hey, yeah, there was no progress, as we decided that there'll be only changes in nwaku (in thread vac/acz-pm/deprecate tree sync strategy). Would you like the ACZ team to do it? 
author_association: NONE
user: login: darshankabariya
created_at: 2025-04-30T22:56:36Z
body: It seems like an issue on the nwaku side. We’re happy to take it on if you’d like — Ivan actually assigned it to me last week. I’m planning to work on it once my current PR is merged. ( cc @seemenkina @jm-clius @Ivansete-status )
author_association: CONTRIBUTOR
user: login: seemenkina
created_at: 2025-05-05T07:52:22Z
body: @darshankabariya sounds good, thank you! Let me know If you need any support from zerokit side or if changes are still needed
author_association: NONE

Diff:
diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim
index 907b7c1b37..e89d8e2e6f 100644
--- a/tests/waku_rln_relay/test_waku_rln_relay.nim
+++ b/tests/waku_rln_relay/test_waku_rln_relay.nim
@@ -1,7 +1,7 @@
 {.used.}
 
 import
-  std/[options, os, sequtils, times, tempfiles],
+  std/[options, os, sequtils, tempfiles],
   stew/byteutils,
   stew/shims/net as stewNet,
   testutils/unittests,
@@ -17,7 +17,10 @@ import
     waku_rln_relay/protocol_metrics,
     waku_keystore,
   ],
-  ./rln/waku_rln_relay_utils
+  ./rln/waku_rln_relay_utils,
+  ../testlib/[wakucore, futures, wakunode, testutils]
+
+from std/times import epochTime
 
 suite "Waku rln relay":
   test "key_gen Nim Wrappers":
@@ -686,7 +689,7 @@ suite "Waku rln relay":
     # it is a duplicate
     assert isDuplicate3, "duplicate should be found"
 
-  asyncTest "validateMessageAndUpdateLog test":
+  asyncTest "validateMessageAndUpdateLog: against epoch gap":
     let index = MembershipIndex(5)
 
     let wakuRlnConfig = WakuRlnConfig(
@@ -700,27 +703,31 @@ suite "Waku rln relay":
     let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr:
       raiseAssert $error
 
-    # get the current epoch time
-    let time = epochTime()
+    let time_1 = epochTime()
 
-    #  create some messages from the same peer and append rln proof to them, except wm4
     var
-      wm1 = WakuMessage(payload: "Valid message".toBytes())
+      # create some messages from the same peer and append rln proof to them, except wm4
+      wm1 = WakuMessage(payload: "Valid message".toBytes(), timestamp: now())
       # another message in the same epoch as wm1, it will break the messaging rate limit
-      wm2 = WakuMessage(payload: "Spam".toBytes())
-      #  wm3 points to the next epoch
-      wm3 = WakuMessage(payload: "Valid message".toBytes())
-      wm4 = WakuMessage(payload: "Invalid message".toBytes())
+      wm2 = WakuMessage(payload: "Spam message".toBytes(), timestamp: now())
 
-    wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+    await sleepAsync(1.seconds)
+    let time_2 = epochTime()
+
+    var
+      #  wm3 points to the next epoch bcz of the sleep 
+      wm3 = WakuMessage(payload: "Valid message".toBytes(), timestamp: now())
+      wm4 = WakuMessage(payload: "Invalid message".toBytes(), timestamp: now())
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm1, time_1).isOkOr:
       raiseAssert $error
-    wakuRlnRelay.unsafeAppendRLNProof(wm2, time).isOkOr:
+    wakuRlnRelay.unsafeAppendRLNProof(wm2, time_1).isOkOr:
       raiseAssert $error
-    wakuRlnRelay.unsafeAppendRLNProof(wm3, time + float64(wakuRlnRelay.rlnEpochSizeSec)).isOkOr:
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm3, time_2).isOkOr:
       raiseAssert $error
 
     # validate messages
-    # validateMessage proc checks the validity of the message fields and adds it to the log (if valid)
     let
       msgValidate1 = wakuRlnRelay.validateMessageAndUpdateLog(wm1)
       # wm2 is published within the same Epoch as wm1 and should be found as spam
@@ -736,6 +743,48 @@ suite "Waku rln relay":
       msgValidate3 == MessageValidationResult.Valid
       msgValidate4 == MessageValidationResult.Invalid
 
+  asyncTest "validateMessageAndUpdateLog: against timestamp gap":
+    let index = MembershipIndex(5)
+
+    let wakuRlnConfig = WakuRlnConfig(
+      dynamic: false,
+      credIndex: some(index),
+      userMessageLimit: 10,
+      epochSizeSec: 10,
+      treePath: genTempPath("rln_tree", "waku_rln_relay_2"),
+    )
+
+    let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr:
+      raiseAssert $error
+
+    # usually it's 20 seconds but we set it to 2 for testing purposes which make the test faster
+    wakuRlnRelay.rlnMaxTimestampGap = 1
+
+    var time = epochTime()
+
+    var
+      wm1 = WakuMessage(payload: "timestamp message".toBytes(), timestamp: now())
+      wm2 = WakuMessage(payload: "timestamp message".toBytes(), timestamp: now())
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+      raiseAssert $error
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm2, time).isOkOr:
+      raiseAssert $error
+
+    # validate the first message because it's timestamp is the same as the generated timestamp
+    let msgValidate1 = wakuRlnRelay.validateMessageAndUpdateLog(wm1)
+
+    # wait for 2 seconds to make the timestamp different from generated timestamp
+    await sleepAsync(2.seconds)
+
+    # invalidate the second message because it's timestamp is different from the generated timestamp
+    let msgValidate2 = wakuRlnRelay.validateMessageAndUpdateLog(wm2)
+
+    check:
+      msgValidate1 == MessageValidationResult.Valid
+      msgValidate2 == MessageValidationResult.Invalid
+
   asyncTest "validateMessageAndUpdateLog: multiple senders with same external nullifier":
     let index1 = MembershipIndex(5)
     let index2 = MembershipIndex(6)
@@ -766,9 +815,11 @@ suite "Waku rln relay":
 
     #  create messages from different peers and append rln proofs to them
     var
-      wm1 = WakuMessage(payload: "Valid message from sender 1".toBytes())
+      wm1 =
+        WakuMessage(payload: "Valid message from sender 1".toBytes(), timestamp: now())
       # another message in the same epoch as wm1, it will break the messaging rate limit
-      wm2 = WakuMessage(payload: "Valid message from sender 2".toBytes())
+      wm2 =
+        WakuMessage(payload: "Valid message from sender 2".toBytes(), timestamp: now())
 
     wakuRlnRelay1.appendRLNProof(wm1, time).isOkOr:
       raiseAssert $error
diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim
index 3ff6923e08..7620dfc144 100644
--- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim
+++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim
@@ -132,7 +132,8 @@ procSuite "WakuNode - RLN relay":
     let payload = "Hello".toBytes()
 
     # prepare the epoch
-    var message = WakuMessage(payload: @payload, contentTopic: contentTopic)
+    var message =
+      WakuMessage(payload: @payload, contentTopic: contentTopic, timestamp: now())
     doAssert(node1.wakuRlnRelay.unsafeAppendRLNProof(message, epochTime()).isOk())
 
     debug "Nodes participating in the test",
@@ -221,19 +222,25 @@ procSuite "WakuNode - RLN relay":
     var messages1: seq[WakuMessage] = @[]
     var messages2: seq[WakuMessage] = @[]
 
-    let epochTime = epochTime()
+    var epochTime = epochTime()
 
     for i in 0 ..< 3:
       var message = WakuMessage(
-        payload: ("Payload_" & $i).toBytes(), contentTopic: contentTopics[0]
+        payload: ("Payload_" & $i).toBytes(),
+        timestamp: now(),
+        contentTopic: contentTopics[0],
       )
       nodes[0].wakuRlnRelay.unsafeAppendRLNProof(message, epochTime).isOkOr:
         raiseAssert $error
       messages1.add(message)
 
+    epochTime = epochTime()
+
     for i in 0 ..< 3:
       var message = WakuMessage(
-        payload: ("Payload_" & $i).toBytes(), contentTopic: contentTopics[1]
+        payload: ("Payload_" & $i).toBytes(),
+        timestamp: now(),
+        contentTopic: contentTopics[1],
       )
       nodes[1].wakuRlnRelay.unsafeAppendRLNProof(message, epochTime).isOkOr:
         raiseAssert $error
@@ -364,8 +371,12 @@ procSuite "WakuNode - RLN relay":
       # check the proof is generated correctly outside when block to avoid duplication
     let rateLimitProof = rateLimitProofRes.get().encode().buffer
 
-    let message =
-      WakuMessage(payload: @payload, contentTopic: contentTopic, proof: rateLimitProof)
+    let message = WakuMessage(
+      payload: @payload,
+      contentTopic: contentTopic,
+      proof: rateLimitProof,
+      timestamp: now(),
+    )
 
     ## node1 publishes a message with an invalid rln proof, the message is then relayed to node2 which in turn
     ## attempts to verify the rate limit proof and fails hence does not relay the message to node3, thus the relayHandler of node3
@@ -452,24 +463,36 @@ procSuite "WakuNode - RLN relay":
     await node3.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()])
 
     # get the current epoch time
-    let time = epochTime()
+    let time_1 = epochTime()
+
     #  create some messages with rate limit proofs
     var
-      wm1 = WakuMessage(payload: "message 1".toBytes(), contentTopic: contentTopic)
+      wm1 = WakuMessage(
+        payload: "message 1".toBytes(), timestamp: now(), contentTopic: contentTopic
+      )
       # another message in the same epoch as wm1, it will break the messaging rate limit
-      wm2 = WakuMessage(payload: "message 2".toBytes(), contentTopic: contentTopic)
+      wm2 = WakuMessage(
+        payload: "message 2".toBytes(), timestamp: now(), contentTopic: contentTopic
+      )
       #  wm3 points to the next epoch
-      wm3 = WakuMessage(payload: "message 3".toBytes(), contentTopic: contentTopic)
-      wm4 = WakuMessage(payload: "message 4".toBytes(), contentTopic: contentTopic)
 
-    node3.wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+    await sleepAsync(1000.millis)
+    let time_2 = epochTime()
+
+    var
+      wm3 = WakuMessage(
+        payload: "message 3".toBytes(), timestamp: now(), contentTopic: contentTopic
+      )
+      wm4 = WakuMessage(
+        payload: "message 4".toBytes(), timestamp: now(), contentTopic: contentTopic
+      )
+
+    node3.wakuRlnRelay.unsafeAppendRLNProof(wm1, time_1).isOkOr:
       raiseAssert $error
-    node3.wakuRlnRelay.unsafeAppendRLNProof(wm2, time).isOkOr:
+    node3.wakuRlnRelay.unsafeAppendRLNProof(wm2, time_1).isOkOr:
       raiseAssert $error
 
-    node3.wakuRlnRelay.unsafeAppendRLNProof(
-      wm3, time + float64(node3.wakuRlnRelay.rlnEpochSizeSec)
-    ).isOkOr:
+    node3.wakuRlnRelay.unsafeAppendRLNProof(wm3, time_2).isOkOr:
       raiseAssert $error
 
     #  relay handler for node3
@@ -700,8 +723,12 @@ procSuite "WakuNode - RLN relay":
     # Given some messages with rln proofs
     let time = epochTime()
     var
-      msg1 = WakuMessage(payload: "message 1".toBytes(), contentTopic: contentTopic)
-      msg2 = WakuMessage(payload: "message 2".toBytes(), contentTopic: contentTopic)
+      msg1 = WakuMessage(
+        payload: "message 1".toBytes(), timestamp: now(), contentTopic: contentTopic
+      )
+      msg2 = WakuMessage(
+        payload: "message 2".toBytes(), timestamp: now(), contentTopic: contentTopic
+      )
 
     node1.wakuRlnRelay.unsafeAppendRLNProof(msg1, time).isOkOr:
       raiseAssert $error
diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim
index 719e66b8a5..8bca121e31 100644
--- a/tests/wakunode_rest/test_rest_relay.nim
+++ b/tests/wakunode_rest/test_rest_relay.nim
@@ -260,7 +260,7 @@ suite "Waku v2 Rest API - Relay":
       RelayWakuMessage(
         payload: base64.encode("TEST-PAYLOAD"),
         contentTopic: some(DefaultContentTopic),
-        timestamp: some(int64(2022)),
+        timestamp: some(now()),
       ),
     )
 
@@ -488,7 +488,7 @@ suite "Waku v2 Rest API - Relay":
       RelayWakuMessage(
         payload: base64.encode("TEST-PAYLOAD"),
         contentTopic: some(DefaultContentTopic),
-        timestamp: some(int64(2022)),
+        timestamp: some(now()),
       )
     )
 
diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim
index 5dae3bd512..48b3e8d793 100644
--- a/waku/waku_rln_relay/rln_relay.nim
+++ b/waku/waku_rln_relay/rln_relay.nim
@@ -93,6 +93,7 @@ type WakuRLNRelay* = ref object of RootObj
   nullifierLog*: OrderedTable[Epoch, Table[Nullifier, ProofMetadata]]
   lastEpoch*: Epoch # the epoch of the last published rln message
   rlnEpochSizeSec*: uint64
+  rlnMaxTimestampGap*: uint64
   rlnMaxEpochGap*: uint64
   groupManager*: GroupManager
   onFatalErrorAction*: OnFatalErrorHandler
@@ -103,6 +104,7 @@ type WakuRLNRelay* = ref object of RootObj
 proc calcEpoch*(rlnPeer: WakuRLNRelay, t: float64): Epoch =
   ## gets time `t` as `flaot64` with subseconds resolution in the fractional part
   ## and returns its corresponding rln `Epoch` value
+
   let e = uint64(t / rlnPeer.rlnEpochSizeSec.float64)
   return toEpoch(e)
 
@@ -211,25 +213,26 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = getTime().toUnixFloat()
+  let messageTime = msg.timestamp.float64 / 1e9
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  let timeDiff = uint64(abs(currentTime - messageTime))
+
+  debug "time info",
+    currentTime = currentTime, messageTime = messageTime, msgHash = msg.hash
+
+  if timeDiff > rlnPeer.rlnMaxTimestampGap:
+    warn "invalid message: timestamp difference exceeds threshold",
+      timeDiff = timeDiff, maxTimestampGap = rlnPeer.rlnMaxTimestampGap
+    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_timestamp"])
+    return MessageValidationResult.Invalid
+
+  let computedEpoch = rlnPeer.calcEpoch(messageTime)
+  if proof.epoch != computedEpoch:
+    warn "invalid message: timestamp mismatches epoch",
+      proofEpoch = fromEpoch(proof.epoch), computedEpoch = fromEpoch(computedEpoch)
+    waku_rln_invalid_messages_total.inc(labelValues = ["timestamp_mismatch"])
     return MessageValidationResult.Invalid
 
   let rootValidationRes = rlnPeer.groupManager.validateRoot(proof.merkleRoot)
@@ -242,8 +245,9 @@ proc validateMessage*(
 
   # verify the proof
   let
-    contentTopicBytes = msg.contentTopic.toBytes
-    input = concat(msg.payload, contentTopicBytes)
+    contentTopicBytes = toBytes(msg.contentTopic)
+    timestampBytes = toBytes(msg.timestamp.uint64)
+    input = concat(msg.payload, contentTopicBytes, @(timestampBytes))
 
   waku_rln_proof_verification_total.inc()
   waku_rln_proof_verification_duration_seconds.nanosecondTime:
@@ -265,6 +269,8 @@ proc validateMessage*(
   if proofMetadataRes.isErr():
     waku_rln_errors_total.inc(labelValues = ["proof_metadata_extraction"])
     return MessageValidationResult.Invalid
+
+  let msgEpoch = proof.epoch
   let hasDup = rlnPeer.hasDuplicate(msgEpoch, proofMetadataRes.get())
   if hasDup.isErr():
     waku_rln_errors_total.inc(labelValues = ["duplicate_check"])
@@ -305,10 +311,12 @@ proc validateMessageAndUpdateLog*(
 
 proc toRLNSignal*(wakumessage: WakuMessage): seq[byte] =
   ## it is a utility proc that prepares the `data` parameter of the proof generation procedure i.e., `proofGen`  that resides in the current module
-  ## it extracts the `contentTopic` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence
+  ## it extracts the `contentTopic`, `timestamp` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence
+
   let
-    contentTopicBytes = wakumessage.contentTopic.toBytes()
-    output = concat(wakumessage.payload, contentTopicBytes)
+    contentTopicBytes = toBytes(wakumessage.contentTopic)
+    timestampBytes = toBytes(wakumessage.timestamp.uint64)
+    output = concat(wakumessage.payload, contentTopicBytes, @(timestampBytes))
   return output
 
 proc appendRLNProof*(
@@ -479,6 +487,7 @@ proc mount(
     nonceManager: NonceManager.init(conf.userMessageLimit, conf.epochSizeSec.float),
     rlnEpochSizeSec: conf.epochSizeSec,
     rlnMaxEpochGap: max(uint64(MaxClockGapSeconds / float64(conf.epochSizeSec)), 1),
+    rlnMaxTimestampGap: uint64(MaxClockGapSeconds),
     onFatalErrorAction: conf.onFatalErrorAction,
   )
 


Reviews:
user: login: jm-clius
body: Thanks!

At this stage, the verification can be easily fooled by just always adding a fake current `timestamp` to messages while actually computing the RLN proof against any number of very old epochs.
In order to use `timestamp` base validation, we need to verify that the `proof` was indeed generated against the `timestamp` included in the message.
In other words:
1. on prover side: `timestamp` should be included in the RLN signal when proofs are generated here: https://github.com/waku-org/nwaku/blob/3464d81a6f83268654242162b348777e028347eb/waku/waku_rln_relay/rln_relay.nim#L311
2. on verifier side: we should verify that the received message `timestamp` was indeed the one used to generate the received proof, by including it in the signal for verification here: https://github.com/waku-org/nwaku/blob/3464d81a6f83268654242162b348777e028347eb/waku/waku_rln_relay/rln_relay.nim#L246

At this stage we can trust that the `timestamp` is the one actually used when generating the proof and not just a fake one that makes the message appear "current". However, we still don't know if the _nullifier_ was generated with any old, expired epoch. In other words, we also have to verify that the received `timestamp` belongs to the received `epoch`. This is a simple check which we can do for example here: https://github.com/waku-org/nwaku/blob/3464d81a6f83268654242162b348777e028347eb/waku/waku_rln_relay/rln_relay.nim#L225 before continuing to verify that the `timestamp` is close to current time.

I hope this makes sense? Let me know if I can clarify anything.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:02:42Z
user: login: stubbsta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T09:01:17Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T16:41:40Z
user: login: jm-clius
body: Almost there! Couple of comments below. LMK if anything is unclear.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:19:53Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T06:21:02Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T06:24:13Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T06:24:31Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T06:24:57Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T06:26:20Z
user: login: stubbsta
body: Thanks Darshan, this looks good. Is there a test to check that a message with an invalid timestamp correctly gets identified and rejected? If not, would it be possible to add one?
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T08:12:53Z
user: login: jm-clius
body: Thanks for this! The minor comments below are non-blocking (i.e. you don't need to request a review again before merging :) ).
Can we create two spin-off issues from this and link to this one?
1. Issue to change the necessary RLN-RELAY and NETWORK specs (in waku-org/specs repo).
2. Issue to deprecate rlnMaxEpochGap as a config item and remove it from the code
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T11:50:16Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T16:58:55Z
user: login: Ivansete-status
body: Thanks for it :raised_hands: ! I'm adding some nitpick comments that I hope you find interesting
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T10:51:49Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T21:05:19Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T21:28:07Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T21:31:03Z
user: login: darshankabariya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T22:24:59Z
user: login: Ivansete-status
body: LGTM! Thanks for it! :100: 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-26T10:46:50Z
user: login: stubbsta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T11:49:09Z
user: login: stubbsta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T11:49:39Z
user: login: stubbsta
body: LGTM. Just some nitpicking about test naming, thanks Darshan!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T11:51:49Z

Review comments:
diff_hunk: @@ -211,25 +213,31 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(currentTime - msg.timestamp)
+
+  debug "time info", currentTime = currentTime, messageTime = msg.timestamp
+
+  if msg.timestamp > currentTime:
+    warn "invalid message: timestamp is in the future", payloadLen = msg.payload.len
path: waku/waku_rln_relay/rln_relay.nim
user: login: stubbsta
body: I'm not sure how informative it is to add the payload length to this warning message. Would adding something like the msg hash not be more useful?
created_at: 2025-05-15T09:01:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,25 +213,31 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(currentTime - msg.timestamp)
+
+  debug "time info", currentTime = currentTime, messageTime = msg.timestamp
+
+  if msg.timestamp > currentTime:
+    warn "invalid message: timestamp is in the future", payloadLen = msg.payload.len
path: waku/waku_rln_relay/rln_relay.nim
user: login: darshankabariya
body: Thanks ! 
created_at: 2025-05-16T16:41:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -306,9 +316,11 @@ proc validateMessageAndUpdateLog*(
 proc toRLNSignal*(wakumessage: WakuMessage): seq[byte] =
   ## it is a utility proc that prepares the `data` parameter of the proof generation procedure i.e., `proofGen`  that resides in the current module
   ## it extracts the `contentTopic` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence
path: waku/waku_rln_relay/rln_relay.nim
user: login: jm-clius
body: A good idea to update the description to make it clear we now include the `timestamp` in the signal. We may also need to update a spec or two to indicate this, specifically this one: https://rfc.vac.dev/waku/standards/core/17/rln-relay#publishing and perhaps the Waku Network spec too (not requirement for this PR, but in a follow-up)
created_at: 2025-05-19T15:12:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,6 +331,9 @@ proc appendRLNProof*(
   ## `senderEpochTime` indicates the number of seconds passed since Unix epoch. The fractional part holds sub-seconds.
   ## The `epoch` field of `RateLimitProof` is derived from the provided `senderEpochTime` (using `calcEpoch()`)
 
+  # need to update the timestamp to the sender's timestamp bcz now we are using the sender's timestamp to generate the proof
+  msg.timestamp = senderEpochTime.int64
path: waku/waku_rln_relay/rln_relay.nim
user: login: jm-clius
body: I am not sure I understand why this is necessary. Shouldn't the `msg.timestamp` here be the sender time in any case? Under what circumstance would `senderEpochTime` be computed on a different timestamp? What does change is that `timestamp` is now a compulsory field and we can return an Error if the message does not include a timestamp.
created_at: 2025-05-19T15:15:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,25 +213,30 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(currentTime - msg.timestamp)
+
+  debug "time info",
+    currentTime = currentTime, messageTime = msg.timestamp, msgHash = msg.hash
+
+  if msg.timestamp > currentTime:
+    warn "invalid message: timestamp is in the future", payloadLen = msg.payload.len
+    waku_rln_invalid_messages_total.inc(labelValues = ["future_timestamp"])
+    return MessageValidationResult.Invalid
+
+  if timeDiff > rlnPeer.rlnMaxTimestampGap:
+    warn "invalid message: timestamp difference exceeds threshold",
+      timeDiff = timeDiff, maxTimestampGap = rlnPeer.rlnMaxTimestampGap
+    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_timestamp"])
+    return MessageValidationResult.Invalid
+
+  let computedEpoch = rlnPeer.calcEpoch(msg.timestamp.float64)
+  if proof.epoch != computedEpoch:
+    warn "invalid message: epoch mismatch",
path: waku/waku_rln_relay/rln_relay.nim
user: login: jm-clius
body: I think `epoch mismatch` would be ambiguous - the message is not merely mismatching our epoch, its `timestamp` is mismatching it's own epoch. So I would rather say something like: 
`invalid message: timestamp mismatches epoch`
created_at: 2025-05-19T15:17:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,25 +213,30 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(currentTime - msg.timestamp)
path: waku/waku_rln_relay/rln_relay.nim
user: login: jm-clius
body: This should be an `abs` difference. Timestamps can be `maxTimestampGap` in the future or in the past. For example, our own clock can be slightly behind, making it appear as if new messages are in the future.
created_at: 2025-05-19T15:18:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,25 +213,30 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(currentTime - msg.timestamp)
+
+  debug "time info",
+    currentTime = currentTime, messageTime = msg.timestamp, msgHash = msg.hash
+
+  if msg.timestamp > currentTime:
path: waku/waku_rln_relay/rln_relay.nim
user: login: jm-clius
body: The only validity requirement is that the `msg.timestamp` should not differ from ours by more than the max allowed gap. This can be both into the future or into the past, to cover normal clock jitter.
created_at: 2025-05-19T15:18:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60058,7 +60058,10 @@ else:
 const StaticGroupMerkleRoot* =
   "2c149e48886b5ba3da2edf8db8d7a364ae7a25618489c04cf0c0380f7cdd4d6f"
 
-const MaxClockGapSeconds* = 20.0 # the maximum clock difference between peers in seconds
+# the maximum clock difference between peers in seconds
+const MaxClockGapSeconds* = 20.0
+# the maximum timestamp difference between the message and the current time
+const MaxTimestampGapSeconds* = 20.0
path: waku/waku_rln_relay/constants.nim
user: login: jm-clius
body: I don't think we need both of these? We only need `MaxTimestampGapSeconds` afaics?
created_at: 2025-05-19T15:19:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -306,9 +316,11 @@ proc validateMessageAndUpdateLog*(
 proc toRLNSignal*(wakumessage: WakuMessage): seq[byte] =
   ## it is a utility proc that prepares the `data` parameter of the proof generation procedure i.e., `proofGen`  that resides in the current module
   ## it extracts the `contentTopic` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence
path: waku/waku_rln_relay/rln_relay.nim
user: login: darshankabariya
body: Addressed !!  

created_at: 2025-05-20T06:21:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,6 +331,9 @@ proc appendRLNProof*(
   ## `senderEpochTime` indicates the number of seconds passed since Unix epoch. The fractional part holds sub-seconds.
   ## The `epoch` field of `RateLimitProof` is derived from the provided `senderEpochTime` (using `calcEpoch()`)
 
+  # need to update the timestamp to the sender's timestamp bcz now we are using the sender's timestamp to generate the proof
+  msg.timestamp = senderEpochTime.int64
path: waku/waku_rln_relay/rln_relay.nim
user: login: darshankabariya
body: Thanks ! There are some test cases that initialize WakuMessage and generate a proof at different times. Initially, I handled that logic here, but I now realize that approach was overfitting. Instead of modifying it here, I’ve updated the relevant test cases instead. 
created_at: 2025-05-20T06:24:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,25 +213,30 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(currentTime - msg.timestamp)
+
+  debug "time info",
+    currentTime = currentTime, messageTime = msg.timestamp, msgHash = msg.hash
+
+  if msg.timestamp > currentTime:
+    warn "invalid message: timestamp is in the future", payloadLen = msg.payload.len
+    waku_rln_invalid_messages_total.inc(labelValues = ["future_timestamp"])
+    return MessageValidationResult.Invalid
+
+  if timeDiff > rlnPeer.rlnMaxTimestampGap:
+    warn "invalid message: timestamp difference exceeds threshold",
+      timeDiff = timeDiff, maxTimestampGap = rlnPeer.rlnMaxTimestampGap
+    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_timestamp"])
+    return MessageValidationResult.Invalid
+
+  let computedEpoch = rlnPeer.calcEpoch(msg.timestamp.float64)
+  if proof.epoch != computedEpoch:
+    warn "invalid message: epoch mismatch",
path: waku/waku_rln_relay/rln_relay.nim
user: login: darshankabariya
body: Addressed !!
created_at: 2025-05-20T06:24:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,25 +213,30 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(currentTime - msg.timestamp)
path: waku/waku_rln_relay/rln_relay.nim
user: login: darshankabariya
body: Got it, Addressed !!
created_at: 2025-05-20T06:24:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60058,7 +60058,10 @@ else:
 const StaticGroupMerkleRoot* =
   "2c149e48886b5ba3da2edf8db8d7a364ae7a25618489c04cf0c0380f7cdd4d6f"
 
-const MaxClockGapSeconds* = 20.0 # the maximum clock difference between peers in seconds
+# the maximum clock difference between peers in seconds
+const MaxClockGapSeconds* = 20.0
+# the maximum timestamp difference between the message and the current time
+const MaxTimestampGapSeconds* = 20.0
path: waku/waku_rln_relay/constants.nim
user: login: darshankabariya
body: Thanks for all the comments. I’ve addressed. 
created_at: 2025-05-20T06:26:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,6 +93,7 @@ type WakuRLNRelay* = ref object of RootObj
   nullifierLog*: OrderedTable[Epoch, Table[Nullifier, ProofMetadata]]
   lastEpoch*: Epoch # the epoch of the last published rln message
   rlnEpochSizeSec*: uint64
+  rlnMaxTimestampGap*: uint64
   rlnMaxEpochGap*: uint64
path: waku/waku_rln_relay/rln_relay.nim
user: login: jm-clius
body: Can we remove maxEpochGap? It can be in a next PR or announced as a deprecation, since it affects configuration.
created_at: 2025-05-20T11:44:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,25 +213,25 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  var timeDiff = uint64(abs(currentTime - msg.timestamp))
path: waku/waku_rln_relay/rln_relay.nim
user: login: jm-clius
body: Why is this a `var`and not a `let`?
created_at: 2025-05-20T11:44:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,6 +93,7 @@ type WakuRLNRelay* = ref object of RootObj
   nullifierLog*: OrderedTable[Epoch, Table[Nullifier, ProofMetadata]]
   lastEpoch*: Epoch # the epoch of the last published rln message
   rlnEpochSizeSec*: uint64
+  rlnMaxTimestampGap*: uint64
   rlnMaxEpochGap*: uint64
path: waku/waku_rln_relay/rln_relay.nim
user: login: darshankabariya
body: Hi @jm-clius, I believe it’s not the right time to deprecate rlnMaxEpochGap yet, as clearNullifierLog still depends on it. 

<img width="566" alt="Screenshot 2025-05-20 at 22 30 05" src="https://github.com/user-attachments/assets/985964b6-4567-45de-ada4-b464f0fc44c9" />

created_at: 2025-05-20T16:58:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,7 +48,7 @@ proc fakeWakuMessage*(
     payload: string | seq[byte] = "TEST-PAYLOAD",
     contentTopic = DefaultContentTopic,
     meta: string | seq[byte] = newSeq[byte](),
-    ts = now(),
+    ts = Timestamp(epochTime()),
path: tests/testlib/wakucore.nim
user: login: Ivansete-status
body: This change is not needed. The `WakuMessageTimestamp` may contain the unix epoch time in nanoseconds. This change gives a second resolution. Although not strictly needed, I think is interesting to keep consistency
created_at: 2025-05-21T10:35:38Z
author_association: COLLABORATOR
diff_hunk: @@ -615,7 +616,7 @@ suite "Waku rln relay":
       absDiff(epoch1, epoch2) == uint64(1)
       absDiff(epoch2, epoch1) == uint64(1)
 
-  test "updateLog and hasDuplicate tests":
+  xasyncTest "updateLog and hasDuplicate xasyncTests":
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: Ivansete-status
body: Kindly recover this test back :)
created_at: 2025-05-21T10:35:56Z
author_association: COLLABORATOR
diff_hunk: @@ -700,27 +701,34 @@ suite "Waku rln relay":
     let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr:
       raiseAssert $error
 
-    # get the current epoch time
-    let time = epochTime()
+    let time_1 = epochTime()
 
     #  create some messages from the same peer and append rln proof to them, except wm4
     var
-      wm1 = WakuMessage(payload: "Valid message".toBytes())
+      wm1 =
+        WakuMessage(payload: "Valid message".toBytes(), timestamp: Timestamp(time_1))
       # another message in the same epoch as wm1, it will break the messaging rate limit
-      wm2 = WakuMessage(payload: "Spam".toBytes())
-      #  wm3 points to the next epoch
-      wm3 = WakuMessage(payload: "Valid message".toBytes())
-      wm4 = WakuMessage(payload: "Invalid message".toBytes())
+      wm2 = WakuMessage(payload: "Spam".toBytes(), timestamp: Timestamp(time_1))
 
-    wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+    await sleepAsync(1000)
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: Ivansete-status
body: I wonder if we could make the test faster by artificially creating the `time_1` in the past, such as: `let time_1 = epochTime() - 1000`
```suggestion
```
created_at: 2025-05-21T10:39:47Z
author_association: COLLABORATOR
diff_hunk: @@ -736,6 +744,48 @@ suite "Waku rln relay":
       msgValidate3 == MessageValidationResult.Valid
       msgValidate4 == MessageValidationResult.Invalid
 
+  asyncTest "validateMessageAndUpdateLog: against timestamp":
+    let index = MembershipIndex(5)
+
+    let wakuRlnConfig = WakuRlnConfig(
+      dynamic: false,
+      credIndex: some(index),
+      userMessageLimit: 10,
+      epochSizeSec: 10,
+      treePath: genTempPath("rln_tree", "waku_rln_relay_2"),
+    )
+
+    let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr:
+      raiseAssert $error
+
+    wakuRlnRelay.rlnMaxTimestampGap = 2
+      # usually it's 20 seconds but we set it to 2 for testing purposes which make the test faster
+
+    var time = epochTime()
+
+    var
+      wm1 = WakuMessage(payload: "timestamp message".toBytes(), timestamp: int64(time))
+      wm2 = WakuMessage(payload: "timestamp message".toBytes(), timestamp: int64(time))
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+      raiseAssert $error
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm2, time).isOkOr:
+      raiseAssert $error
+
+    # validate the first message because it's timestamp is the same as the generated timestamp
+    let msgValidate1 = wakuRlnRelay.validateMessageAndUpdateLog(wm1)
+
+    # wait for 3 seconds to make the timestamp different from generated timestamp
+    await sleepAsync(3000)
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: Ivansete-status
body: Similarly to the previous comment , let's make this test faster , if possible ;P
```suggestion
```
created_at: 2025-05-21T10:40:49Z
author_association: COLLABORATOR
diff_hunk: @@ -260,7 +261,7 @@ suite "Waku v2 Rest API - Relay":
       RelayWakuMessage(
         payload: base64.encode("TEST-PAYLOAD"),
         contentTopic: some(DefaultContentTopic),
-        timestamp: some(int64(2022)),
+        timestamp: some(Timestamp(getTime().toUnixFloat())),
path: tests/wakunode_rest/test_rest_relay.nim
user: login: Ivansete-status
body: This is our idiomatic way to do that:
```suggestion
        timestamp: some(getNowInNanosecondTime()),
```
That suggestion applies elsewhere in this file
created_at: 2025-05-21T10:43:23Z
author_association: COLLABORATOR
diff_hunk: @@ -211,25 +213,25 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
path: waku/waku_rln_relay/rln_relay.nim
user: login: Ivansete-status
body: ```suggestion
  let currentTime = getNowInNanosecondTime()
```
created_at: 2025-05-21T10:45:26Z
author_association: COLLABORATOR
diff_hunk: @@ -211,25 +213,25 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  let timeDiff = uint64(abs(currentTime - msg.timestamp))
+
+  debug "time info",
+    currentTime = currentTime, messageTime = msg.timestamp, msgHash = msg.hash
path: waku/waku_rln_relay/rln_relay.nim
user: login: Ivansete-status
body: For analogy with other code:
```suggestion
    currentTime = currentTime, messageTime = msg.timestamp, msg_hash = msg.hash
```
created_at: 2025-05-21T10:47:34Z
author_association: COLLABORATOR
diff_hunk: @@ -211,25 +213,25 @@ proc validateMessage*(
   # track message count for metrics
   waku_rln_messages_total.inc()
 
-  # checks if the `msg`'s epoch is far from the current epoch
-  # it corresponds to the validation of rln external nullifier
-  # get current rln epoch
-  let epoch: Epoch = rlnPeer.getCurrentEpoch()
+  # checks if the message's timestamp is within acceptable range
+  let currentTime = Timestamp(getTime().toUnixFloat())
 
-  let
-    msgEpoch = proof.epoch
-    # calculate the gaps
-    gap = absDiff(epoch, msgEpoch)
-
-  trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch)
-
-  # validate the epoch
-  if gap > rlnPeer.rlnMaxEpochGap:
-    # message's epoch is too old or too ahead
-    # accept messages whose epoch is within +-MaxEpochGap from the current epoch
-    warn "invalid message: epoch gap exceeds a threshold",
-      gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch)
-    waku_rln_invalid_messages_total.inc(labelValues = ["invalid_epoch"])
+  let timeDiff = uint64(abs(currentTime - msg.timestamp))
+
+  debug "time info",
+    currentTime = currentTime, messageTime = msg.timestamp, msgHash = msg.hash
path: waku/waku_rln_relay/rln_relay.nim
user: login: darshankabariya
body: Thanks @Ivansete-status, I’ve addressed all the suggestions. I’m now using nanosecond format for timestemp that's why few suggestion need to ignore.
created_at: 2025-05-21T21:05:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -700,27 +701,34 @@ suite "Waku rln relay":
     let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr:
       raiseAssert $error
 
-    # get the current epoch time
-    let time = epochTime()
+    let time_1 = epochTime()
 
     #  create some messages from the same peer and append rln proof to them, except wm4
     var
-      wm1 = WakuMessage(payload: "Valid message".toBytes())
+      wm1 =
+        WakuMessage(payload: "Valid message".toBytes(), timestamp: Timestamp(time_1))
       # another message in the same epoch as wm1, it will break the messaging rate limit
-      wm2 = WakuMessage(payload: "Spam".toBytes())
-      #  wm3 points to the next epoch
-      wm3 = WakuMessage(payload: "Valid message".toBytes())
-      wm4 = WakuMessage(payload: "Invalid message".toBytes())
+      wm2 = WakuMessage(payload: "Spam".toBytes(), timestamp: Timestamp(time_1))
 
-    wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+    await sleepAsync(1000)
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: darshankabariya
body: Hi @Ivansete-status,
This isn’t possible with the current code because the validateMessage function uses the current clock time. The inject time doesn’t help in creating an artificial case. So, the machine needs to wait at least one second to test this properly. I think that should be okay, right? 
<img width="686" alt="Screenshot 2025-05-26 at 02 57 54" src="https://github.com/user-attachments/assets/92d0f726-1bfe-43c8-9806-aa41248ba875" />

created_at: 2025-05-25T21:28:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -700,27 +701,34 @@ suite "Waku rln relay":
     let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr:
       raiseAssert $error
 
-    # get the current epoch time
-    let time = epochTime()
+    let time_1 = epochTime()
 
     #  create some messages from the same peer and append rln proof to them, except wm4
     var
-      wm1 = WakuMessage(payload: "Valid message".toBytes())
+      wm1 =
+        WakuMessage(payload: "Valid message".toBytes(), timestamp: Timestamp(time_1))
       # another message in the same epoch as wm1, it will break the messaging rate limit
-      wm2 = WakuMessage(payload: "Spam".toBytes())
-      #  wm3 points to the next epoch
-      wm3 = WakuMessage(payload: "Valid message".toBytes())
-      wm4 = WakuMessage(payload: "Invalid message".toBytes())
+      wm2 = WakuMessage(payload: "Spam".toBytes(), timestamp: Timestamp(time_1))
 
-    wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+    await sleepAsync(1000)
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: darshankabariya
body: After this comment, there might be some confusion about the purpose of setting the time in the unsafeAppendRLNProof function. That time is actually used for epoch validation, not for timestamp validation.
created_at: 2025-05-25T21:31:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -736,6 +744,48 @@ suite "Waku rln relay":
       msgValidate3 == MessageValidationResult.Valid
       msgValidate4 == MessageValidationResult.Invalid
 
+  asyncTest "validateMessageAndUpdateLog: against timestamp":
+    let index = MembershipIndex(5)
+
+    let wakuRlnConfig = WakuRlnConfig(
+      dynamic: false,
+      credIndex: some(index),
+      userMessageLimit: 10,
+      epochSizeSec: 10,
+      treePath: genTempPath("rln_tree", "waku_rln_relay_2"),
+    )
+
+    let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr:
+      raiseAssert $error
+
+    wakuRlnRelay.rlnMaxTimestampGap = 2
+      # usually it's 20 seconds but we set it to 2 for testing purposes which make the test faster
+
+    var time = epochTime()
+
+    var
+      wm1 = WakuMessage(payload: "timestamp message".toBytes(), timestamp: int64(time))
+      wm2 = WakuMessage(payload: "timestamp message".toBytes(), timestamp: int64(time))
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr:
+      raiseAssert $error
+
+    wakuRlnRelay.unsafeAppendRLNProof(wm2, time).isOkOr:
+      raiseAssert $error
+
+    # validate the first message because it's timestamp is the same as the generated timestamp
+    let msgValidate1 = wakuRlnRelay.validateMessageAndUpdateLog(wm1)
+
+    # wait for 3 seconds to make the timestamp different from generated timestamp
+    await sleepAsync(3000)
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: darshankabariya
body:  I’ve make bit faster from 3 seconds to 2 seconds. To reduce it further, we’d need to make rlnMaxTimestamp a float. I think it’s fine for now, but I can optimize it more if you suggest.
created_at: 2025-05-25T22:24:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -686,7 +689,7 @@ suite "Waku rln relay":
     # it is a duplicate
     assert isDuplicate3, "duplicate should be found"
 
-  asyncTest "validateMessageAndUpdateLog test":
+  asyncTest "validateMessageAndUpdateLog: again epoch":
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: stubbsta
body: Hi, this test name isn't clear to me, is the message in the previous epoch, or the proof is copied from a previous message, or what does it mean?
created_at: 2025-05-26T11:49:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -736,6 +743,48 @@ suite "Waku rln relay":
       msgValidate3 == MessageValidationResult.Valid
       msgValidate4 == MessageValidationResult.Invalid
 
+  asyncTest "validateMessageAndUpdateLog: against timestamp":
path: tests/waku_rln_relay/test_waku_rln_relay.nim
user: login: stubbsta
body: It is also not clear to me what the name of this test refers to
created_at: 2025-05-26T11:49:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-14T20:23:37Z
body: You can find the image built from this PR at

```
quay.io/wakuorg/nwaku-pr:3406
```

Built from 837742758a4253cbd96b58730ea8f15f66788ad6
<!-- thollander/actions-comment-pull-request "execution-rln-v" -->
author_association: NONE
user: login: darshankabariya
created_at: 2025-05-14T23:27:37Z
body: > At this stage, the verification can be easily fooled by just always adding a fake current `timestamp` to messages while actually computing the RLN proof against any number of very old epochs. In order to use `timestamp` base validation, we need to verify that the `proof` was indeed generated against the `timestamp` included in the message. In other words:

>we still don't know if the nullifier was generated with any old, expired epoch. In other words, we also have to verify that the received timestamp belongs to the received epoch:


Thanks for the clarification @jm-clius
I’ve updated the code to include a timestamp in the signal and verification logic now handles epoch validation against the provided timestamp.

FYI: You can ignore the newly added test case for now — it’s still a work in progress and needs to be adjusted to properly test timestamp gaps. plz let me know is current implementation is logically correct ??

( cc @Ivansete-status @stubbsta )
author_association: CONTRIBUTOR
user: login: darshankabariya
created_at: 2025-05-20T11:02:22Z
body: > Thanks Darshan, this looks good. Is there a test to check that a message with an invalid timestamp correctly gets identified and rejected? If not, would it be possible to add one?

Thanks for pointing that out. There’s actually a limitation when adding a wait between appendRLNProof and message validation because it uses the POST API flow. I’ll look into finding a way to test that approach as well.
author_association: CONTRIBUTOR
user: login: darshankabariya
created_at: 2025-05-20T19:48:34Z
body: > Thanks Darshan, this looks good. Is there a test to check that a message with an invalid timestamp correctly gets identified and rejected? If not, would it be possible to add one?

Hi @stubbsta , I have added a test case that covers both valid and invalid scenarios.
author_association: CONTRIBUTOR
user: login: darshankabariya
created_at: 2025-05-26T12:25:02Z
body: Hi @stubbsta,
I’ve updated the implementation to follow better naming conventions.

Just to clarify the functionality: the epoch and timestamp validations overlap slightly. The epoch helps prevent overspending the message limit, while the timestamp ensures messages older than a certain time gap are discarded from the network.

If you look at the tests, you’ll see I’m testing the epoch with respect to message limits, and the timestamp with respect to the allowed gap.

Let me know if anything’s still unclear—happy to explain further!
author_association: CONTRIBUTOR