Skip to content

Commit

Permalink
feat: implement functionality to make rpc call to other nodes. (#7)
Browse files Browse the repository at this point in the history
- What does this PR do?\
Implements functionality to make RPC calls to other nodes.
- Why are these changes needed?
So that Fruzhin fallbacks to rpc call when populating sync state if
protocols fail.

Fixes LimeChain#494

## Checklist:
- [X] I have read the [contributing
guidelines](https://github.com/LimeChain/Fruzhin/blob/dev/CONTRIBUTING.md).
- [X] My PR title matches the [Conventional Commits
spec](https://www.conventionalcommits.org/).
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [X] All new and existing tests passed.

---------

Co-authored-by: Мurаd Hаmzа <[email protected]>
  • Loading branch information
Zurcusa and ablax authored Aug 30, 2024
1 parent b019740 commit 92481d9
Show file tree
Hide file tree
Showing 32 changed files with 552 additions and 83 deletions.
14 changes: 12 additions & 2 deletions src/main/java/com/limechain/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@

import com.limechain.client.HostNode;
import com.limechain.client.LightClient;
import com.limechain.config.HostConfig;
import com.limechain.rpc.server.AppBean;
import com.limechain.rpc.Function;
import com.limechain.rpc.RpcClient;
import com.limechain.rpc.server.RpcApp;
import com.limechain.utils.DivLogger;
import org.teavm.jso.JSBody;
import org.teavm.jso.core.JSString;

import java.util.logging.Level;

public class Main {

private static final String RPC_VARIABLE_NAME = "rpc";

private static final DivLogger log = new DivLogger();

public static void main(String[] args) {
log.log("Starting LimeChain node...");
exportAPI(RpcClient::sendRpcRequest, JSString.valueOf(RPC_VARIABLE_NAME));

RpcApp rpcApp = new RpcApp();
rpcApp.start();

Expand All @@ -25,4 +31,8 @@ public static void main(String[] args) {
client.start();
log.log(Level.INFO, "\uD83D\uDE80Started light client!");
}

@JSBody(params = {"f", "apiName"}, script = "window[apiName] = f;" +
"isRpcExported = true;")
private static native void exportAPI(Function f, JSString apiName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import lombok.Getter;
import lombok.ToString;

import java.util.Arrays;
import java.util.Map;

/**
Expand Down Expand Up @@ -51,14 +50,14 @@ public static LightSyncState decode(Map<String, String> lightSyncStateMap) {
LightSyncState lightSyncState = new LightSyncState();
byte[] bytes = StringUtils.hexToBytes(header);
lightSyncState.finalizedBlockHeader = new BlockHeaderReader()
.read(new ScaleCodecReader(bytes));
.read(new ScaleCodecReader(bytes));

byte[] bytes1 = StringUtils.hexToBytes(epochChanges);
lightSyncState.epochChanges = new EpochChangesReader()
.read(new ScaleCodecReader(bytes1));
.read(new ScaleCodecReader(bytes1));

lightSyncState.grandpaAuthoritySet = new AuthoritySetReader()
.read(new ScaleCodecReader(StringUtils.hexToBytes(grandpaAuthoritySet)));
.read(new ScaleCodecReader(StringUtils.hexToBytes(grandpaAuthoritySet)));

return lightSyncState;
}
Expand Down
26 changes: 17 additions & 9 deletions src/main/java/com/limechain/client/LightClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
import com.limechain.network.Network;
import com.limechain.rpc.server.AppBean;
import com.limechain.sync.warpsync.WarpSyncMachine;
import com.limechain.utils.DivLogger;
import lombok.SneakyThrows;
import lombok.extern.java.Log;

import java.util.logging.Level;

/**
* Main light client class that starts and stops execution of
* the client and hold references to dependencies
*/
@Log
public class LightClient implements HostNode {
// TODO: Add service dependencies i.e rpc, sync, network, etc.
// TODO: Do we need those as fields here...?
private final Network network;
private WarpSyncMachine warpSyncMachine;

private static final DivLogger log = new DivLogger();

/**
* @implNote the RpcApp is assumed to have been started before constructing the client,
Expand All @@ -33,16 +33,24 @@ public LightClient() {
@SneakyThrows
public void start() {
this.network.start();
while (true) {
WarpSyncMachine warpSyncMachine = AppBean.getBean(WarpSyncMachine.class);

log.log(Level.INFO, "Syncing to latest finalized block state...");

int retryCount = 0;
while (retryCount < 3) {
this.network.updateCurrentSelectedPeer();

if (this.network.getKademliaService().getSuccessfulBootNodes() > 0) {
log.log(Level.INFO, "Node successfully connected to a peer! Sync can start!");
this.warpSyncMachine = AppBean.getBean(WarpSyncMachine.class);
this.warpSyncMachine.start();
warpSyncMachine.setProtocolSync(true);
break;
} else {
retryCount++;
System.out.println("Waiting to retry peer connection...");
Thread.sleep(2000);
}
log.log(Level.INFO, "Waiting for peer connection...");
Thread.sleep(10000);
}

warpSyncMachine.start();
}
}
18 changes: 14 additions & 4 deletions src/main/java/com/limechain/config/HostConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.limechain.utils.DivLogger;
import lombok.Getter;

import java.util.List;
import java.util.logging.Level;

/**
Expand All @@ -19,10 +20,10 @@ public class HostConfig {
// private final NodeRole nodeRole;
private final String rpcNodeAddress;

private String polkadotGenesisPath = "genesis/polkadot.json";
private String kusamaGenesisPath = "genesis/ksmcc3.json";
private String westendGenesisPath = "genesis/westend2.json";
private String localGenesisPath = "genesis/westend-local.json";
private final String polkadotGenesisPath = "genesis/polkadot.json";
private final String kusamaGenesisPath = "genesis/ksmcc3.json";
private final String westendGenesisPath = "genesis/westend2.json";
private final String localGenesisPath = "genesis/westend-local.json";

private static final DivLogger log = new DivLogger();

Expand Down Expand Up @@ -66,4 +67,13 @@ public String getGenesisPath() {
case LOCAL -> localGenesisPath;
};
}

public List<String> getHttpsRpcEndpoints() {
return switch (chain) {
case POLKADOT -> RpcConstants.POLKADOT_HTTPS_RPC;
case KUSAMA -> RpcConstants.KUSAMA_HTTPS_RPC;
case WESTEND -> RpcConstants.WESTEND_HTTPS_RPC;
case LOCAL -> List.of();
};
}
}
12 changes: 3 additions & 9 deletions src/main/java/com/limechain/config/SystemInfo.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
package com.limechain.config;

import com.limechain.chain.Chain;
import com.limechain.storage.block.SyncState;
import com.limechain.utils.DivLogger;
import lombok.Getter;

import java.math.BigInteger;
import java.util.Map;
import java.util.logging.Level;

/**
* Configuration class used to hold and information used by the system rpc methods
*/
@Getter
public class SystemInfo {
// private final String role;
// private final String role;
private final Chain chain;
// private final String hostIdentity;
// private final String hostIdentity;
private String hostName = "Fruzhin";
private String hostVersion = "0.0.1";
private final BigInteger highestBlock;

private static final DivLogger log = new DivLogger();

public SystemInfo(HostConfig hostConfig, SyncState syncState) {
public SystemInfo(HostConfig hostConfig) {
// this.role = network.getNodeRole().name();
this.chain = hostConfig.getChain();
// this.hostIdentity = network.getHost().getPeerId().toString();
this.highestBlock = syncState.getLastFinalizedBlockNumber();
logSystemInfo();
}

Expand All @@ -48,6 +43,5 @@ public void logSystemInfo() {
// log.log(Level.INFO, authEmoji + "Role: " + role);
// log.log(Level.INFO, "Local node identity is: " + hostIdentity);
log.log(Level.INFO, "Operating System: " + System.getProperty("os.name"));
log.log(Level.INFO, "Highest known block at #" + highestBlock);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/limechain/constants/RpcConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,27 @@
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.List;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class RpcConstants {
public static final String POLKADOT_WS_RPC = "wss://rpc.polkadot.io";
public static final String KUSAMA_WS_RPC = "wss://kusama-rpc.polkadot.io";
public static final String WESTEND_WS_RPC = "wss://westend-rpc.polkadot.io";

public static final List<String> POLKADOT_HTTPS_RPC = List.of(
"https://rpc.ibp.network/polkadot",
"https://polkadot-rpc.dwellir.com",
"https://rpc.polkadot.io"
);
public static final List<String> KUSAMA_HTTPS_RPC = List.of(
"https://rpc.ibp.network/kusama",
"https://kusama-rpc.dwellir.com",
"https://kusama-rpc.polkadot.io"
);
public static final List<String> WESTEND_HTTPS_RPC = List.of(
"https://rpc.ibp.network/westend",
"https://westend-rpc.dwellir.com",
"https://westend-rpc.polkadot.io"
);
}
24 changes: 24 additions & 0 deletions src/main/java/com/limechain/rpc/ChainRpcClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.limechain.rpc;

import com.limechain.polkaj.Hash256;
import com.limechain.rpc.dto.ChainGetHeaderResult;
import com.limechain.rpc.dto.RpcMethod;
import com.limechain.rpc.dto.RpcResponse;

import java.util.List;

/**
* An implementation of {@link RpcClient}, which implements RPC calls from the "chain" category.
*/
public final class ChainRpcClient extends RpcClient {

public static Hash256 getLastFinalizedBlockHash() {
RpcResponse response = sendRpcRequest(RpcMethod.CHAIN_GET_FINALIZED_HEAD, List.of());
return Hash256.from(getResult(response, String.class));
}

public static ChainGetHeaderResult getHeader(String blockHash) {
RpcResponse response = sendRpcRequest(RpcMethod.CHAIN_GET_HEADER, List.of(blockHash));
return getResult(response, ChainGetHeaderResult.class);
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/limechain/rpc/Function.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.limechain.rpc;

import org.teavm.jso.JSObject;

/**
* A functional interface used to export rpc functionalities to the user. A function which conforms with "sendRequest"
* signature can be exported via {@link com.limechain.Main}{@code .exportAPI(Function, JSString)}
*/
@FunctionalInterface
public interface Function extends JSObject {

String sendRequest(String method, String[] params);
}
18 changes: 18 additions & 0 deletions src/main/java/com/limechain/rpc/GrandpaRpcClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.limechain.rpc;

import com.limechain.rpc.dto.GrandpaRoundStateResult;
import com.limechain.rpc.dto.RpcMethod;
import com.limechain.rpc.dto.RpcResponse;

import java.util.List;

/**
* An implementation of {@link RpcClient}, which implements RPC calls from the "grandpa" category.
*/
public final class GrandpaRpcClient extends RpcClient {

public static GrandpaRoundStateResult getGrandpaRoundState() {
RpcResponse response = sendRpcRequest(RpcMethod.GRANDPA_ROUND_STATE, List.of());
return getResult(response, GrandpaRoundStateResult.class);
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/limechain/rpc/LoadBalancer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.limechain.rpc;

import com.limechain.config.HostConfig;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
* A simple load balancer that switches between provided endpoints. Each consecutive call takes the latter endpoint
* in the provided list.
*/
public class LoadBalancer {

private final List<String> endpoints;
private final AtomicInteger index;

public LoadBalancer(HostConfig hostConfig) {
this.endpoints = hostConfig.getHttpsRpcEndpoints();
this.index = new AtomicInteger(0);
}

public String getNextEndpoint() {
int currentIndex = index.getAndUpdate(i -> (i + 1) % endpoints.size());
return endpoints.get(currentIndex);
}
}
75 changes: 75 additions & 0 deletions src/main/java/com/limechain/rpc/RpcClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.limechain.rpc;

import com.limechain.config.HostConfig;
import com.limechain.rpc.dto.RpcMethod;
import com.limechain.rpc.dto.RpcRequest;
import com.limechain.rpc.dto.RpcResponse;
import com.limechain.rpc.server.AppBean;
import com.limechain.teavm.HttpRequest;
import com.limechain.utils.json.JsonUtil;
import com.limechain.utils.json.ObjectMapper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
* Base class for executing RPC requests.
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public sealed class RpcClient permits ChainRpcClient, GrandpaRpcClient {

private static final String POST = "POST";
private static final AtomicInteger ID_COUNTER = new AtomicInteger(1);
private static final LoadBalancer LOAD_BALANCER = new LoadBalancer(AppBean.getBean(HostConfig.class));
protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(false);

/**
* Send an RPC request. Currently used only by the exported RPC client.
*
* @param method {@link String} representation of the RPC method name. For example "system_name".
* @param params An array of parameters for the sent RPC request.
* @return The {@link String} representation of the received RPC json result.
*/
public static String sendRpcRequest(String method, Object[] params) {
return HttpRequest.createHttpRequest(POST, LOAD_BALANCER.getNextEndpoint(),
createRpcRequestJson(method, List.of(params)));
}

/**
* Send an RPC request. Used by the specific implementations of the RpcClient.
*
* @param method Enum representation of an RPC method name.
* @param params An array of parameters for the sent RPC request.
* @return The {@link RpcResponse} representation of the received RPC json result.
*/
protected static RpcResponse sendRpcRequest(RpcMethod method, List<Object> params) {
String jsonResult = HttpRequest.asyncHttpRequest(POST, LOAD_BALANCER.getNextEndpoint(),
createRpcRequestJson(method.getMethod(), params));
return OBJECT_MAPPER.mapToClass(jsonResult, RpcResponse.class);
}

private static String createRpcRequestJson(String method, List<Object> params) {
RpcRequest request = new RpcRequest(ID_COUNTER.getAndAdd(1), method, params);
return JsonUtil.stringify(request);
}

/**
* Method used to map an {@link RpcResponse} result to a provided class type. This is needed because TeaVM does not
* support use of {@link java.lang.reflect.ParameterizedType} and we cannot use an object mapper with generics
* inside.
*
* @param response the {@link RpcResponse} whose result we have to map to an object.
* @param klazz the desired class for the mapping.
* @return a mapped version of the response result in the form of the provided {@code klazz} type.
*/
protected static <T> T getResult(RpcResponse response, Class<T> klazz) {
if (response.getError() != null) {
throw new IllegalStateException("RPC request resulted in an error with code:" + response.getError().getCode()
+ " and message:" + response.getError().getMessage());
}

return OBJECT_MAPPER.mapToClass(JsonUtil.stringify(response.getResult()), klazz);
}
}
Loading

0 comments on commit 92481d9

Please sign in to comment.