diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/Eth.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/Eth.java index e33aa2bcc6..057034d90b 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/Eth.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/Eth.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() + }; + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/EthGetWorkAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/EthGetWorkAcceptanceTest.java new file mode 100644 index 0000000000..c2b99bf420 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/EthGetWorkAcceptanceTest.java @@ -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"); + } +} diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/DirectAcyclicGraphSeed.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/DirectAcyclicGraphSeed.java new file mode 100644 index 0000000000..f205fed40f --- /dev/null +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/DirectAcyclicGraphSeed.java @@ -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 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; + } +} diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHash.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHash.java index f1c68d5fe0..7c92640b0f 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHash.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHash.java @@ -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 KECCAK_256 = - ThreadLocal.withInitial( - () -> { - try { - return BouncyCastleMessageDigestFactory.create(Hash.KECCAK256_ALG); - } catch (final NoSuchAlgorithmException ex) { - throw new IllegalStateException(ex); - } - }); - private static final ThreadLocal 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]); diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolver.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolver.java index 225c0fce17..72fd4d183d 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolver.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolver.java @@ -110,7 +110,7 @@ public class EthHashSolver { private Optional 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 = diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolverInputs.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolverInputs.java index 9ae57fe4c6..2f39bb9780 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolverInputs.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/EthHashSolverInputs.java @@ -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; } } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java index a77bfceadb..c74a27072c 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java @@ -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 = diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWork.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWork.java new file mode 100644 index 0000000000..4c7407467a --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWork.java @@ -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 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); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java index a23364030f..397c0a058c 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java @@ -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"), diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWorkTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWorkTest.java new file mode 100644 index 0000000000..fcab8ab01b --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetWorkTest.java @@ -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); + } +}