Skip to content

Commit 500d195

Browse files
committed
Portal finalized history: Add native API and JSON-RPC API
1 parent 71dca54 commit 500d195

File tree

6 files changed

+257
-6
lines changed

6 files changed

+257
-6
lines changed

portal/network/finalized_history/content/content_values.nim

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ import eth/common/blocks_rlp, eth/common/receipts_rlp
1111

1212
export blocks_rlp, receipts_rlp
1313

14-
type Receipts* = seq[Receipt]
14+
type
15+
Receipts* = seq[Receipt]
16+
ContentValueType* = BlockBody | Receipts
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Nimbus
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed and distributed under either of
4+
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
5+
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
6+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
7+
8+
{.push raises: [].}
9+
10+
import results, chronicles, chronos, ./finalized_history_network
11+
12+
export results, finalized_history_network
13+
14+
proc getBlockBody*(
15+
n: FinalizedHistoryNetwork, header: Header
16+
): Future[Opt[BlockBody]] {.async: (raises: [CancelledError], raw: true).} =
17+
n.getContent(blockBodyContentKey(header.number), BlockBody, header)
18+
19+
proc getReceipts*(
20+
n: FinalizedHistoryNetwork, header: Header
21+
): Future[Opt[BlockBody]] {.async: (raises: [CancelledError], raw: true).} =
22+
n.getContent(blockBodyContentKey(header.number), BlockBody, header)

portal/network/finalized_history/finalized_history_network.nim

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import
2222

2323
from eth/common/accounts import EMPTY_ROOT_HASH
2424

25+
export finalized_history_content, headers
26+
2527
logScope:
2628
topics = "portal_fin_hist"
2729

@@ -82,6 +84,60 @@ proc new*(
8284
contentQueueWorkers: contentQueueWorkers,
8385
)
8486

