Skip to content

Commit 77f2d73

Browse files
authored
Merge pull request #98 from meldsun0/portal_historyLookupEnr
lookupEnr endpoint and tests
2 parents 38cf357 + 5cae115 commit 77f2d73

File tree

7 files changed

+366
-0
lines changed

7 files changed

+366
-0
lines changed

Diff for: core/src/main/java/samba/network/history/HistoryJsonRpcRequests.java

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.Optional;
1414

1515
import org.apache.tuweni.bytes.Bytes;
16+
import org.apache.tuweni.units.bigints.UInt256;
1617
import org.ethereum.beacon.discovery.schema.NodeRecord;
1718
import tech.pegasys.teku.infrastructure.async.SafeFuture;
1819

@@ -36,4 +37,6 @@ SafeFuture<Optional<FindContentResult>> findContent(
3637
Optional<String> getLocalContent(ContentKey contentKey);
3738

3839
SafeFuture<Optional<Bytes>> offer(NodeRecord nodeRecord, List<Bytes> content, Offer offer);
40+
41+
Optional<String> lookupEnr(final UInt256 nodeId);
3942
}

Diff for: core/src/main/java/samba/network/history/HistoryNetwork.java

+32
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,38 @@ public SafeFuture<Optional<Bytes>> offer(
270270
.exceptionallyCompose(createDefaultErrorWhenSendingMessage(message.getMessageType()));
271271
}
272272

