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>
pull/2/head
Michael Connor 6 years ago committed by GitHub
parent 9fc55db3fe
commit ee78db1a86
  1. 11
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/Eth.java
  2. 52
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/EthGetWorkAcceptanceTest.java
  3. 51
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/DirectAcyclicGraphSeed.java
  4. 37
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHash.java
  5. 2
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolver.java
  6. 11
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolverInputs.java
  7. 4
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java
  8. 62
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWork.java
  9. 1
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java
  10. 108
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWorkTest.java

@ -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);
}
}
Loading…
Cancel
Save