87+
proc getContent*(
88+
n: FinalizedHistoryNetwork,
89+
contentKey: ContentKey,
90+
V: type ContentValueType,
91+
header: Header,
92+
): Future[Opt[V]] {.async: (raises: [CancelledError]).} =
93+
let contentKeyBytes = encode(contentKey)
94+
95+
logScope:
96+
contentKeyBytes
97+
98+
let contentId = contentKeyBytes.toContentId().valueOr:
99+
warn "Received invalid content key", contentKeyBytes
100+
return Opt.none(V)
101+
102+
# Check first locally
103+
n.portalProtocol.getLocalContent(contentKeyBytes, contentId).isErrOr:
104+
let contentValue = decodeRlp(value(), V).valueOr:
105+
raiseAssert("Unable to decode history local content value")
106+
107+
debug "Fetched local content value"
108+
return Opt.some(contentValue)
109+
110+
for i in 0 ..< (1 + n.contentRequestRetries):
111+
let
112+
lookupRes = (await n.portalProtocol.contentLookup(contentKeyBytes, contentId)).valueOr:
113+
warn "Failed fetching content from the network"
114+
return Opt.none(V)
115+
116+
contentValue = decodeRlp(lookupRes.content, V).valueOr:
117+
warn "Unable to decode content value from content lookup"
118+
continue
119+
120+
validateContent(contentValue, header).isOkOr:
121+
n.portalProtocol.banNode(
122+
lookupRes.receivedFrom.id, NodeBanDurationContentLookupFailedValidation
123+
)
124+
warn "Error validating retrieved content", error = error
125+
continue
126+
127+
debug "Fetched valid content from the network"
128+
n.portalProtocol.storeContent(
129+
contentKeyBytes, contentId, lookupRes.content, cacheContent = true
130+
)
131+
132+
asyncSpawn n.portalProtocol.triggerPoke(
133+
lookupRes.nodesInterestedInContent, contentKeyBytes, lookupRes.content
134+
)
135+
136+
return Opt.some(contentValue)
137+
138+
# Content was requested `1 + requestRetries` times and all failed on validation
139+
Opt.none(V)
140+
85141
proc validateContent(
86142
n: FinalizedHistoryNetwork, content: seq[byte], contentKeyBytes: ContentKeyByteList
87143
): Future[Result[void, string]] {.async: (raises: [CancelledError]).} =
@@ -113,7 +169,7 @@ proc validateContent(
113169
header = Header()
114170
receipts = decodeRlp(content, seq[Receipt]).valueOr:
115171
return err("Error decoding receipts: " & error)
116-
validateReceipts(receipts, header.receiptsRoot).isOkOr:
172+
validateReceipts(receipts, header).isOkOr:
117173
return err("Failed validating receipts: " & error)
118174

119175
ok()

portal/network/finalized_history/finalized_history_validation.nim

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
{.push raises: [].}
99

1010
import
11+
std/typetraits,
1112
eth/trie/ordered_trie,
1213
eth/common/[headers_rlp, blocks_rlp, receipts, hashes],
1314
./finalized_history_content
@@ -47,12 +48,34 @@ func validateBlockBody*(body: BlockBody, header: Header): Result[void, string] =
4748

4849
ok()
4950

50-
func validateReceipts*(receipts: Receipts, receiptsRoot: Hash32): Result[void, string] =
51+
func validateReceipts*(receipts: Receipts, header: Header): Result[void, string] =
5152
let calculatedReceiptsRoot = orderedTrieRoot(receipts)
52-
if calculatedReceiptsRoot != receiptsRoot:
53+
if calculatedReceiptsRoot != header.receiptsRoot:
5354
err(
54-
"Unexpected receipt root: expected " & $receiptsRoot & " - got " &
55+
"Unexpected receipt root: expected " & $header.receiptsRoot & " - got " &
5556
$calculatedReceiptsRoot
5657
)
5758
else:
5859
ok()
60+
61+
func validateContent*(
62+
content: BlockBody | Receipts, header: Header
63+
): Result[void, string] =
64+
type T = type(content)
65+
when T is BlockBody:
66+
validateBlockBody(content, header)
67+
elif T is Receipts:
68+
validateReceipts(content, header)
69+
70+
func validateContent*(
71+
key: ContentKey, contentBytes: seq[byte], header: Header
72+
): Result[void, string] =
73+
case key.contentType
74+
of unused:
75+
raiseAssert("ContentKey contentType: unused")
76+
of blockBody:
77+
let content = ?decodeRlp(contentBytes, BlockBody)
78+
validateBlockBody(content, header)
79+
of receipts:
80+
let content = ?decodeRlp(contentBytes, Receipts)
81+
validateReceipts(content, header)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Nimbus
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed and distributed under either of
4+
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
5+
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
6+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
7+
8+
{.push raises: [].}
9+
10+
import
11+
json_rpc/rpcserver,
12+
json_serialization/std/tables,
13+
stew/byteutils,
14+
../common/common_types,
15+
../network/wire/portal_protocol,
16+
../network/finalized_history/finalized_history_content,
17+
../network/finalized_history/finalized_history_validation,
18+
./rpc_types
19+
20+
export tables
21+
22+
# Portal Finalized History Network JSON-RPC API
23+
# Note:
24+
# - This API is not part of the Portal Network specification yet.
25+
# - Lower level API calls are not implemented as they are typically only used for (Hive)
26+
# testing and it is not clear yet of this will be needed in the future.
27+
# - Added the header parameter so that validation can happen on json-rpc server side,
28+
# but it could also be moved to client side.
29+
# - Could also make a less generic API
30+
31+
ContentInfo.useDefaultSerializationIn JrpcConv
32+
TraceContentLookupResult.useDefaultSerializationIn JrpcConv
33+
TraceObject.useDefaultSerializationIn JrpcConv
34+
NodeMetadata.useDefaultSerializationIn JrpcConv
35+
TraceResponse.useDefaultSerializationIn JrpcConv
36+
37+
# TODO: It would be cleaner to use the existing getContent/getBlockBody/getReceipts calls for
38+
# less code duplication + automatic retries, but the specific error messages + extra content
39+
# info would need to be added to the existing calls.
40+
proc installPortalFinalizedHistoryApiHandlers*(
41+
rpcServer: RpcServer, p: PortalProtocol
42+
) =
43+
rpcServer.rpc("portal_finalizedHistoryGetContent") do(
44+
contentKeyBytes: string, headerBytes: string
45+
) -> ContentInfo:
46+
let
47+
contentKeyByteList = ContentKeyByteList.init(hexToSeqByte(contentKeyBytes))
48+
contentKey = decode(contentKeyByteList).valueOr:
49+
raise invalidKeyErr()
50+
contentId = toContentId(contentKey)
51+
header = decodeRlp(hexToSeqByte(headerBytes), Header).valueOr:
52+
raise invalidRequest((code: -39005, msg: "Failed to decode header: " & error))
53+
54+
p.getLocalContent(contentKeyByteList, contentId).isErrOr:
55+
return ContentInfo(content: value.to0xHex(), utpTransfer: false)
56+
57+
let contentLookupResult = (await p.contentLookup(contentKeyByteList, contentId)).valueOr:
58+
raise contentNotFoundErr()
59+
60+
validateContent(contentKey, contentLookupResult.content, header).isOkOr:
61+
p.banNode(
62+
contentLookupResult.receivedFrom.id,
63+
NodeBanDurationContentLookupFailedValidation,
64+
)
65+
raise invalidValueErr()
66+
67+
p.storeContent(
68+
contentKeyByteList, contentId, contentLookupResult.content, cacheContent = true
69+
)
70+
71+
ContentInfo(
72+
content: contentLookupResult.content.to0xHex(),
73+
utpTransfer: contentLookupResult.utpTransfer,
74+
)
75+
76+
rpcServer.rpc("portal_finalizedHistoryTraceGetContent") do(
77+
contentKeyBytes: string, headerBytes: string
78+
) -> TraceContentLookupResult:
79+
let
80+
contentKeyByteList = ContentKeyByteList.init(hexToSeqByte(contentKeyBytes))
81+
contentKey = decode(contentKeyByteList).valueOr:
82+
raise invalidKeyErr()
83+
contentId = toContentId(contentKey)
84+
header = decodeRlp(hexToSeqByte(headerBytes), Header).valueOr:
85+
raise invalidRequest((code: -39005, msg: "Failed to decode header: " & error))
86+
87+
p.getLocalContent(contentKeyByteList, contentId).isErrOr:
88+
return TraceContentLookupResult(
89+
content: Opt.some(value),
90+
utpTransfer: false,
91+
trace: TraceObject(
92+
origin: p.localNode.id,
93+
targetId: contentId,
94+
receivedFrom: Opt.some(p.localNode.id),
95+
),
96+
)
97+
98+
# TODO: Might want to restructure the lookup result here. Potentially doing
99+
# the json conversion in this module.
100+
let
101+
res = await p.traceContentLookup(contentKeyByteList, contentId)
102+
valueBytes = res.content.valueOr:
103+
let data = Opt.some(JrpcConv.encode(res.trace).JsonString)
104+
raise contentNotFoundErrWithTrace(data)
105+
106+
validateContent(contentKey, valueBytes, header).isOkOr:
107+
if res.trace.receivedFrom.isSome():
108+
p.banNode(
109+
res.trace.receivedFrom.get(), NodeBanDurationContentLookupFailedValidation
110+
)
111+
raise invalidValueErr()
112+
113+
p.storeContent(contentKeyByteList, contentId, valueBytes, cacheContent = true)
114+
115+
res
116+
117+
rpcServer.rpc("portal_finalizedHistoryPutContent") do(
118+
contentKeyBytes: string, contentValueBytes: string
119+
) -> PutContentResult:
120+
let
121+
contentKeyByteList = ContentKeyByteList.init(hexToSeqByte(contentKeyBytes))
122+
_ = decode(contentKeyByteList).valueOr:
123+
raise invalidKeyErr()
124+
offerValueBytes = hexToSeqByte(contentValueBytes)
125+
126+
# Note: Not validating content as this would have a high impact on bridge
127+
# gossip performance.
128+
# As no validation is done here, the content is not stored locally.
129+
# TODO: Add default on validation by optional validation parameter.
130+
gossipMetadata = await p.neighborhoodGossip(
131+
Opt.none(NodeId),
132+
ContentKeysList(@[contentKeyByteList]),
133+
@[offerValueBytes],
134+
enableNodeLookup = true,
135+
)
136+
137+
PutContentResult(
138+
storedLocally: false,
139+
peerCount: gossipMetadata.successCount,
140+
acceptMetadata: AcceptMetadata(
141+
acceptedCount: gossipMetadata.acceptedCount,
142+
genericDeclineCount: gossipMetadata.genericDeclineCount,
143+
alreadyStoredCount: gossipMetadata.alreadyStoredCount,
144+
notWithinRadiusCount: gossipMetadata.notWithinRadiusCount,
145+
rateLimitedCount: gossipMetadata.rateLimitedCount,
146+
transferInProgressCount: gossipMetadata.transferInProgressCount,
147+
),
148+
)

portal/rpc/rpc_types.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ template payloadTypeRequiredError*(): auto =
6464
template userSpecifiedPayloadBlockedByClientError*(): auto =
6565
UserSpecifiedPayloadBlockedByClientError.applicationError()
6666

67-
template invalidRequest(error: (int, string)): auto =
67+
template invalidRequest*(error: (int, string)): auto =
6868
(ref errors.InvalidRequest)(code: error.code, msg: error.msg)
6969

7070
template invalidKeyErr*(): auto =

0 commit comments

Comments
 (0)