From 1f9b0650b9ecdb9b569f73f32ef17ee44f036b33 Mon Sep 17 00:00:00 2001
From: Matt Haggard <haggardii@gmail.com>
Date: Wed, 3 Dec 2025 10:36:14 -0500
Subject: [PATCH] RelayMessages have a resp_id to associate them with
 RelayCommand that caused the message

---
 ...ew-RelayMessages-have-a-20251203-103527.md |   1 +
 src/bucketsrelay/v2/objs.nim                  |  57 ++++--
 src/bucketsrelay/v2/proto2.nim                |  64 ++++---
 tests/tfunctional.nim                         |   2 +-
 tests/tproto2.nim                             | 173 +++++++++++++++++-
 tests/tserde2.nim                             |  43 +++--
 6 files changed, 280 insertions(+), 60 deletions(-)
 create mode 100644 changes/new-RelayMessages-have-a-20251203-103527.md

diff --git a/changes/new-RelayMessages-have-a-20251203-103527.md b/changes/new-RelayMessages-have-a-20251203-103527.md
new file mode 100644
index 0000000..eb3335c
--- /dev/null
+++ b/changes/new-RelayMessages-have-a-20251203-103527.md
@@ -0,0 +1 @@
+RelayMessages have a resp_id to associate them with RelayCommand that caused the message
diff --git a/src/bucketsrelay/v2/objs.nim b/src/bucketsrelay/v2/objs.nim
index 1209d87..f7714ec 100644
--- a/src/bucketsrelay/v2/objs.nim
+++ b/src/bucketsrelay/v2/objs.nim
@@ -48,6 +48,7 @@ type
     InvalidParams = 5
 
   RelayMessage* = object
+    resp_id*: int
     case kind*: MessageKind
     of Who:
       who_challenge*: Challenge
@@ -82,6 +83,7 @@ type
     HasChunks
 
   RelayCommand* = object
+    resp_id*: int
     case kind*: CommandKind
     of Iam:
       iam_pubkey*: PublicKey
@@ -186,7 +188,7 @@ proc `$`*(msg: RelayMessage): string =
   result.add ")"
 
 proc `==`*(a, b: RelayMessage): bool =
-  if a.kind != b.kind:
+  if a.kind != b.kind or a.resp_id != b.resp_id:
     return false
   else:
     case a.kind
@@ -231,7 +233,7 @@ proc `$`*(cmd: RelayCommand): string =
   result.add ")"
 
 proc `==`*(a, b: RelayCommand): bool =
-  if a.kind != b.kind:
+  if a.kind != b.kind or a.resp_id != b.resp_id:
     return false
   else:
     case a.kind
@@ -419,6 +421,9 @@ proc deserialize*(typ: typedesc[seq[string]], val: string): seq[string] =
 
 proc serialize*(msg: RelayMessage): string =
   result &= msg.kind.serialize()
+  # For Who and Data messages, resp_id is always omitted (always 0)
+  if msg.kind notin {Who, Data}:
+    result &= nsencode($msg.resp_id)
   case msg.kind
   of Who:
     result &= msg.who_challenge.serialize()
@@ -448,37 +453,45 @@ proc serialize*(msg: RelayMessage): string =
 proc deserialize*(typ: typedesc[RelayMessage], s: string): RelayMessage =
   if s.len == 0:
     raise ValueError.newException("Empty RelayMessage")
-  let kind = MessageKind.deserialize(s[0])
+  var idx = 0
+  let kind = MessageKind.deserialize(s[idx])
+  idx.inc()
+  # For Who and Data messages, resp_id is always 0 and not serialized
+  let resp_id = if kind in {Who, Data}:
+      0
+    else:
+      s.nsdecode(idx).parseInt()
   case kind
   of Who:
-    return RelayMessage(kind: Who, who_challenge: Challenge.deserialize(s[1..^1]))
+    return RelayMessage(kind: Who, resp_id: resp_id, who_challenge: Challenge.deserialize(s[idx..^1]))
   of Okay:
-    return RelayMessage(kind: Okay, ok_cmd: CommandKind.deserialize(s[1]))
+    return RelayMessage(kind: Okay, resp_id: resp_id, ok_cmd: CommandKind.deserialize(s[idx]))
   of Error:
     return RelayMessage(
       kind: Error,
-      err_cmd: CommandKind.deserialize(s[1]),
-      err_code: ErrorCode.deserialize(s[2]),
-      err_message: s[3..^1]
+      resp_id: resp_id,
+      err_cmd: CommandKind.deserialize(s[idx]),
+      err_code: ErrorCode.deserialize(s[idx+1]),
+      err_message: s[(idx+2)..^1]
     )
   of Note:
-    var idx = 1
     return RelayMessage(
       kind: Note,
+      resp_id: resp_id,
       note_topic: s.nsdecode(idx),
       note_data: s.nsdecode(idx),
     )
   of Data:
-    var idx = 1
     return RelayMessage(
       kind: Data,
+      resp_id: resp_id,
       data_src: s.nsdecode(idx).PublicKey,
       data_val: s.nsdecode(idx),
     )
   of Chunk:
-    var idx = 1
     return RelayMessage(
       kind: Chunk,
+      resp_id: resp_id,
       chunk_src: s.nsdecode(idx).PublicKey,
       chunk_key: s.nsdecode(idx),
       chunk_val: if idx >= s.len:
@@ -487,9 +500,9 @@ proc deserialize*(typ: typedesc[RelayMessage], s: string): RelayMessage =
           some(s.nsdecode(idx)),
     )
   of ChunkStatus:
-    var idx = 1
     return RelayMessage(
       kind: ChunkStatus,
+      resp_id: resp_id,
       status_src: s.nsdecode(idx).PublicKey,
       present: deserialize(seq[string], s.nsdecode(idx)),
       absent: deserialize(seq[string], s.nsdecode(idx)),
@@ -497,6 +510,7 @@ proc deserialize*(typ: typedesc[RelayMessage], s: string): RelayMessage =
 
 proc serialize*(cmd: RelayCommand): string =
   result &= cmd.kind.serialize
+  result &= nsencode($cmd.resp_id)
   case cmd.kind
   of Iam:
     result &= cmd.iam_pubkey.string.nsencode
@@ -523,54 +537,57 @@ proc serialize*(cmd: RelayCommand): string =
 proc deserialize*(typ: typedesc[RelayCommand], s: string): RelayCommand =
   if s.len == 0:
     raise ValueError.newException("Empty RelayCommand")
-  let kind = CommandKind.deserialize(s[0])
+  var idx = 0
+  let kind = CommandKind.deserialize(s[idx])
+  idx.inc()
+  let resp_id = s.nsdecode(idx).parseInt()
   case kind
   of Iam:
-    var idx = 1
     return RelayCommand(
       kind: Iam,
+      resp_id: resp_id,
       iam_pubkey: s.nsdecode(idx).PublicKey,
       iam_answer: ChallengeAnswer.deserialize(s.nsdecode(idx)),
     )
   of PublishNote:
-    var idx = 1
     return RelayCommand(
       kind: PublishNote,
+      resp_id: resp_id,
       pub_topic: s.nsdecode(idx),
       pub_data: s.nsdecode(idx),
     )
   of FetchNote:
-    var idx = 1
     return RelayCommand(
       kind: FetchNote,
+      resp_id: resp_id,
       fetch_topic: s.nsdecode(idx),
     )
   of SendData:
-    var idx = 1
     return RelayCommand(
       kind: SendData,
+      resp_id: resp_id,
       send_dst: s.nsdecode(idx).PublicKey,
       send_val: s.nsdecode(idx),
     )
   of StoreChunk:
-    var idx = 1
     return RelayCommand(
       kind: StoreChunk,
+      resp_id: resp_id,
       chunk_dst: deserializePubKeys(s.nsdecode(idx)),
       chunk_key: s.nsdecode(idx),
       chunk_val: s.nsdecode(idx),
     )
   of GetChunks:
-    var idx = 1
     return RelayCommand(
       kind: GetChunks,
+      resp_id: resp_id,
       chunk_src: s.nsdecode(idx).PublicKey,
       chunk_keys: deserialize(seq[string], s.nsdecode(idx)),
     )
   of HasChunks:
-    var idx = 1
     return RelayCommand(
       kind: HasChunks,
+      resp_id: resp_id,
       has_src: s.nsdecode(idx).PublicKey,
       has_keys: deserialize(seq[string], s.nsdecode(idx)),
     )
diff --git a/src/bucketsrelay/v2/proto2.nim b/src/bucketsrelay/v2/proto2.nim
index 3042135..3be39f0 100644
--- a/src/bucketsrelay/v2/proto2.nim
+++ b/src/bucketsrelay/v2/proto2.nim
@@ -275,18 +275,20 @@ proc newRelay*[T](db: DbConn): Relay[T] =
   result.clients = newTable[PublicKey, RelayConnection[T]]()
   db.updateSchema()
 
-template sendError*[T](conn: RelayConnection[T], msg: string, cmd: CommandKind, code: ErrorCode) =
+template sendError*[T](conn: RelayConnection[T], cmd: RelayCommand, msg: string, code: ErrorCode) =
   conn.sendMessage(RelayMessage(
     kind: Error,
+    resp_id: cmd.resp_id,
     err_code: code,
     err_message: msg,
-    err_cmd: cmd,
+    err_cmd: cmd.kind,
   ))
 
-template sendOkay*[T](conn: RelayConnection[T], cmd: CommandKind) =
+template sendOkay*[T](conn: RelayConnection[T], cmd: RelayCommand) =
   conn.sendMessage(RelayMessage(
     kind: Okay,
-    ok_cmd: cmd,
+    resp_id: cmd.resp_id,
+    ok_cmd: cmd.kind,
   ))
 
 proc is_valid*(x: PublicKey): bool =
@@ -308,6 +310,7 @@ proc initAuth*[T](relay: Relay[T], client: T): RelayConnection[T] =
   result.challenge = some(generateChallenge())
   result.sendMessage(RelayMessage(
     kind: Who,
+    resp_id: 0,  # Who messages are not triggered by a command
     who_challenge: result.challenge.get(),
   ))
   result.relay = relay
@@ -480,6 +483,7 @@ proc nextMessage(relay: Relay, dst: PublicKey): Option[RelayMessage] =
     let row = orow.get()
     result = some(RelayMessage(
       kind: Data,
+      resp_id: 0,  # Data messages are not triggered by recipient's command
       data_src: PublicKey.fromDB(row[0].b),
       data_val: row[1].b.string,
     ))
@@ -502,23 +506,23 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
   when LOG_COMMS:
     info "[" & conn.pubkey.abbr & "] DO " & $cmd
   if conn.pubkey.isNone and cmd.kind != Iam:
-    conn.sendError("Not allowed", cmd.kind, NotAllowed)
+    conn.sendError(cmd, "Not allowed", NotAllowed)
     return
 
   case cmd.kind
   of Iam:
     if conn.challenge.isNone:
-      conn.sendError("Already authenticated", cmd.kind, Generic)
+      conn.sendError(cmd, "Already authenticated", Generic)
       return
     let challenge = conn.challenge.get()
     conn.challenge = none[Challenge]() # disable future authentication attempts
-    
+
     try:
       if not is_valid_answer(cmd.iam_pubkey, challenge, cmd.iam_answer):
-        conn.sendError("Invalid answer", cmd.kind, Generic)
+        conn.sendError(cmd, "Invalid answer", Generic)
         return
     except CatchableError:
-      conn.sendError("Invalid answer", cmd.kind, Generic)
+      conn.sendError(cmd, "Invalid answer", Generic)
       return
 
     # successful connection
@@ -526,7 +530,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
     conn.pubkey = some(pubkey)
     relay.clients[pubkey] = conn
     info &"[{conn.pubkey.abbr}] connected"
-    conn.sendOkay cmd.kind
+    conn.sendOkay(cmd)
     relay.db.record_event_stat(
       ip = conn.ip,
       pubkey = pubkey,
@@ -550,13 +554,13 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
         break
   of PublishNote:
     if cmd.pub_topic.len > RELAY_MAX_TOPIC_SIZE:
-      conn.sendError("Topic too long", cmd.kind, TooLarge)
+      conn.sendError(cmd, "Topic too long", TooLarge)
     elif cmd.pub_data.len > RELAY_MAX_NOTE_SIZE:
-      conn.sendError("Data too long", cmd.kind, TooLarge)
+      conn.sendError(cmd, "Data too long", TooLarge)
     else:
       let pubkey = conn.pubkey.get()
       if relay.noteCount(pubkey) >= RELAY_MAX_NOTES:
-        conn.sendError("Too many notes", cmd.kind, StorageLimitExceeded)
+        conn.sendError(cmd, "Too many notes", StorageLimitExceeded)
       else:
         relay.db.record_transfer_stat(
           ip = conn.ip,
@@ -572,9 +576,10 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
         if opubkey.isSome:
           # someone is waiting
           var other_conn = relay.clients[opubkey.get()]
-          conn.sendOkay cmd.kind
+          conn.sendOkay(cmd)
           other_conn.sendMessage(RelayMessage(
             kind: Note,
+            resp_id: 0,  # Not triggered by other_conn's command
             note_data: cmd.pub_data,
             note_topic: cmd.pub_topic,
           ))
@@ -592,12 +597,12 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
               cmd.pub_data.DbBlob,
               pubkey,
             )
-            conn.sendOkay cmd.kind
+            conn.sendOkay(cmd)
           except:
-            conn.sendError("Duplicate topic", cmd.kind, Generic)
+            conn.sendError(cmd, "Duplicate topic", Generic)
   of FetchNote:
     if cmd.fetch_topic.len > RELAY_MAX_TOPIC_SIZE:
-      conn.sendError("Topic too long", cmd.kind, TooLarge)
+      conn.sendError(cmd, "Topic too long", TooLarge)
     else:
       let odata = relay.popNote(cmd.fetch_topic)
       if odata.isSome():
@@ -605,6 +610,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
         let data = odata.get()
         conn.sendMessage(RelayMessage(
           kind: Note,
+          resp_id: cmd.resp_id,  # Response to FetchNote command
           note_data: data,
           note_topic: cmd.fetch_topic,
         ))
@@ -618,13 +624,13 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
         relay.addNoteSub(cmd.fetch_topic, conn.pubkey.get())
   of SendData:
     if cmd.send_val.len > RELAY_MAX_MESSAGE_SIZE:
-      conn.sendError("Data too long", cmd.kind, TooLarge)
+      conn.sendError(cmd, "Data too long", TooLarge)
     elif not cmd.send_dst.is_valid():
-      conn.sendError("Invalid pubkey", cmd.kind, InvalidParams)
+      conn.sendError(cmd, "Invalid pubkey", InvalidParams)
     else:
       let pubkey = conn.pubkey.get()
       if relay.max_transfer_rate != 0 and relay.db.current_data_in(pubkey) > relay.max_transfer_rate:
-        conn.sendError("Rate limit exceeded", cmd.kind, TransferLimitExceeeded)
+        conn.sendError(cmd, "Rate limit exceeded", TransferLimitExceeeded)
       else:
         relay.db.record_transfer_stat(
           ip = conn.ip,
@@ -641,6 +647,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
           var other_conn = relay.clients[cmd.send_dst]
           other_conn.sendMessage(RelayMessage(
             kind: Data,
+            resp_id: 0,  # Not triggered by other_conn's command
             data_src: pubkey,
             data_val: cmd.send_val,
           ))
@@ -655,17 +662,17 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
               pubkey, cmd.send_dst, cmd.send_val.DbBlob)
   of StoreChunk:
     if cmd.chunk_key.len > RELAY_MAX_CHUNK_KEY_SIZE:
-      conn.sendError("Key too long", cmd.kind, TooLarge)
+      conn.sendError(cmd, "Key too long", TooLarge)
     elif cmd.chunk_val.len > RELAY_MAX_CHUNK_SIZE:
-      conn.sendError("Value too long", cmd.kind, TooLarge)
+      conn.sendError(cmd, "Value too long", TooLarge)
     elif cmd.chunk_dst.len > RELAY_MAX_CHUNK_DSTS:
-      conn.sendError("Too many recipients", cmd.kind, TooLarge)
+      conn.sendError(cmd, "Too many recipients", TooLarge)
     elif cmd.chunk_dst.any_invalid():
-      conn.sendError("Invalid pubkey", cmd.kind, InvalidParams)
+      conn.sendError(cmd, "Invalid pubkey", InvalidParams)
     else:
       let pubkey = conn.pubkey.get()
       if relay.max_chunk_space > 0 and relay.db.chunk_space_used(pubkey) > relay.max_chunk_space:
-        conn.sendError("Too much chunk data", cmd.kind, StorageLimitExceeded)
+        conn.sendError(cmd, "Too much chunk data", StorageLimitExceeded)
       else:
         relay.db.record_event_stat(
           ip = conn.ip,
@@ -696,7 +703,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
   of GetChunks:
     for key in cmd.chunk_keys:
       if key.len > RELAY_MAX_CHUNK_KEY_SIZE:
-        conn.sendError("Key too long", cmd.kind, TooLarge)
+        conn.sendError(cmd, "Key too long", TooLarge)
         return
     relay.delExpiredChunks()
     let pubkey = conn.pubkey.get()
@@ -718,6 +725,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
         let row = orow.get()
         conn.sendMessage(RelayMessage(
           kind: Chunk,
+          resp_id: cmd.resp_id,
           chunk_src: cmd.chunk_src,
           chunk_key: key,
           chunk_val: some(row[0].b.string),
@@ -725,6 +733,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
       else:
         conn.sendMessage(RelayMessage(
           kind: Chunk,
+          resp_id: cmd.resp_id,
           chunk_src: cmd.chunk_src,
           chunk_key: key,
           chunk_val: none[string](),
@@ -732,7 +741,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
   of HasChunks:
     for key in cmd.has_keys:
       if key.len > RELAY_MAX_CHUNK_KEY_SIZE:
-        conn.sendError("Key too long", cmd.kind, TooLarge)
+        conn.sendError(cmd, "Key too long", TooLarge)
         return
     relay.delExpiredChunks()
     let pubkey = conn.pubkey.get()
@@ -768,6 +777,7 @@ proc handleCommand*[T](relay: Relay[T], conn: var RelayConnection[T], cmd: Relay
         absent.add(key)
     conn.sendMessage(RelayMessage(
       kind: ChunkStatus,
+      resp_id: cmd.resp_id,
       status_src: cmd.has_src,
       present: present,
       absent: absent,
diff --git a/tests/tfunctional.nim b/tests/tfunctional.nim
index 85f06f7..dba3d31 100644
--- a/tests/tfunctional.nim
+++ b/tests/tfunctional.nim
@@ -27,7 +27,7 @@ proc startServer(port: Port): Process =
   let compileProcess = startProcess(
     "nim",
     workingDir = currentSourcePath().parentDir().parentDir(),
-    args = ["c", "-d:testmode", "-o:" & bin, "src/bucketsrelay/server2.nim"],
+    args = ["c", "-d:testmode", "-o:" & bin, "src/bucketsrelay/v2/server2.nim"],
     options = {poStdErrToStdOut, poUsePath}
   )
   let output = compileProcess.outputStream.readAll()
diff --git a/tests/tproto2.nim b/tests/tproto2.nim
index e33f63e..d5bcd37 100644
--- a/tests/tproto2.nim
+++ b/tests/tproto2.nim
@@ -80,12 +80,14 @@ proc authenticatedConn(relay: Relay, keys: KeyPair): RelayConnection[TestClient]
   let answer = who.who_challenge.answer(client.sk)
   relay.handleCommand(conn, RelayCommand(
     kind: Iam,
+    resp_id: 1,
     iam_answer: answer,
     iam_pubkey: client.pk
   ))
   let ok = conn.pop()
   doAssert ok.kind == Okay
   doAssert ok.ok_cmd == Iam
+  doAssert ok.resp_id == 1
   return conn
 
 proc authenticatedConn(relay: Relay): RelayConnection[TestClient] =
@@ -846,4 +848,173 @@ suite "stats":
     check db.stats_transfer_total(period="2010-01") == (1000+2000, 500+250, "", "".PublicKey, "2010-01")
     check db.stats_transfer_total(period="2010-02") == (3000+500, 100+100, "", "".PublicKey, "2010-02")
 
-  
\ No newline at end of file
+suite "resp_id":
+
+  test "Who message has resp_id 0":
+    let relay = testRelay()
+    let client = newTestClient(genkeys())
+    var conn = relay.initAuth(client)
+    let who = conn.pop(Who)
+    check who.resp_id == 0
+
+  test "Iam command response has matching resp_id":
+    let relay = testRelay()
+    let client = newTestClient(genkeys())
+    var conn = relay.initAuth(client)
+    let who = conn.pop(Who)
+    let answer = who.who_challenge.answer(client.sk)
+
+    relay.handleCommand(conn, RelayCommand(
+      kind: Iam,
+      resp_id: 42,
+      iam_answer: answer,
+      iam_pubkey: client.pk
+    ))
+    let ok = conn.pop(Okay)
+    check ok.resp_id == 42
+
+  test "PublishNote response has matching resp_id":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+
+    relay.handleCommand(alice, RelayCommand(
+      kind: PublishNote,
+      resp_id: 123,
+      pub_topic: "test",
+      pub_data: "data",
+    ))
+    let ok = alice.pop(Okay)
+    check ok.resp_id == 123
+
+  test "FetchNote response has matching resp_id":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+
+    # Publish a note first
+    relay.handleCommand(alice, RelayCommand(
+      kind: PublishNote,
+      pub_topic: "test",
+      pub_data: "data",
+    ))
+    discard alice.pop(Okay)
+
+    # Fetch the note with resp_id
+    relay.handleCommand(alice, RelayCommand(
+      kind: FetchNote,
+      resp_id: 456,
+      fetch_topic: "test",
+    ))
+    let note = alice.pop(Note)
+    check note.resp_id == 456
+
+  test "Error response has matching resp_id":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+
+    relay.handleCommand(alice, RelayCommand(
+      kind: PublishNote,
+      resp_id: 789,
+      pub_topic: "a".repeat(RELAY_MAX_TOPIC_SIZE + 1),
+      pub_data: "data",
+    ))
+    let err = alice.pop(Error)
+    check err.resp_id == 789
+
+  test "Data message has resp_id 0 (no command trigger)":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+    var bob = relay.authenticatedConn()
+
+    relay.handleCommand(alice, RelayCommand(
+      kind: SendData,
+      resp_id: 111,
+      send_dst: bob.pk,
+      send_val: "hello",
+    ))
+
+    # Bob receives the Data message - it should have resp_id 0
+    # because it wasn't triggered by Bob's command
+    let data = bob.pop(Data)
+    check data.resp_id == 0
+
+  test "StoreChunk command response has matching resp_id":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+
+    relay.handleCommand(alice, RelayCommand(
+      kind: StoreChunk,
+      resp_id: 222,
+      chunk_dst: @[],
+      chunk_key: "key",
+      chunk_val: "val",
+    ))
+    # StoreChunk doesn't send a response by default, no message to check
+    check alice.msgCount == 0
+
+  test "GetChunks response has matching resp_id":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+
+    # Store a chunk first
+    relay.handleCommand(alice, RelayCommand(
+      kind: StoreChunk,
+      chunk_dst: @[],
+      chunk_key: "key",
+      chunk_val: "val",
+    ))
+
+    # Get the chunk with resp_id
+    relay.handleCommand(alice, RelayCommand(
+      kind: GetChunks,
+      resp_id: 333,
+      chunk_src: alice.pk,
+      chunk_keys: @["key"],
+    ))
+    let chunk = alice.pop(Chunk)
+    check chunk.resp_id == 333
+
+  test "HasChunks response has matching resp_id":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+
+    # Store a chunk first
+    relay.handleCommand(alice, RelayCommand(
+      kind: StoreChunk,
+      chunk_dst: @[],
+      chunk_key: "key",
+      chunk_val: "val",
+    ))
+
+    # Check if chunk exists with resp_id
+    relay.handleCommand(alice, RelayCommand(
+      kind: HasChunks,
+      resp_id: 444,
+      has_src: alice.pk,
+      has_keys: @["key"],
+    ))
+    let status = alice.pop(ChunkStatus)
+    check status.resp_id == 444
+
+  test "Multiple commands with different resp_ids":
+    let relay = testRelay()
+    var alice = relay.authenticatedConn()
+
+    # Send multiple commands with different resp_ids
+    relay.handleCommand(alice, RelayCommand(
+      kind: PublishNote,
+      resp_id: 100,
+      pub_topic: "topic1",
+      pub_data: "data1",
+    ))
+    relay.handleCommand(alice, RelayCommand(
+      kind: PublishNote,
+      resp_id: 200,
+      pub_topic: "topic2",
+      pub_data: "data2",
+    ))
+
+    let ok1 = alice.pop(Okay)
+    check ok1.resp_id == 100
+    let ok2 = alice.pop(Okay)
+    check ok2.resp_id == 200
+
diff --git a/tests/tserde2.nim b/tests/tserde2.nim
index 122362e..6d30580 100644
--- a/tests/tserde2.nim
+++ b/tests/tserde2.nim
@@ -17,13 +17,13 @@ test "CommandKind":
 test "RelayMessage":
   for kind in low(MessageKind)..high(MessageKind):
     let example = case kind
-      of Who: RelayMessage(kind: Who, who_challenge: generateChallenge())
-      of Okay: RelayMessage(kind: Okay, ok_cmd: SendData)
-      of Error: RelayMessage(kind: Error, err_cmd: SendData, err_code: TooLarge, err_message: "foo")
-      of Note: RelayMessage(kind: Note, note_topic: "something", note_data: "data")
-      of Data: RelayMessage(kind: Data, data_src: "hey".PublicKey, data_val: "foo")
-      of Chunk: RelayMessage(kind: Chunk, chunk_src: "hey".PublicKey, chunk_key: "key", chunk_val: some("theval"))
-      of ChunkStatus: RelayMessage(kind: ChunkStatus, status_src: "a".PublicKey, present: @["foo"], absent: @["bar"])
+      of Who: RelayMessage(kind: Who, resp_id: 0, who_challenge: generateChallenge())
+      of Okay: RelayMessage(kind: Okay, resp_id: 42, ok_cmd: SendData)
+      of Error: RelayMessage(kind: Error, resp_id: 123, err_cmd: SendData, err_code: TooLarge, err_message: "foo")
+      of Note: RelayMessage(kind: Note, resp_id: 456, note_topic: "something", note_data: "data")
+      of Data: RelayMessage(kind: Data, resp_id: 0, data_src: "hey".PublicKey, data_val: "foo")
+      of Chunk: RelayMessage(kind: Chunk, resp_id: 789, chunk_src: "hey".PublicKey, chunk_key: "key", chunk_val: some("theval"))
+      of ChunkStatus: RelayMessage(kind: ChunkStatus, resp_id: 999, status_src: "a".PublicKey, present: @["foo"], absent: @["bar"])
     let serialized = example.serialize()
     info $example
     info "serialized: " & serialized.nice
@@ -34,6 +34,7 @@ test "RelayCommand":
     let example = case kind
       of Iam: RelayCommand(
         kind: Iam,
+        resp_id: 1,
         iam_pubkey: "hey".PublicKey,
         iam_answer: (
           nonce: 1,
@@ -41,18 +42,20 @@ test "RelayCommand":
           signature: "hey",
         ),
       )
-      of PublishNote: RelayCommand(kind: PublishNote, pub_topic: "topic", pub_data: "data")
-      of FetchNote: RelayCommand(kind: FetchNote, fetch_topic: "topic")
-      of SendData: RelayCommand(kind: SendData, send_dst: "one".PublicKey, send_val: "data")
+      of PublishNote: RelayCommand(kind: PublishNote, resp_id: 100, pub_topic: "topic", pub_data: "data")
+      of FetchNote: RelayCommand(kind: FetchNote, resp_id: 200, fetch_topic: "topic")
+      of SendData: RelayCommand(kind: SendData, resp_id: 300, send_dst: "one".PublicKey, send_val: "data")
       of StoreChunk: RelayCommand(
           kind: StoreChunk,
+          resp_id: 400,
           chunk_dst: @["one".PublicKey],
           chunk_key: "theky",
           chunk_val: "someval"
         )
-      of GetChunks: RelayCommand(kind: GetChunks, chunk_src: "hey".PublicKey, chunk_keys: @["foo", "bar"])
+      of GetChunks: RelayCommand(kind: GetChunks, resp_id: 500, chunk_src: "hey".PublicKey, chunk_keys: @["foo", "bar"])
       of HasChunks: RelayCommand(
           kind: HasChunks,
+          resp_id: 600,
           has_src: "hey".PublicKey,
           has_keys: @["foo", "Bar"],
         )
@@ -63,6 +66,7 @@ test "RelayCommand":
 
 test "Chunk with none":
   let chunk = RelayMessage(kind: Chunk,
+    resp_id: 888,
     chunk_src: "foo".PublicKey,
     chunk_key: "key",
     chunk_val: none[string](),
@@ -77,3 +81,20 @@ test "ErrorCodes":
     let serialized = err.serialize()
     checkpoint "serialized.nice: " & nice($serialized)
     check ErrorCode.deserialize(serialized) == err
+
+test "resp_id serialization":
+  # Test that resp_id values are preserved during serialization
+  let msg1 = RelayMessage(kind: Okay, resp_id: 12345, ok_cmd: SendData)
+  check RelayMessage.deserialize(msg1.serialize()).resp_id == 12345
+
+  let msg2 = RelayMessage(kind: Error, resp_id: 99999, err_cmd: Iam, err_code: Generic, err_message: "test")
+  check RelayMessage.deserialize(msg2.serialize()).resp_id == 99999
+
+  let msg3 = RelayMessage(kind: Who, resp_id: 0, who_challenge: generateChallenge())
+  check RelayMessage.deserialize(msg3.serialize()).resp_id == 0
+
+  let cmd1 = RelayCommand(kind: PublishNote, resp_id: 54321, pub_topic: "topic", pub_data: "data")
+  check RelayCommand.deserialize(cmd1.serialize()).resp_id == 54321
+
+  let cmd2 = RelayCommand(kind: SendData, resp_id: 0, send_dst: "dst".PublicKey, send_val: "val")
+  check RelayCommand.deserialize(cmd2.serialize()).resp_id == 0