273+
@Override
274+
public Optional<String> lookupEnr(final UInt256 nodeId) {
275+
if (nodeId.equals(this.discv5Client.getHomeNodeRecord().getNodeId())) {
276+
return Optional.of(this.discv5Client.getHomeNodeRecord().asEnr());
277+
} else {
278+
return this.routingTable
279+
.findNode(nodeId.toBytes())
280+
.flatMap(
281+
nodeRecord ->
282+
Optional.ofNullable(
283+
this.findNodes(nodeRecord, new FindNodes(Set.of(0)))
284+
.thenApply(Optional::get)
285+
.thenApply(nodes -> nodes.getEnrList().stream().findFirst().orElse(null))
286+
.thenApply(
287+
enr ->
288+
Optional.ofNullable(enr)
289+
.map(NodeRecordFactory.DEFAULT::fromEnr)
290+
.orElse(null))
291+
.thenApply(
292+
enr ->
293+
(enr != null && nodeRecord.getSeq().compareTo(enr.getSeq()) >= 0)
294+
? nodeRecord
295+
: enr)
296+
.thenApply(NodeRecord::asEnr)
297+
.exceptionally(
298+
ex ->
299+
this.discv5Client.lookupEnr(nodeId).orElseGet(nodeRecord::asEnr))
300+
.join()))
301+
.or(() -> this.discv5Client.lookupEnr(nodeId));
302+
}
303+
}
304+
273305
@Override
274306
public void addEnr(String enr) {
275307
final NodeRecord nodeRecord = NodeRecordFactory.DEFAULT.fromEnr(enr);

Diff for: core/src/main/java/samba/services/PortalNodeMainService.java

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import samba.services.jsonrpc.methods.history.PortalHistoryGetContent;
3434
import samba.services.jsonrpc.methods.history.PortalHistoryGetEnr;
3535
import samba.services.jsonrpc.methods.history.PortalHistoryLocalContent;
36+
import samba.services.jsonrpc.methods.history.PortalHistoryLookupEnr;
3637
import samba.services.jsonrpc.methods.history.PortalHistoryOffer;
3738
import samba.services.jsonrpc.methods.history.PortalHistoryPing;
3839
import samba.services.jsonrpc.methods.history.PortalHistoryStore;
@@ -164,6 +165,9 @@ private void initJsonRPCService() {
164165
methods.put(
165166
RpcMethod.PORTAL_HISTORY_LOCAL_CONTENT.getMethodName(),
166167
new PortalHistoryLocalContent(this.historyNetwork));
168+
methods.put(
169+
RpcMethod.PORTAL_HISTORY_LOOKUP_ENR.getMethodName(),
170+
new PortalHistoryLookupEnr(this.historyNetwork));
167171

168172
jsonRpcService =
169173
Optional.of(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package samba.services.jsonrpc.methods.history;
2+
3+
import samba.jsonrpc.config.RpcMethod;
4+
import samba.jsonrpc.reponse.JsonRpcMethod;
5+
import samba.jsonrpc.reponse.JsonRpcParameter;
6+
import samba.jsonrpc.reponse.JsonRpcRequestContext;
7+
import samba.jsonrpc.reponse.JsonRpcResponse;
8+
import samba.jsonrpc.reponse.JsonRpcSuccessResponse;
9+
import samba.network.history.HistoryJsonRpcRequests;
10+
import samba.services.jsonrpc.methods.parameters.ParametersUtil;
11+
12+
import java.util.Optional;
13+
14+
import org.apache.tuweni.units.bigints.UInt256;
15+
16+
public class PortalHistoryLookupEnr implements JsonRpcMethod {
17+
18+
private final HistoryJsonRpcRequests historyJsonRpcRequests;
19+
20+
public PortalHistoryLookupEnr(HistoryJsonRpcRequests historyJsonRpcRequests) {
21+
this.historyJsonRpcRequests = historyJsonRpcRequests;
22+
}
23+
24+
@Override
25+
public String getName() {
26+
return RpcMethod.PORTAL_HISTORY_LOOKUP_ENR.getMethodName();
27+
}
28+
29+
@Override
30+
public JsonRpcResponse response(JsonRpcRequestContext requestContext) {
31+
try {
32+
String nodeId = ParametersUtil.parseNodeId(requestContext, 0);
33+
Optional<String> enr = historyJsonRpcRequests.lookupEnr(UInt256.fromHexString(nodeId));
34+
if (enr.isPresent()) {
35+
return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), enr.get());
36+
}
37+
return createJsonRpcInvalidRequestResponse(requestContext);
38+
39+
} catch (JsonRpcParameter.JsonRpcParameterException e) {
40+
return createJsonRpcInvalidRequestResponse(requestContext);
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package samba.services.jsonrpc.methods.parameters;
2+
3+
import samba.jsonrpc.reponse.JsonRpcParameter;
4+
import samba.jsonrpc.reponse.JsonRpcRequestContext;
5+
6+
public class ParametersUtil {
7+
8+
public static String parseNodeId(final JsonRpcRequestContext requestContext, final int index)
9+
throws JsonRpcParameter.JsonRpcParameterException {
10+
String nodeId = requestContext.getRequiredParameter(index, String.class);
11+
if (!InputsValidations.isNodeIdValid(nodeId))
12+
throw new JsonRpcParameter.JsonRpcParameterException(
13+
String.format("Invalid nodeId parameter at index %d", index));
14+
return nodeId.startsWith("0x") ? nodeId.substring(2) : nodeId;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package samba.network.history;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
import static org.mockito.ArgumentMatchers.any;
7+
import static org.mockito.Mockito.mock;
8+
import static org.mockito.Mockito.when;
9+
10+
import samba.TestHelper;
11+
import samba.domain.messages.response.Nodes;
12+
import samba.network.RoutingTable;
13+
import samba.services.discovery.Discv5Client;
14+
import samba.services.utp.UTPManager;
15+
import samba.storage.HistoryDB;
16+
17+
import java.lang.reflect.Field;
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.Optional;
21+
import java.util.concurrent.CompletableFuture;
22+
23+
import org.apache.tuweni.bytes.Bytes;
24+
import org.apache.tuweni.units.bigints.UInt256;
25+
import org.apache.tuweni.units.bigints.UInt64;
26+
import org.ethereum.beacon.discovery.schema.EnrField;
27+
import org.ethereum.beacon.discovery.schema.NodeRecord;
28+
import org.ethereum.beacon.discovery.schema.NodeRecordFactory;
29+
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Test;
31+
import org.junit.platform.commons.util.ReflectionUtils;
32+
33+
public class LookupEnrTest {
34+
35+
private HistoryJsonRpcRequests historyNetwork;
36+
private NodeRecord nodeRecord;
37+
private RoutingTable routingTable;
38+
private Discv5Client discv5Client;
39+
40+
@BeforeEach
41+
public void setUp() throws IllegalAccessException {
42+
this.discv5Client = mock(Discv5Client.class);
43+
HistoryDB historyDB = mock(HistoryDB.class);
44+
when(historyDB.isAvailable()).thenReturn(true);
45+
this.historyNetwork = new HistoryNetwork(discv5Client, historyDB, mock(UTPManager.class));
46+
this.nodeRecord = TestHelper.createNodeRecord();
47+
this.routingTable = this.mockRoutingTable();
48+
}
49+
50+
@Test
51+
public void testLookupEnrIsSameAsHomeNodeRecord() {
52+
when(this.discv5Client.getHomeNodeRecord()).thenReturn(this.nodeRecord);
53+
Optional<String> enr =
54+
this.historyNetwork.lookupEnr(
55+
UInt256.fromHexString(this.nodeRecord.getNodeId().toHexString()));
56+
assertNotNull(enr);
57+
assertTrue(enr.isPresent());
58+
assertEquals(this.nodeRecord.asEnr(), enr.get());
59+
}
60+
61+
@Test
62+
public void testLookupEnrIsNotFoundOnRoutingTableSoShouldReturnFromDiscv5RoutingTable() {
63+
UInt256 nodeId = UInt256.fromHexString(this.nodeRecord.getNodeId().toHexString());
64+
when(this.discv5Client.getHomeNodeRecord()).thenReturn(TestHelper.createNodeRecord());
65+
when(this.discv5Client.lookupEnr(nodeId)).thenReturn(Optional.of(this.nodeRecord.asEnr()));
66+
67+
Optional<String> enr = this.historyNetwork.lookupEnr(nodeId);
68+
assertNotNull(enr);
69+
assertTrue(enr.isPresent());
70+
assertEquals(this.nodeRecord.asEnr(), enr.get());
71+
}
72+
73+
@Test
74+
public void testLookupEnrFoundOnRoutingTableSoFindNodeIsCalledAndReturnWithHigherSeq()
75+
throws IllegalAccessException, NoSuchFieldException {
76+
UInt256 nodeId = UInt256.fromHexString(this.nodeRecord.getNodeId().toHexString());
77+
UInt64 originalSeq = this.nodeRecord.getSeq();
78+
NodeRecord updatedNodeRecord = this.incrementSeq(this.nodeRecord, originalSeq.add(1));
79+
when(this.discv5Client.getHomeNodeRecord()).thenReturn(TestHelper.createNodeRecord());
80+
when(this.routingTable.findNode(any(Bytes.class))).thenReturn(Optional.of(this.nodeRecord));
81+
whenFindNodes(List.of(updatedNodeRecord.asBase64()));
82+
83+
Optional<String> enr = this.historyNetwork.lookupEnr(nodeId);
84+
assertNotNull(enr);
85+
assertTrue(enr.isPresent());
86+
assertEquals(updatedNodeRecord.asEnr(), enr.get());
87+
assertEquals(updatedNodeRecord.getSeq(), originalSeq.add(1));
88+
}
89+
90+
@Test
91+
public void
92+
testLookupEnrFoundOnRoutingTableSoFindNodeIsCalledAndReturnWithSameSeqSoOriginalNodeRecordMustBeAnswered()
93+
throws IllegalAccessException, NoSuchFieldException {
94+
UInt256 nodeId = UInt256.fromHexString(this.nodeRecord.getNodeId().toHexString());
95+
96+
NodeRecord updatedNodeRecord = this.incrementSeq(this.nodeRecord, this.nodeRecord.getSeq());
97+
when(this.discv5Client.getHomeNodeRecord()).thenReturn(TestHelper.createNodeRecord());
98+
when(this.routingTable.findNode(any(Bytes.class))).thenReturn(Optional.of(this.nodeRecord));
99+
whenFindNodes(List.of(updatedNodeRecord.asBase64()));
100+
101+
Optional<String> enr = this.historyNetwork.lookupEnr(nodeId);
102+
assertNotNull(enr);
103+
assertTrue(enr.isPresent());
104+
assertEquals(this.nodeRecord.asEnr(), enr.get());
105+
}
106+
107+
@Test
108+
public void testLookupEnrFoundOnRoutingTableSoFindNodeIsCalledButReturnEmptyListSoDicv5IsCalled()
109+
throws IllegalAccessException, NoSuchFieldException {
110+
UInt256 nodeId = UInt256.fromHexString(this.nodeRecord.getNodeId().toHexString());
111+
112+
when(this.discv5Client.getHomeNodeRecord()).thenReturn(TestHelper.createNodeRecord());
113+
when(this.routingTable.findNode(any(Bytes.class))).thenReturn(Optional.of(this.nodeRecord));
114+
whenFindNodes(List.of());
115+
when(this.discv5Client.lookupEnr(nodeId)).thenReturn(Optional.of(this.nodeRecord.asEnr()));
116+
117+
Optional<String> enr = this.historyNetwork.lookupEnr(nodeId);
118+
assertNotNull(enr);
119+
assertTrue(enr.isPresent());
120+
assertEquals(this.nodeRecord.asEnr(), enr.get());
121+
}
122+
123+
@Test
124+
public void
125+
testLookupEnrFoundOnRoutingTableSoFindNodeIsCalledButReturnEmptyListSoDicv5IsCalledWithError()
126+
throws IllegalAccessException, NoSuchFieldException {
127+
UInt256 nodeId = UInt256.fromHexString(this.nodeRecord.getNodeId().toHexString());
128+
129+
when(this.discv5Client.getHomeNodeRecord()).thenReturn(TestHelper.createNodeRecord());
130+
when(this.routingTable.findNode(any(Bytes.class))).thenReturn(Optional.of(this.nodeRecord));
131+
whenFindNodes(List.of());
132+
when(this.discv5Client.lookupEnr(nodeId)).thenReturn(Optional.empty());
133+
134+
Optional<String> enr = this.historyNetwork.lookupEnr(nodeId);
135+
assertNotNull(enr);
136+
assertTrue(enr.isPresent());
137+
assertEquals(this.nodeRecord.asEnr(), enr.get());
138+
}
139+
140+
@Test
141+
public void testLookupEnrFoundOnRoutingTableWhenFindNodesThrowAnException()
142+
throws IllegalAccessException, NoSuchFieldException {
143+
UInt256 nodeId = UInt256.fromHexString(this.nodeRecord.getNodeId().toHexString());
144+
145+
when(this.discv5Client.getHomeNodeRecord()).thenReturn(TestHelper.createNodeRecord());
146+
when(this.routingTable.findNode(any(Bytes.class))).thenReturn(Optional.of(this.nodeRecord));
147+
when(this.discv5Client.sendDisv5Message(
148+
any(NodeRecord.class), any(Bytes.class), any(Bytes.class)))
149+
.thenAnswer(invocation -> CompletableFuture.failedFuture(new RuntimeException()));
150+
151+
when(this.discv5Client.lookupEnr(nodeId)).thenReturn(Optional.of(this.nodeRecord.asEnr()));
152+
153+
Optional<String> enr = this.historyNetwork.lookupEnr(nodeId);
154+
assertNotNull(enr);
155+
assertTrue(enr.isPresent());
156+
assertEquals(this.nodeRecord.asEnr(), enr.get());
157+
}
158+
159+
private void whenFindNodes(List<String> enrs) {
160+
when(this.discv5Client.sendDisv5Message(
161+
any(NodeRecord.class), any(Bytes.class), any(Bytes.class)))
162+
.thenAnswer(
163+
invocation -> CompletableFuture.completedFuture((new Nodes(enrs)).getSszBytes()));
164+
}
165+
166+
private RoutingTable mockRoutingTable() throws IllegalAccessException {
167+
RoutingTable mockedRoutingTable = mock(RoutingTable.class);
168+
Field field =
169+
ReflectionUtils.findFields(
170+
HistoryNetwork.class,
171+
f -> f.getName().equals("routingTable"),
172+
ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
173+
.get(0);
174+
field.setAccessible(true);
175+
field.set(historyNetwork, mockedRoutingTable);
176+
return mockedRoutingTable;
177+
}
178+
179+
private NodeRecord incrementSeq(NodeRecord nodeRecord, UInt64 newSeq) {
180+
List<EnrField> enrFields = new ArrayList<>();
181+
nodeRecord.forEachField(
182+
(fieldName, fieldValue) -> enrFields.add(new EnrField(fieldName, fieldValue)));
183+
return NodeRecordFactory.DEFAULT.createFromValues(newSeq, enrFields);
184+
}
185+
}

0 commit comments

Comments
 (0)