mirror of
https://github.com/vacp2p/status-linea-besu.git
synced 2026-01-08 15:03:52 -05:00
NC-1244 Implement JSON-RPC method "eth_getWork" (#70)
* EthGetWork Added with Unit and acceptance tests * EthGetWork Added with Unit and acceptance tests * EthGetWork Added with Unit and acceptance tests * EthGetWork Added with Unit and acceptance tests Debugged * EthGetWork Added with Unit and acceptance tests Debugged Change Requests Actioned Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
This commit is contained in:
@@ -19,6 +19,7 @@ import java.math.BigInteger;
|
||||
|
||||
import org.web3j.protocol.Web3j;
|
||||
import org.web3j.protocol.core.methods.response.EthBlockNumber;
|
||||
import org.web3j.protocol.core.methods.response.EthGetWork;
|
||||
|
||||
public class Eth {
|
||||
|
||||
@@ -34,4 +35,14 @@ public class Eth {
|
||||
assertThat(result.hasError()).isFalse();
|
||||
return result.getBlockNumber();
|
||||
}
|
||||
|
||||
public String[] getWork() throws IOException {
|
||||
final EthGetWork result = web3j.ethGetWork().send();
|
||||
assertThat(result).isNotNull();
|
||||
return new String[] {
|
||||
result.getCurrentBlockHeaderPowHash(),
|
||||
result.getSeedHashForDag(),
|
||||
result.getBoundaryCondition()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2018 ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
package tech.pegasys.pantheon.tests.acceptance.jsonrpc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
import static tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode;
|
||||
import static tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode;
|
||||
|
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
|
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.web3j.protocol.exceptions.ClientConnectionException;
|
||||
|
||||
public class EthGetWorkAcceptanceTest extends AcceptanceTestBase {
|
||||
|
||||
private PantheonNode minerNode;
|
||||
private PantheonNode fullNode;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
minerNode = cluster.create(pantheonMinerNode("node1"));
|
||||
fullNode = cluster.create(pantheonNode("node2"));
|
||||
cluster.start(minerNode, fullNode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnSuccessResponseWhenMining() throws Exception {
|
||||
String[] response = minerNode.eth().getWork();
|
||||
assertThat(response).hasSize(3);
|
||||
assertThat(response).doesNotContainNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnErrorResponseWhenNotMining() {
|
||||
Throwable thrown = catchThrowable(() -> fullNode.eth().getWork());
|
||||
assertThat(thrown).isInstanceOf(ClientConnectionException.class);
|
||||
assertThat(thrown.getMessage()).contains("No mining work available yet");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2018 ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
package tech.pegasys.pantheon.ethereum.mainnet;
|
||||
|
||||
import static tech.pegasys.pantheon.ethereum.mainnet.EthHash.EPOCH_LENGTH;
|
||||
|
||||
import tech.pegasys.pantheon.crypto.BouncyCastleMessageDigestFactory;
|
||||
import tech.pegasys.pantheon.crypto.Hash;
|
||||
|
||||
import java.security.DigestException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class DirectAcyclicGraphSeed {
|
||||
|
||||
public static final ThreadLocal<MessageDigest> KECCAK_256 =
|
||||
ThreadLocal.withInitial(
|
||||
() -> {
|
||||
try {
|
||||
return BouncyCastleMessageDigestFactory.create(Hash.KECCAK256_ALG);
|
||||
} catch (final NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
public static byte[] dagSeed(final long block) {
|
||||
final byte[] seed = new byte[32];
|
||||
if (Long.compareUnsigned(block, EPOCH_LENGTH) >= 0) {
|
||||
final MessageDigest keccak256 = KECCAK_256.get();
|
||||
for (int i = 0; i < Long.divideUnsigned(block, EPOCH_LENGTH); ++i) {
|
||||
keccak256.update(seed);
|
||||
try {
|
||||
keccak256.digest(seed, 0, seed.length);
|
||||
} catch (final DigestException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@
|
||||
*/
|
||||
package tech.pegasys.pantheon.ethereum.mainnet;
|
||||
|
||||
import tech.pegasys.pantheon.crypto.BouncyCastleMessageDigestFactory;
|
||||
import tech.pegasys.pantheon.crypto.Hash;
|
||||
import tech.pegasys.pantheon.ethereum.core.SealableBlockHeader;
|
||||
import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput;
|
||||
|
||||
@@ -22,7 +20,6 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.DigestException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
@@ -36,7 +33,7 @@ public final class EthHash {
|
||||
|
||||
public static final BigInteger TARGET_UPPER_BOUND = BigInteger.valueOf(2).pow(256);
|
||||
|
||||
private static final int EPOCH_LENGTH = 30000;
|
||||
public static final int EPOCH_LENGTH = 30000;
|
||||
|
||||
private static final int DATASET_INIT_BYTES = 1 << 30;
|
||||
|
||||
@@ -58,16 +55,6 @@ public final class EthHash {
|
||||
|
||||
private static final int ACCESSES = 64;
|
||||
|
||||
private static final ThreadLocal<MessageDigest> KECCAK_256 =
|
||||
ThreadLocal.withInitial(
|
||||
() -> {
|
||||
try {
|
||||
return BouncyCastleMessageDigestFactory.create(Hash.KECCAK256_ALG);
|
||||
} catch (final NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
private static final ThreadLocal<MessageDigest> KECCAK_512 =
|
||||
ThreadLocal.withInitial(Keccak.Digest512::new);
|
||||
|
||||
@@ -123,7 +110,7 @@ public final class EthHash {
|
||||
}
|
||||
final byte[] result = new byte[32 + 32];
|
||||
intToByte(result, cmix);
|
||||
final MessageDigest keccak256 = KECCAK_256.get();
|
||||
final MessageDigest keccak256 = DirectAcyclicGraphSeed.KECCAK_256.get();
|
||||
keccak256.update(seed);
|
||||
keccak256.update(result, 0, 32);
|
||||
try {
|
||||
@@ -190,7 +177,7 @@ public final class EthHash {
|
||||
out.writeLongScalar(header.getTimestamp());
|
||||
out.writeBytesValue(header.getExtraData());
|
||||
out.endList();
|
||||
return KECCAK_256.get().digest(out.encoded().extractArray());
|
||||
return DirectAcyclicGraphSeed.KECCAK_256.get().digest(out.encoded().extractArray());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,7 +199,7 @@ public final class EthHash {
|
||||
*/
|
||||
public static int[] mkCache(final int cacheSize, final long block) {
|
||||
final MessageDigest keccak512 = KECCAK_512.get();
|
||||
keccak512.update(seed(block));
|
||||
keccak512.update(DirectAcyclicGraphSeed.dagSeed(block));
|
||||
final int rows = cacheSize / HASH_BYTES;
|
||||
final byte[] cache = new byte[rows * HASH_BYTES];
|
||||
try {
|
||||
@@ -295,22 +282,6 @@ public final class EthHash {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte[] seed(final long block) {
|
||||
final byte[] seed = new byte[32];
|
||||
if (Long.compareUnsigned(block, EPOCH_LENGTH) >= 0) {
|
||||
final MessageDigest keccak256 = KECCAK_256.get();
|
||||
for (int i = 0; i < Long.divideUnsigned(block, EPOCH_LENGTH); ++i) {
|
||||
keccak256.update(seed);
|
||||
try {
|
||||
keccak256.digest(seed, 0, seed.length);
|
||||
} catch (final DigestException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
|
||||
private static int readLittleEndianInt(final byte[] buffer, final int offset) {
|
||||
return Ints.fromBytes(
|
||||
buffer[offset + 3], buffer[offset + 2], buffer[offset + 1], buffer[offset]);
|
||||
|
||||
@@ -110,7 +110,7 @@ public class EthHashSolver {
|
||||
|
||||
private Optional<EthHashSolution> testNonce(
|
||||
final EthHashSolverInputs inputs, final long nonce, final byte[] hashBuffer) {
|
||||
ethHasher.hash(hashBuffer, nonce, inputs.getDagSeed(), inputs.getPrePowHash());
|
||||
ethHasher.hash(hashBuffer, nonce, inputs.getBlockNumber(), inputs.getPrePowHash());
|
||||
final UInt256 x = UInt256.wrap(Bytes32.wrap(hashBuffer, 32));
|
||||
if (x.compareTo(inputs.getTarget()) <= 0) {
|
||||
final Hash mixedHash =
|
||||
|
||||
@@ -17,12 +17,13 @@ import tech.pegasys.pantheon.util.uint.UInt256;
|
||||
public class EthHashSolverInputs {
|
||||
private final UInt256 target;
|
||||
private final byte[] prePowHash;
|
||||
private final long dagSeed; // typically block number
|
||||
private final long blockNumber;
|
||||
|
||||
public EthHashSolverInputs(final UInt256 target, final byte[] prePowHash, final long dagSeed) {
|
||||
public EthHashSolverInputs(
|
||||
final UInt256 target, final byte[] prePowHash, final long blockNumber) {
|
||||
this.target = target;
|
||||
this.prePowHash = prePowHash;
|
||||
this.dagSeed = dagSeed;
|
||||
this.blockNumber = blockNumber;
|
||||
}
|
||||
|
||||
public UInt256 getTarget() {
|
||||
@@ -33,7 +34,7 @@ public class EthHashSolverInputs {
|
||||
return prePowHash;
|
||||
}
|
||||
|
||||
public long getDagSeed() {
|
||||
return dagSeed;
|
||||
public long getBlockNumber() {
|
||||
return blockNumber;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleByBloc
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleByBlockNumberAndIndex;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleCountByBlockHash;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleCountByBlockNumber;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetWork;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthMining;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthNewBlockFilter;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthNewFilter;
|
||||
@@ -176,7 +177,8 @@ public class JsonRpcMethodsFactory {
|
||||
new EthMining<>(miningCoordinator),
|
||||
new EthCoinbase(miningCoordinator),
|
||||
new EthProtocolVersion(supportedCapabilities),
|
||||
new EthGasPrice<>(miningCoordinator));
|
||||
new EthGasPrice<>(miningCoordinator),
|
||||
new EthGetWork(miningCoordinator));
|
||||
}
|
||||
if (rpcApis.contains(RpcApis.DEBUG)) {
|
||||
final BlockReplay blockReplay =
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2018 ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import tech.pegasys.pantheon.ethereum.blockcreation.AbstractMiningCoordinator;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import tech.pegasys.pantheon.ethereum.mainnet.DirectAcyclicGraphSeed;
|
||||
import tech.pegasys.pantheon.ethereum.mainnet.EthHashSolverInputs;
|
||||
|
||||
import java.util.Optional;
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class EthGetWork implements JsonRpcMethod {
|
||||
|
||||
private final AbstractMiningCoordinator<?, ?> miner;
|
||||
private static final Logger LOG = getLogger();
|
||||
|
||||
public EthGetWork(final AbstractMiningCoordinator<?, ?> miner) {
|
||||
this.miner = miner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "eth_getWork";
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonRpcResponse response(final JsonRpcRequest req) {
|
||||
Optional<EthHashSolverInputs> solver = miner.getWorkDefinition();
|
||||
if (solver.isPresent()) {
|
||||
EthHashSolverInputs rawResult = solver.get();
|
||||
byte[] dagSeed = DirectAcyclicGraphSeed.dagSeed(rawResult.getBlockNumber());
|
||||
String[] result = {
|
||||
"0x" + DatatypeConverter.printHexBinary(rawResult.getPrePowHash()).toLowerCase(),
|
||||
"0x" + DatatypeConverter.printHexBinary(dagSeed).toLowerCase(),
|
||||
rawResult.getTarget().toHexString()
|
||||
};
|
||||
return new JsonRpcSuccessResponse(req.getId(), result);
|
||||
} else {
|
||||
LOG.trace("Mining is not operational, eth_getWork request cannot be processed");
|
||||
return new JsonRpcErrorResponse(req.getId(), JsonRpcError.NO_MINING_WORK_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public enum JsonRpcError {
|
||||
// Filter & Subscription Errors
|
||||
FILTER_NOT_FOUND(-32000, "Filter not found"),
|
||||
SUBSCRIPTION_NOT_FOUND(-32000, "Subscription not found"),
|
||||
NO_MINING_WORK_FOUND(-32000, "No mining work available yet"),
|
||||
|
||||
// Transaction validation failures
|
||||
NONCE_TOO_LOW(-32001, "Nonce too low"),
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2018 ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import tech.pegasys.pantheon.ethereum.blockcreation.EthHashMiningCoordinator;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import tech.pegasys.pantheon.ethereum.mainnet.DirectAcyclicGraphSeed;
|
||||
import tech.pegasys.pantheon.ethereum.mainnet.EthHashSolverInputs;
|
||||
import tech.pegasys.pantheon.util.uint.UInt256;
|
||||
|
||||
import java.util.Optional;
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class EthGetWorkTest {
|
||||
|
||||
private EthGetWork method;
|
||||
private final String ETH_METHOD = "eth_getWork";
|
||||
private final String hexValue =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
||||
|
||||
@Mock private EthHashMiningCoordinator miningCoordinator;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
method = new EthGetWork(miningCoordinator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnCorrectMethodName() {
|
||||
assertThat(method.getName()).isEqualTo(ETH_METHOD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnCorrectResultOnGenesisDAG() {
|
||||
final JsonRpcRequest request = requestWithParams();
|
||||
final EthHashSolverInputs values =
|
||||
new EthHashSolverInputs(
|
||||
UInt256.fromHexString(hexValue), DatatypeConverter.parseHexBinary(hexValue), 0);
|
||||
final String[] expectedValue = {
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
};
|
||||
final JsonRpcResponse expectedResponse =
|
||||
new JsonRpcSuccessResponse(request.getId(), expectedValue);
|
||||
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(values));
|
||||
|
||||
JsonRpcResponse actualResponse = method.response(request);
|
||||
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnCorrectResultOnHighBlockSeed() {
|
||||
final JsonRpcRequest request = requestWithParams();
|
||||
final EthHashSolverInputs values =
|
||||
new EthHashSolverInputs(
|
||||
UInt256.fromHexString(hexValue), DatatypeConverter.parseHexBinary(hexValue), 30000);
|
||||
final String[] expectedValue = {
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"0x" + DatatypeConverter.printHexBinary(DirectAcyclicGraphSeed.dagSeed(30000)).toLowerCase(),
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
};
|
||||
final JsonRpcResponse expectedResponse =
|
||||
new JsonRpcSuccessResponse(request.getId(), expectedValue);
|
||||
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(values));
|
||||
JsonRpcResponse actualResponse = method.response(request);
|
||||
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnErrorOnNoneMiningNode() {
|
||||
final JsonRpcRequest request = requestWithParams();
|
||||
final JsonRpcResponse expectedResponse =
|
||||
new JsonRpcErrorResponse(request.getId(), JsonRpcError.NO_MINING_WORK_FOUND);
|
||||
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.empty());
|
||||
|
||||
JsonRpcResponse actualResponse = method.response(request);
|
||||
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
|
||||
}
|
||||
|
||||
private JsonRpcRequest requestWithParams(final Object... params) {
|
||||
return new JsonRpcRequest("2.0", ETH_METHOD, params);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user