Skip to content

Commit d98a602

Browse files
authored
Fluffy: Update ping endpoints to correctly support ping extensions (#3185)
* Update ping endpoint to conform to ping extensions/json rpc spec.
1 parent 760be96 commit d98a602

File tree

5 files changed

+110
-26
lines changed

5 files changed

+110
-26
lines changed

fluffy/docs/the_fluffy_book/mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Nimbus Fluffy book
2-
# Copyright (c) 2023-2024 Status Research & Development GmbH
2+
# Copyright (c) 2023-2025 Status Research & Development GmbH
33
# Licensed and distributed under either of
44
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
55
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).

fluffy/network/wire/portal_protocol.nim

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ proc recordsFromBytes(rawRecords: List[ByteList[2048], 32]): PortalResult[seq[Re
779779

780780
proc ping*(
781781
p: PortalProtocol, dst: Node
782-
): Future[PortalResult[(uint64, CapabilitiesPayload)]] {.
782+
): Future[PortalResult[(uint64, uint16, CapabilitiesPayload)]] {.
783783
async: (raises: [CancelledError])
784784
.} =
785785
if p.isBanned(dst.id):
@@ -800,7 +800,7 @@ proc ping*(
800800

801801
p.radiusCache.put(dst.id, payload.data_radius)
802802

803-
ok((pong.enrSeq, payload))
803+
ok((pong.enrSeq, pong.payload_type, payload))
804804

805805
proc findNodes*(
806806
p: PortalProtocol, dst: Node, distances: seq[uint16]
@@ -1774,7 +1774,7 @@ proc revalidateNode*(p: PortalProtocol, n: Node) {.async: (raises: [CancelledErr
17741774
let pong = await p.ping(n)
17751775

17761776
if pong.isOk():
1777-
let (enrSeq, _) = pong.get()
1777+
let (enrSeq, _, _) = pong.get()
17781778
if enrSeq > n.record.seqNum:
17791779
# Request new ENR
17801780
let nodesMessage = await p.findNodes(n, @[0'u16])

fluffy/rpc/rpc_portal_common_api.nim

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import
1111
std/[sequtils, json],
1212
json_rpc/rpcserver,
1313
stew/byteutils,
14-
../network/wire/[portal_protocol, portal_protocol_config],
14+
results,
15+
../network/wire/[portal_protocol, portal_protocol_config, ping_extensions],
1516
./rpc_types
1617

1718
{.warning[UnusedImport]: off.}
@@ -75,16 +76,37 @@ proc installPortalCommonApiHandlers*(
7576
else:
7677
raise newException(ValueError, "Record not found in DHT lookup.")
7778

78-
rpcServer.rpc("portal_" & networkStr & "Ping") do(enr: Record) -> PingResult:
79+
rpcServer.rpc("portal_" & networkStr & "Ping") do(
80+
enr: Record, payloadType: Opt[uint16], payload: Opt[UnknownPayload]
81+
) -> PingResult:
82+
if payloadType.isSome() and payloadType.get() != CapabilitiesType:
83+
# We only support sending the default CapabilitiesPayload for now.
84+
# This is fine because according to the spec clients are only required
85+
# to support the standard extensions.
86+
raise payloadTypeNotSupportedError()
87+
88+
if payload.isSome():
89+
# We don't support passing in a custom payload. In order to implement
90+
# this we use the empty UnknownPayload type which is defined in the spec
91+
# as a json object with no required fields. Just using it here to indicate
92+
# if an object was supplied or not and then throw the correct error if so.
93+
raise userSpecifiedPayloadBlockedByClientError()
94+
7995
let
8096
node = toNodeWithAddress(enr)
81-
pong = await p.ping(node)
97+
pong = (await p.ping(node)).valueOr:
98+
raise newException(ValueError, $error)
8299

83-
if pong.isErr():
84-
raise newException(ValueError, $pong.error)
85-
else:
86-
let (enrSeq, pongPayload) = pong.get()
87-
return (enrSeq, pongPayload.data_radius)
100+
let
101+
(enrSeq, payloadType, capabilitiesPayload) = pong
102+
clientInfo = capabilitiesPayload.client_info.asSeq()
103+
payload = (
104+
string.fromBytes(clientInfo),
105+
capabilitiesPayload.data_radius,
106+
capabilitiesPayload.capabilities.asSeq(),
107+
)
108+
109+
return PingResult(enrSeq: enrSeq, payloadType: payloadType, payload: payload)
88110

89111
rpcServer.rpc("portal_" & networkStr & "FindNodes") do(
90112
enr: Record, distances: seq[uint16]

fluffy/rpc/rpc_types.nim

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,29 @@ import
1717
export jsonmarshal, routing_table, enr, node
1818

1919
# Portal Network JSON-RPC errors
20+
2021
const
2122
# These errors are defined in the portal jsonrpc spec: https://github.com/ethereum/portal-network-specs/tree/master/jsonrpc
2223
ContentNotFoundError* = (code: -39001, msg: "Content not found")
2324
ContentNotFoundErrorWithTrace* = (code: -39002, msg: "Content not found")
25+
PayloadTypeNotSupportedError* = (code: -39004, msg: "Payload type not supported")
26+
FailedToDecodePayloadError* = (code: -39005, msg: "Failed to decode payload")
27+
PayloadTypeRequiredError* =
28+
(code: -39006, msg: "Payload type is required if payload is specified")
29+
UserSpecifiedPayloadBlockedByClientError* = (
30+
code: -39007,
31+
msg: "The client has blocked users from specifying the payload for this extension",
32+
)
33+
2434
# These errors are used by Fluffy but are not yet in the spec
2535
InvalidContentKeyError* = (code: -32602, msg: "Invalid content key")
2636
InvalidContentValueError* = (code: -32602, msg: "Invalid content value")
2737

38+
template applicationError(error: (int, string)): auto =
39+
(ref ApplicationError)(code: error.code, msg: error.msg)
40+
2841
template contentNotFoundErr*(): auto =
29-
(ref ApplicationError)(code: ContentNotFoundError.code, msg: ContentNotFoundError.msg)
42+
ContentNotFoundError.applicationError()
3043

3144
template contentNotFoundErrWithTrace*(data: typed): auto =
3245
(ref ApplicationError)(
@@ -35,15 +48,30 @@ template contentNotFoundErrWithTrace*(data: typed): auto =
3548
data: data,
3649
)
3750

38-
template invalidKeyErr*(): auto =
39-
(ref errors.InvalidRequest)(
40-
code: InvalidContentKeyError.code, msg: InvalidContentKeyError.msg
51+
template payloadTypeNotSupportedError*(): auto =
52+
(ref ApplicationError)(
53+
code: PayloadTypeNotSupportedError.code,
54+
msg: PayloadTypeNotSupportedError.msg,
55+
data: Opt.some(JsonString("{ \"reason\" : \"client\" }")),
4156
)
4257

58+
template failedToDecodePayloadError*(): auto =
59+
FailedToDecodePayloadError.applicationError()
60+
61+
template payloadTypeRequiredError*(): auto =
62+
PayloadTypeRequiredError.applicationError()
63+
64+
template userSpecifiedPayloadBlockedByClientError*(): auto =
65+
UserSpecifiedPayloadBlockedByClientError.applicationError()
66+
67+
template invalidRequest(error: (int, string)): auto =
68+
(ref errors.InvalidRequest)(code: error.code, msg: error.msg)
69+
70+
template invalidKeyErr*(): auto =
71+
InvalidContentKeyError.invalidRequest()
72+
4373
template invalidValueErr*(): auto =
44-
(ref errors.InvalidRequest)(
45-
code: InvalidContentValueError.code, msg: InvalidContentValueError.msg
46-
)
74+
InvalidContentValueError.invalidRequest()
4775

4876
type
4977
NodeInfo* = object
@@ -54,7 +82,15 @@ type
5482
localNodeId*: NodeId
5583
buckets*: seq[seq[NodeId]]
5684

57-
PingResult* = tuple[enrSeq: uint64, dataRadius: UInt256]
85+
UnknownPayload* = tuple[]
86+
87+
CapabilitiesPayload* =
88+
tuple[clientInfo: string, dataRadius: UInt256, capabilities: seq[uint16]]
89+
90+
PingResult* = object
91+
enrSeq*: uint64
92+
payloadType*: uint16
93+
payload*: CapabilitiesPayload
5894

5995
ContentInfo* = object
6096
content*: string
@@ -68,6 +104,7 @@ type
68104

69105
NodeInfo.useDefaultSerializationIn JrpcConv
70106
RoutingTableInfo.useDefaultSerializationIn JrpcConv
107+
PingResult.useDefaultSerializationIn JrpcConv
71108
(string, string).useDefaultSerializationIn JrpcConv
72109
ContentInfo.useDefaultSerializationIn JrpcConv
73110
PutContentResult.useDefaultSerializationIn JrpcConv
@@ -140,24 +177,30 @@ proc readValue*(
140177
r.raiseUnexpectedValue("seq[byte] parser error: " & exc.msg)
141178

142179
proc writeValue*(
143-
w: var JsonWriter[JrpcConv], v: PingResult
180+
w: var JsonWriter[JrpcConv], v: CapabilitiesPayload
144181
) {.gcsafe, raises: [IOError].} =
145182
w.beginRecord()
146-
w.writeField("enrSeq", v.enrSeq)
183+
184+
w.writeField("clientInfo", v.clientInfo)
147185
# Portal json-rpc specifications allows for dropping leading zeroes.
148186
w.writeField("dataRadius", "0x" & v.dataRadius.toHex())
187+
w.writeField("capabilities", v.capabilities)
188+
149189
w.endRecord()
150190

151191
proc readValue*(
152-
r: var JsonReader[JrpcConv], val: var PingResult
192+
r: var JsonReader[JrpcConv], val: var CapabilitiesPayload
153193
) {.gcsafe, raises: [IOError, SerializationError].} =
154194
try:
155195
for field in r.readObjectFields():
156196
case field
157-
of "enrSeq":
158-
val.enrSeq = r.parseInt(uint64)
197+
of "clientInfo":
198+
val.clientInfo = r.parseString()
159199
of "dataRadius":
160200
val.dataRadius = UInt256.fromHex(r.parseString())
201+
of "capabilities":
202+
r.parseArray:
203+
val.capabilities.add(r.parseInt(uint16))
161204
else:
162205
discard
163206
except ValueError as exc:
@@ -181,3 +224,21 @@ proc readValue*(
181224

182225
if count != value.len():
183226
r.raiseUnexpectedValue("Array length mismatch")
227+
228+
proc readValue*(
229+
r: var JsonReader[JrpcConv], val: var Opt[uint16]
230+
) {.gcsafe, raises: [IOError, SerializationError].} =
231+
if r.tokKind == JsonValueKind.Null:
232+
reset val
233+
r.parseNull()
234+
else:
235+
val.ok r.readValue(uint16)
236+
237+
proc readValue*(
238+
r: var JsonReader[JrpcConv], val: var Opt[UnknownPayload]
239+
) {.gcsafe, raises: [IOError, SerializationError].} =
240+
if r.tokKind == JsonValueKind.Null:
241+
reset val
242+
r.parseNull()
243+
else:
244+
val.ok default(UnknownPayload)

fluffy/tests/wire_protocol_tests/test_portal_wire_protocol.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ procSuite "Portal Wire Protocol Tests":
9292

9393
check pong.isOk()
9494

95-
let (enrSeq, payload) = pong.value()
95+
let (enrSeq, payloadType, payload) = pong.value()
9696
check:
9797
enrSeq == 1'u64
98+
payloadType == 0
9899
payload == customPayload
99100

100101
await proto1.stopPortalProtocol()

0 commit comments

Comments
 (0)