Skip to content

Commit

Permalink
Implement Child State RPC API (LimeChain#320)
Browse files Browse the repository at this point in the history
* feat: child state rpc methods

* chore: change child_trie default prefix

* refactor: store runtime only when blockstate is used

* chore: set root only if available

* chore: fix spaces
  • Loading branch information
ablax authored Mar 26, 2024
1 parent 23115f5 commit c7940ed
Show file tree
Hide file tree
Showing 14 changed files with 372 additions and 50 deletions.
3 changes: 2 additions & 1 deletion src/main/java/com/limechain/rpc/methods/RPCMethods.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.googlecode.jsonrpc4j.JsonRpcMethod;
import com.googlecode.jsonrpc4j.JsonRpcService;
import com.limechain.rpc.methods.chain.ChainRPC;
import com.limechain.rpc.methods.childstate.ChildStateRPC;
import com.limechain.rpc.methods.offchain.OffchainRPC;
import com.limechain.rpc.methods.state.StateRPC;
import com.limechain.rpc.methods.sync.SyncRPC;
Expand All @@ -16,7 +17,7 @@
* Therefore, as a workaround, we have to combine them into a single interface 🤷
*/
@JsonRpcService("/")
public interface RPCMethods extends SystemRPC, SyncRPC, ChainRPC, OffchainRPC, StateRPC {
public interface RPCMethods extends SystemRPC, SyncRPC, ChainRPC, OffchainRPC, StateRPC, ChildStateRPC {
@JsonRpcMethod("rpc_methods")
String[] rpcMethods();

Expand Down
30 changes: 29 additions & 1 deletion src/main/java/com/limechain/rpc/methods/RPCMethodsImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.limechain.rpc.exceptions.InvalidParametersException;
import com.limechain.rpc.methods.chain.ChainRPC;
import com.limechain.rpc.methods.chain.ChainRPCImpl;
import com.limechain.rpc.methods.childstate.ChildStateRPCImpl;
import com.limechain.rpc.methods.offchain.OffchainRPC;
import com.limechain.rpc.methods.offchain.OffchainRPCImpl;
import com.limechain.rpc.methods.state.StateRPC;
Expand Down Expand Up @@ -62,6 +63,11 @@ public class RPCMethodsImpl implements RPCMethods {
*/
private final StateRPCImpl stateRPC;

/**
* References to child state rpc method implementation classes
*/
private final ChildStateRPCImpl childStateRPC;

@Override
public String[] rpcMethods() {
ArrayList<Method> methods = new ArrayList<>();
Expand Down Expand Up @@ -228,7 +234,7 @@ public String[][] stateGetPairs(final String prefix, final String blockHash) {
}

@Override
public String[][] stateGetKeysPaged(final String prefix, final int limit, final String key, final String blockHash) {
public List<String> stateGetKeysPaged(final String prefix, final int limit, final String key, final String blockHash) {
return stateRPC.stateGetKeysPaged(prefix, limit, key, blockHash);
}

Expand Down Expand Up @@ -277,4 +283,26 @@ public Map<String, Object> stateGetReadProof(final List<String> key, final Strin
return stateRPC.stateGetReadProof(key, blockHash);
}
//endregion

//region ChildStateRPC methods
@Override
public List<String> childStateGetKeys(String childKeyHex, String prefix, String blockHashHex) {
return childStateRPC.childStateGetKeys(prefix, childKeyHex, blockHashHex);
}

@Override
public String childStateGetStorage(String childKeyHex, String keyHex, String blockHashHex) {
return childStateRPC.stateGetStorage(childKeyHex, keyHex, blockHashHex);
}

@Override
public String childStateGetStorageHash(String childKeyHex, String keyHex, String blockHashHex) {
return childStateRPC.stateGetStorageHash(childKeyHex, keyHex, blockHashHex);
}

@Override
public String childStateGetStorageSize(String childKeyHex, String keyHex, String blockHashHex) {
return childStateRPC.stateGetStorageSize(childKeyHex, keyHex, blockHashHex);
}
//endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import com.limechain.storage.block.exception.BlockNotFoundException;
import com.limechain.storage.block.exception.BlockStorageGenericException;
import com.limechain.storage.block.exception.HeaderNotFoundException;
import com.limechain.utils.StringUtils;
import io.emeraldpay.polkaj.types.Hash256;
import lombok.AllArgsConstructor;
import org.apache.tomcat.util.buf.HexUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;

Expand Down Expand Up @@ -41,7 +41,7 @@ public static Map<String, Object> headerToMap(BlockHeader header) {
"logs", Arrays
.stream(header.getDigest())
.map(HeaderDigest::getMessage)
.map(HexUtils::toHexString)
.map(StringUtils::toHexWithPrefix)
.toArray()
),
"extrinsicsRoot", header.getExtrinsicsRoot().toString(),
Expand Down Expand Up @@ -101,7 +101,7 @@ public Map<String, Object> chainGetBlock(String blockHash) {
return Map.of(
"block", Map.of("header", headerToMap(block.getHeader()),
"extrinsics", Arrays.stream(block.getBody().getExtrinsicsAsByteArray())
.map(HexUtils::toHexString)
.map(StringUtils::toHexWithPrefix)
.toArray())
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.limechain.rpc.methods.childstate;

import com.googlecode.jsonrpc4j.JsonRpcMethod;
import com.limechain.rpc.config.UnsafeRpcMethod;

import java.util.List;

public interface ChildStateRPC {

@JsonRpcMethod("childstate_getKeys")
@UnsafeRpcMethod
List<String> childStateGetKeys(final String childKeyHex, final String prefix, final String blockHashHex);

@JsonRpcMethod("childstate_getStorage")
String childStateGetStorage(final String childKeyHex, final String keyHex, final String blockHashHex);

@JsonRpcMethod("childstate_getStorageHash")
String childStateGetStorageHash(final String childKeyHex, final String keyHex, final String blockHashHex);

@JsonRpcMethod("childstate_getStorageSize")
String childStateGetStorageSize(final String childKeyHex, final String keyHex, final String blockHashHex);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.limechain.rpc.methods.childstate;

import com.limechain.storage.block.BlockState;
import com.limechain.storage.trie.TrieStorage;
import com.limechain.trie.structure.database.NodeData;
import com.limechain.utils.StringUtils;
import io.emeraldpay.polkaj.types.Hash256;
import org.springframework.stereotype.Service;

import java.lang.reflect.Array;
import java.util.Collections;
import java.util.List;

@Service
public class ChildStateRPCImpl {

private final TrieStorage trieStorage = TrieStorage.getInstance();
private final BlockState blockState = BlockState.getInstance();

/**
* Retrieves a list of storage keys that match a given prefix within a specific child storage and block.
*
* @param prefixHex The prefix (in hexadecimal format) to match the keys against.
* @param childKeyHex The hexadecimal representation of the child storage key.
* @param blockHashHex The block hash (in hexadecimal format) to scope the search.
* @return A list of matching keys in hexadecimal format. Returns an empty list if the block state is not initialized.
*/
public List<String> childStateGetKeys(String prefixHex, String childKeyHex, String blockHashHex) {
if (!this.blockState.isInitialized()) {
return Collections.emptyList();
}

byte[] prefix = StringUtils.hexToBytes(prefixHex);
byte[] childStorageMerkle = getChildStorageMerkle(childKeyHex, blockHashHex);

return trieStorage
.getKeysWithPrefix(childStorageMerkle, prefix)
.stream().map(StringUtils::toHexWithPrefix)
.toList();
}

/**
* Retrieves the storage value for a specific key within a child storage and block.
*
* @param childKeyHex The hexadecimal representation of the child storage key.
* @param keyHex The key (in hexadecimal format) for which to retrieve the storage value.
* @param blockHashHex The block hash (in hexadecimal format) to scope the search.
* @return The storage value in hexadecimal format,
* or {@code null} if the block state is not initialized or the value does not exist.
*/
public String stateGetStorage(String childKeyHex, String keyHex, String blockHashHex) {
if (!this.blockState.isInitialized()) {
return null;
}
byte[] key = StringUtils.hexToBytes(keyHex);
byte[] childStorageMerkle = getChildStorageMerkle(childKeyHex, blockHashHex);

return trieStorage.getByKeyFromMerkle(childStorageMerkle, new String(key))
.map(NodeData::getValue)
.map(StringUtils::toHexWithPrefix)
.orElse(null);
}

/**
* Retrieves the storage hash for a specific key within a child storage and block.
*
* @param childKeyHex The hexadecimal representation of the child storage key.
* @param keyHex The key (in hexadecimal format) for which to retrieve the storage hash.
* @param blockHashHex The block hash (in hexadecimal format) to scope the search.
* @return The storage hash in hexadecimal format, or {@code null} if the block state is not initialized or the hash does not exist.
*/
public String stateGetStorageHash(String childKeyHex, String keyHex, String blockHashHex) {
if (!this.blockState.isInitialized()) {
return null;
}

byte[] key = StringUtils.hexToBytes(keyHex);
byte[] childStorageMerkle = getChildStorageMerkle(childKeyHex, blockHashHex);

return trieStorage.getByKeyFromMerkle(childStorageMerkle, new String(key))
.map(NodeData::getMerkleValue)
.map(StringUtils::toHexWithPrefix)
.orElse(null);
}

/**
* Retrieves the storage size for a specific key within a child storage and block.
*
* @param childKeyHex The hexadecimal representation of the child storage key.
* @param keyHex The key (in hexadecimal format) for which to retrieve the storage size.
* @param blockHashHex The block hash (in hexadecimal format) to scope the search.
* @return The storage size as a string, or {@code null} if the block state is not initialized or the key does not exist.
*/
public String stateGetStorageSize(String childKeyHex, String keyHex, String blockHashHex) {
if (!this.blockState.isInitialized()) {
return null;
}

byte[] key = StringUtils.hexToBytes(keyHex);
byte[] childStorageMerkle = getChildStorageMerkle(childKeyHex, blockHashHex);

return trieStorage
.getByKeyFromMerkle(childStorageMerkle, new String(key))
.map(NodeData::getValue)
.map(Array::getLength)
.map(String::valueOf)
.orElse(null);
}

private Hash256 getHash256FromHex(String blockHashHex) {
return blockHashHex != null ? Hash256.from(blockHashHex) : blockState.getHighestFinalizedHash();
}

private byte[] getChildStorageMerkle(String childKeyHex, String blockHashHex) {
byte[] childKey = StringUtils.hexToBytes(childKeyHex);
final Hash256 blockHash = getHash256FromHex(blockHashHex);

return getChildMerkle(blockHash, childKey);
}

private byte[] getChildMerkle(Hash256 blockHash, byte[] childKey) {
return trieStorage
.getByKeyFromBlock(blockHash, ":child_storage:default:" + new String(childKey))
.map(NodeData::getValue)
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
import com.limechain.storage.offchain.OffchainStore;
import com.limechain.storage.offchain.StorageKind;
import com.limechain.utils.StringUtils;
import org.apache.tomcat.util.buf.HexUtils;
import org.springframework.stereotype.Service;

@Service
public class OffchainRPCImpl {

private static final String HEX_PREFIX = "0x";
private final OffchainStore persistentStorage;
private final OffchainStore localStorage;

Expand All @@ -19,18 +17,34 @@ public OffchainRPCImpl(final KVRepository<String, Object> db) {
localStorage = new OffchainStore(db, StorageKind.LOCAL);
}

/**
* Sets a value in the specified offchain storage.
* The key is expected to be in hexadecimal format and is converted internally. The value is stored as bytes.
*
* @param storageKind The type of storage (PERSISTENT or LOCAL) where the value is to be stored.
* @param key The key under which the value is stored, provided in hexadecimal format.
* @param value The value to be stored, provided in hexadecimal format.
*/
public void offchainLocalStorageSet(StorageKind storageKind, String key, String value) {
OffchainStore offchainStore = storageByKind(storageKind);
offchainStore.set(new String(StringUtils.hexToBytes(key)), StringUtils.hexToBytes(value));
}

/**
* Retrieves a value from the specified offchain storage.
* The key is expected to be in hexadecimal format and is converted internally to retrieve the corresponding bytes.
*
* @param storageKind The type of storage (PERSISTENT or LOCAL) from which to retrieve the value.
* @param key The key whose value is to be retrieved, provided in hexadecimal format.
* @return The value associated with the key in hexadecimal format, prefixed with "0x", or null if the key does not exist.
*/
public String offchainLocalStorageGet(StorageKind storageKind, String key) {
OffchainStore offchainStore = storageByKind(storageKind);
byte[] bytes = offchainStore.get(new String(StringUtils.hexToBytes(key)));
if (bytes == null) {
return null;
}
return HEX_PREFIX + HexUtils.toHexString(bytes);
return StringUtils.toHexWithPrefix(bytes);
}

private OffchainStore storageByKind(StorageKind kind) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public interface StateRPC {

@JsonRpcMethod("state_getKeysPaged")
@UnsafeRpcMethod
String[][] stateGetKeysPaged(final String prefix, int limit, String keyHex, final String blockHashHex);
List<String> stateGetKeysPaged(final String prefix, int limit, String keyHex, final String blockHashHex);

@JsonRpcMethod("state_getStorage")
String stateGetStorage(final String keyHex, final String blockHashHex);
Expand Down
Loading

0 comments on commit c7940ed

Please sign in to comment.