Stratum Server Support (#140)

Add support for external GPU mining via the stratum protocol.  

Three new CLI Options support this: `--miner-stratum-enabled`, 
`--miner-stratum-host`, and `--miner-stratum-port`.

To use stratum first use the `--miner-enabled` option and add the 
`--miner-stratum-enabled` option. This disables local CPU mining and opens up 
a stratum server, configurable via `--miner-stratum-host` (default is 
`0.0.0.0`) and `--miner-stratum-port` (default is 8008).  This server supports 
`stratum+tcp` mining and the JSON-RPC services (if enabled) will support the 
`eth_getWork` and `eth_submitWork` calls as well (supporting `getwork` or 
`http` schemes).

This is known to work with ethminer.

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>
pull/182/head
Antoine Toulme 5 years ago committed by Danno Ferrin
parent dd46332530
commit 55e24759b7
  1. 7
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java
  2. 1
      besu/build.gradle
  3. 6
      besu/src/main/java/org/hyperledger/besu/Runner.java
  4. 16
      besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
  5. 47
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  6. 8
      besu/src/main/java/org/hyperledger/besu/controller/BesuController.java
  7. 1
      besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java
  8. 1
      besu/src/main/java/org/hyperledger/besu/controller/MainnetBesuControllerBuilder.java
  9. 2
      besu/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java
  10. 32
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  11. 3
      besu/src/test/resources/everything_config.toml
  12. 5
      consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java
  13. 30
      consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java
  14. 1
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java
  15. 70
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWork.java
  16. 2
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/EthJsonRpcMethods.java
  17. 132
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWorkTest.java
  18. 11
      ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMinerExecutor.java
  19. 12
      ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinator.java
  20. 22
      ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutor.java
  21. 4
      ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinator.java
  22. 3
      ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/MiningCoordinator.java
  23. 12
      ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinatorTest.java
  24. 13
      ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashBlockCreatorTest.java
  25. 2
      ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutorTest.java
  26. 2
      ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinatorTest.java
  27. 39
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/EthHashObserver.java
  28. 98
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningParameters.java
  29. 20
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolution.java
  30. 29
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolver.java
  31. 14
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverInputs.java
  32. 10
      ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverTest.java
  33. 5
      ethereum/retesteth/src/main/java/org/hyperledger/besu/ethereum/retesteth/RetestethContext.java
  34. 45
      ethereum/stratum/build.gradle
  35. 137
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1EthProxyProtocol.java
  36. 208
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1Protocol.java
  37. 105
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumConnection.java
  38. 62
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumProtocol.java
  39. 130
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServer.java
  40. 23
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServerException.java
  41. 118
      ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/StratumConnectionTest.java
  42. 1
      settings.gradle

@ -86,6 +86,13 @@ public class ProcessBesuNodeRunner implements BesuNodeRunner {
params.add("--miner-enabled");
params.add("--miner-coinbase");
params.add(node.getMiningParameters().getCoinbase().get().toString());
params.add("--miner-stratum-port");
params.add(Integer.toString(node.getMiningParameters().getStratumPort()));
params.add("--miner-stratum-host");
params.add(node.getMiningParameters().getStratumNetworkInterface());
}
if (node.getMiningParameters().isStratumMiningEnabled()) {
params.add("--miner-stratum-enabled");
}
if (node.getPrivacyParameters().isEnabled()) {

@ -44,6 +44,7 @@ dependencies {
implementation project(':ethereum:p2p')
implementation project(':ethereum:retesteth')
implementation project(':ethereum:rlp')
implementation project(':ethereum:stratum')
implementation project(':metrics:core')
implementation project(':nat')
implementation project(':plugin-api')

@ -20,6 +20,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcHttpService;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketService;
import org.hyperledger.besu.ethereum.p2p.network.NetworkRunner;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.stratum.StratumServer;
import org.hyperledger.besu.metrics.prometheus.MetricsService;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
@ -56,6 +57,7 @@ public class Runner implements AutoCloseable {
private final BesuController<?> besuController;
private final Path dataDir;
private final Optional<StratumServer> stratumServer;
Runner(
final Vertx vertx,
@ -64,6 +66,7 @@ public class Runner implements AutoCloseable {
final Optional<JsonRpcHttpService> jsonRpc,
final Optional<GraphQLHttpService> graphQLHttp,
final Optional<WebSocketService> websocketRpc,
final Optional<StratumServer> stratumServer,
final Optional<MetricsService> metrics,
final BesuController<?> besuController,
final Path dataDir) {
@ -76,6 +79,7 @@ public class Runner implements AutoCloseable {
this.metrics = metrics;
this.besuController = besuController;
this.dataDir = dataDir;
this.stratumServer = stratumServer;
}
public void start() {
@ -87,6 +91,7 @@ public class Runner implements AutoCloseable {
besuController.getSynchronizer().start();
}
besuController.getMiningCoordinator().start();
stratumServer.ifPresent(server -> waitForServiceToStart("stratum", server.start()));
vertx.setPeriodic(
TimeUnit.MINUTES.toMillis(1),
time ->
@ -111,6 +116,7 @@ public class Runner implements AutoCloseable {
besuController.getMiningCoordinator().stop();
waitForServiceToStop("Mining Coordinator", besuController.getMiningCoordinator()::awaitStop);
stratumServer.ifPresent(server -> waitForServiceToStop("Stratum", server::stop));
if (networkRunner.getNetwork().isP2pEnabled()) {
besuController.getSynchronizer().stop();
waitForServiceToStop("Synchronizer", besuController.getSynchronizer()::awaitStop);

@ -50,6 +50,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.syncing.
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
@ -79,6 +80,7 @@ import org.hyperledger.besu.ethereum.permissioning.account.AccountPermissioningC
import org.hyperledger.besu.ethereum.permissioning.node.InsufficientPeersPermissioningProvider;
import org.hyperledger.besu.ethereum.permissioning.node.NodePermissioningController;
import org.hyperledger.besu.ethereum.permissioning.node.PeerPermissionsAdapter;
import org.hyperledger.besu.ethereum.stratum.StratumServer;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulator;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
@ -360,6 +362,19 @@ public class RunnerBuilder {
final P2PNetwork peerNetwork = networkRunner.getNetwork();
final MiningParameters miningParameters = besuController.getMiningParameters();
Optional<StratumServer> stratumServer = Optional.empty();
if (miningParameters.isStratumMiningEnabled()) {
stratumServer =
Optional.of(
new StratumServer(
vertx,
miningParameters.getStratumPort(),
miningParameters.getStratumNetworkInterface(),
miningParameters.getStratumExtranonce()));
miningCoordinator.addEthHashObserver(stratumServer.get());
}
staticNodes.stream()
.map(DefaultPeer::fromEnodeURL)
.forEach(peerNetwork::addMaintainConnectionPeer);
@ -484,6 +499,7 @@ public class RunnerBuilder {
jsonRpcHttpService,
graphQLHttpService,
webSocketService,
stratumServer,
metricsService,
besuController,
dataDir);

@ -562,6 +562,29 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
description = "Set if node will perform mining (default: ${DEFAULT-VALUE})")
private final Boolean isMiningEnabled = false;
@Option(
names = {"--miner-stratum-enabled"},
description = "Set if node will perform Stratum mining (default: ${DEFAULT-VALUE})")
private final Boolean iStratumMiningEnabled = false;
@SuppressWarnings("FieldMayBeFinal") // Because PicoCLI requires Strings to not be final.
@Option(
names = {"--miner-stratum-host"},
description = "Host for Stratum network mining service (default: ${DEFAULT-VALUE})")
private String stratumNetworkInterface = "0.0.0.0";
@Option(
names = {"--miner-stratum-port"},
description = "Stratum port binding (default: ${DEFAULT-VALUE})")
private final Integer stratumPort = 8008;
@SuppressWarnings("FieldMayBeFinal") // Because PicoCLI requires Strings to not be final.
@Option(
hidden = true,
names = {"--Xminer-stratum-extranonce"},
description = "Extranonce for Stratum network miners (default: ${DEFAULT-VALUE})")
private String stratumExtranonce = "080c";
@Option(
names = {"--miner-coinbase"},
description =
@ -940,14 +963,20 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
return this;
}
@SuppressWarnings("ConstantConditions")
private void validateMiningParams() {
// noinspection ConstantConditions
if (isMiningEnabled && coinbase == null) {
throw new ParameterException(
this.commandLine,
"Unable to mine without a valid coinbase. Either disable mining (remove --miner-enabled)"
+ "or specify the beneficiary of mining (via --miner-coinbase <Address>)");
}
if (!isMiningEnabled && iStratumMiningEnabled) {
throw new ParameterException(
this.commandLine,
"Unable to mine with Stratum if mining is disabled. Either disable Stratum mining (remove --miner-stratum-enabled)"
+ "or specify mining is enabled (--miner-enabled)");
}
}
protected void validateP2PInterface(final String p2pInterface) {
@ -984,7 +1013,11 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
commandLine,
"--miner-enabled",
!isMiningEnabled,
asList("--miner-coinbase", "--min-gas-price", "--miner-extra-data"));
asList(
"--miner-coinbase",
"--min-gas-price",
"--miner-extra-data",
"--miner-stratum-enabled"));
CommandLineUtils.checkOptionDependencies(
logger,
@ -1058,7 +1091,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
.ethProtocolConfiguration(ethProtocolOptions.toDomainObject())
.dataDirectory(dataDir())
.miningParameters(
new MiningParameters(coinbase, minTransactionGasPrice, extraData, isMiningEnabled))
new MiningParameters(
coinbase,
minTransactionGasPrice,
extraData,
isMiningEnabled,
iStratumMiningEnabled,
stratumNetworkInterface,
stratumPort,
stratumExtranonce))
.transactionPoolConfiguration(buildTransactionPoolConfiguration())
.nodePrivateKeyFile(nodePrivateKeyFile())
.metricsSystem(metricsSystem.get())

@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethods;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManager;
@ -58,6 +59,7 @@ public class BesuController<C> implements java.io.Closeable {
private final MiningCoordinator miningCoordinator;
private final PrivacyParameters privacyParameters;
private final List<Closeable> closeables;
private final MiningParameters miningParameters;
private final PluginServiceFactory additionalPluginServices;
private final SyncState syncState;
@ -72,6 +74,7 @@ public class BesuController<C> implements java.io.Closeable {
final TransactionPool transactionPool,
final MiningCoordinator miningCoordinator,
final PrivacyParameters privacyParameters,
final MiningParameters miningParameters,
final JsonRpcMethods additionalJsonRpcMethodsFactory,
final KeyPair keyPair,
final List<Closeable> closeables,
@ -89,6 +92,7 @@ public class BesuController<C> implements java.io.Closeable {
this.miningCoordinator = miningCoordinator;
this.privacyParameters = privacyParameters;
this.closeables = closeables;
this.miningParameters = miningParameters;
this.additionalPluginServices = additionalPluginServices;
}
@ -145,6 +149,10 @@ public class BesuController<C> implements java.io.Closeable {
return privacyParameters;
}
public MiningParameters getMiningParameters() {
return miningParameters;
}
public Map<String, JsonRpcMethod> getAdditionalJsonRpcMethods(
final Collection<RpcApi> enabledRpcApis) {
return additionalJsonRpcMethodsFactory.create(enabledRpcApis);

@ -306,6 +306,7 @@ public abstract class BesuControllerBuilder<C> {
transactionPool,
miningCoordinator,
privacyParameters,
miningParameters,
additionalJsonRpcMethodFactory,
nodeKeys,
closeables,

@ -54,6 +54,7 @@ public class MainnetBesuControllerBuilder extends BesuControllerBuilder<Void> {
final EthHashMiningCoordinator miningCoordinator =
new EthHashMiningCoordinator(protocolContext.getBlockchain(), executor, syncState);
miningCoordinator.addMinedBlockObserver(ethProtocolManager);
miningCoordinator.setStratumMiningEnabled(miningParameters.isStratumMiningEnabled());
if (miningParameters.isMiningEnabled()) {
miningCoordinator.enable();
}

@ -420,7 +420,7 @@ public abstract class JsonBlockImporterTest {
.miningParameters(
new MiningParametersTestBuilder()
.minTransactionGasPrice(Wei.ZERO)
.enabled(false)
.enabled(true)
.build())
.nodeKeys(KeyPair.generate())
.metricsSystem(new NoOpMetricsSystem())

@ -2284,6 +2284,25 @@ public class BesuCommandTest extends CommandTestAbstract {
.isEqualTo(Optional.of(Address.fromHexString(coinbaseStr)));
}
@Test
public void stratumMiningIsEnabledWhenSpecified() throws Exception {
final String coinbaseStr = String.format("%040x", 1);
parseCommand("--miner-enabled", "--miner-coinbase=" + coinbaseStr, "--miner-stratum-enabled");
final ArgumentCaptor<MiningParameters> miningArg =
ArgumentCaptor.forClass(MiningParameters.class);
verify(mockControllerBuilder).miningParameters(miningArg.capture());
verify(mockControllerBuilder).build();
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
assertThat(miningArg.getValue().isMiningEnabled()).isTrue();
assertThat(miningArg.getValue().getCoinbase())
.isEqualTo(Optional.of(Address.fromHexString(coinbaseStr)));
assertThat(miningArg.getValue().isStratumMiningEnabled()).isTrue();
}
@Test
public void miningOptionsRequiresServiceToBeEnabled() {
@ -2294,13 +2313,20 @@ public class BesuCommandTest extends CommandTestAbstract {
"--min-gas-price",
"42",
"--miner-extra-data",
"0x1122334455667788990011223344556677889900112233445566778899001122");
"0x1122334455667788990011223344556677889900112233445566778899001122",
"--miner-stratum-enabled");
verifyOptionsConstraintLoggerCall(
"--miner-enabled", "--miner-coinbase", "--min-gas-price", "--miner-extra-data");
"--miner-enabled",
"--miner-coinbase",
"--min-gas-price",
"--miner-extra-data",
"--miner-stratum-enabled");
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.startsWith(
"Unable to mine with Stratum if mining is disabled. Either disable Stratum mining (remove --miner-stratum-enabled)or specify mining is enabled (--miner-enabled)");
}
@Test

@ -79,9 +79,12 @@ metrics-push-prometheus-job="besu-everything"
# Mining
miner-enabled=false
miner-stratum-enabled=false
miner-coinbase="0x0000000000000000000000000000000000000002"
miner-extra-data="0x444F4E27542050414E4943202120484F444C2C20484F444C2C20484F444C2021"
min-gas-price=1
miner-stratum-host="0.0.0.0"
miner-stratum-port=8008
# Pruning
pruning-enabled=true

@ -23,6 +23,7 @@ import org.hyperledger.besu.crypto.SECP256K1.KeyPair;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.blockcreation.AbstractBlockScheduler;
import org.hyperledger.besu.ethereum.blockcreation.AbstractMinerExecutor;
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.BlockHeader;
@ -69,7 +70,9 @@ public class CliqueMinerExecutor extends AbstractMinerExecutor<CliqueContext, Cl
@Override
public CliqueBlockMiner createMiner(
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
final Subscribers<MinedBlockObserver> observers,
final Subscribers<EthHashObserver> ethHashObservers,
final BlockHeader parentHeader) {
final Function<BlockHeader, CliqueBlockCreator> blockCreator =
(header) ->
new CliqueBlockCreator(

@ -90,7 +90,7 @@ public class CliqueMiningCoordinatorTest {
when(protocolContext.getConsensusState()).thenReturn(cliqueContext);
when(protocolContext.getBlockchain()).thenReturn(blockChain);
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
when(syncState.isInSync()).thenReturn(true);
miningTracker = new CliqueMiningTracker(proposerAddress, protocolContext);
@ -111,7 +111,7 @@ public class CliqueMiningCoordinatorTest {
coordinator.enable();
coordinator.start();
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
reset(minerExecutor);
@ -121,7 +121,7 @@ public class CliqueMiningCoordinatorTest {
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
// validator, and the created block came from an out-turn validator.
verify(minerExecutor, never()).startAsyncMining(any(), any());
verify(minerExecutor, never()).startAsyncMining(any(), any(), any());
}
@Test
@ -138,10 +138,10 @@ public class CliqueMiningCoordinatorTest {
coordinator.enable();
coordinator.start();
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
reset(minerExecutor);
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys);
@ -150,7 +150,7 @@ public class CliqueMiningCoordinatorTest {
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
// validator, and the created block came from an out-turn validator.
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
}
@ -169,10 +169,10 @@ public class CliqueMiningCoordinatorTest {
coordinator.enable();
coordinator.start();
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
reset(minerExecutor);
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys);
@ -181,7 +181,7 @@ public class CliqueMiningCoordinatorTest {
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
// validator, and the created block came from an out-turn validator.
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
}
@ -200,10 +200,10 @@ public class CliqueMiningCoordinatorTest {
coordinator.enable();
coordinator.start();
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
reset(minerExecutor);
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys);
@ -212,7 +212,7 @@ public class CliqueMiningCoordinatorTest {
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
// validator, and the created block came from an out-turn validator.
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
}
@ -226,10 +226,10 @@ public class CliqueMiningCoordinatorTest {
coordinator.enable();
coordinator.start();
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
reset(minerExecutor);
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
final Block importedBlock = createEmptyBlock(1, blockChain.getChainHeadHash(), proposerKeys);
blockChain.appendBlock(importedBlock, Lists.emptyList());
@ -237,7 +237,7 @@ public class CliqueMiningCoordinatorTest {
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
// validator, and the created block came from an out-turn validator.
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
}

@ -81,6 +81,7 @@ public enum RpcMethod {
ETH_PROTOCOL_VERSION("eth_protocolVersion"),
ETH_SEND_RAW_TRANSACTION("eth_sendRawTransaction"),
ETH_SEND_TRANSACTION("eth_sendTransaction"),
ETH_SUBMIT_WORK("eth_submitWork"),
ETH_SUBSCRIBE("eth_subscribe"),
ETH_SYNCING("eth_syncing"),
ETH_UNINSTALL_FILTER("eth_uninstallFilter"),

@ -0,0 +1,70 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import static org.apache.logging.log4j.LogManager.getLogger;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import org.hyperledger.besu.util.bytes.BytesValue;
import java.util.Optional;
import org.apache.logging.log4j.Logger;
public class EthSubmitWork implements JsonRpcMethod {
private final MiningCoordinator miner;
private final JsonRpcParameter parameters;
private static final Logger LOG = getLogger();
public EthSubmitWork(final MiningCoordinator miner, final JsonRpcParameter parameters) {
this.miner = miner;
this.parameters = parameters;
}
@Override
public String getName() {
return RpcMethod.ETH_SUBMIT_WORK.getMethodName();
}
@Override
public JsonRpcResponse response(final JsonRpcRequest req) {
final Optional<EthHashSolverInputs> solver = miner.getWorkDefinition();
if (solver.isPresent()) {
final EthHashSolution solution =
new EthHashSolution(
BytesValue.fromHexString(parameters.required(req.getParams(), 0, String.class))
.getLong(0),
parameters.required(req.getParams(), 2, Hash.class),
BytesValue.fromHexString(parameters.required(req.getParams(), 1, String.class))
.getArrayUnsafe());
final boolean result = miner.submitWork(solution);
return new JsonRpcSuccessResponse(req.getId(), result);
} else {
LOG.trace("Mining is not operational, eth_submitWork request cannot be processed");
return new JsonRpcErrorResponse(req.getId(), JsonRpcError.NO_MINING_WORK_FOUND);
}
}
}

@ -53,6 +53,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthNewPendingT
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthProtocolVersion;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSendRawTransaction;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSendTransaction;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSubmitWork;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSyncing;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthUninstallFilter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
@ -157,6 +158,7 @@ public class EthJsonRpcMethods extends ApiGroupJsonRpcMethods {
new EthProtocolVersion(supportedCapabilities),
new EthGasPrice(miningCoordinator),
new EthGetWork(miningCoordinator),
new EthSubmitWork(miningCoordinator, parameter),
new EthHashrate(miningCoordinator),
new EthChainId(protocolSchedule.getChainId()));
}

@ -0,0 +1,132 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.blockcreation.EthHashMiningCoordinator;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import org.hyperledger.besu.util.bytes.BytesValue;
import org.hyperledger.besu.util.bytes.BytesValues;
import org.hyperledger.besu.util.uint.UInt256;
import java.util.Optional;
import com.google.common.io.BaseEncoding;
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 EthSubmitWorkTest {
private EthSubmitWork method;
private final String ETH_METHOD = "eth_submitWork";
private final String hexValue =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
@Mock private EthHashMiningCoordinator miningCoordinator;
@Before
public void setUp() {
method = new EthSubmitWork(miningCoordinator, new JsonRpcParameter());
}
@Test
public void shouldReturnCorrectMethodName() {
assertThat(method.getName()).isEqualTo(ETH_METHOD);
}
@Test
public void shouldFailIfNoMiningEnabled() {
final JsonRpcRequest request = requestWithParams();
final JsonRpcResponse actualResponse = method.response(request);
final JsonRpcResponse expectedResponse =
new JsonRpcErrorResponse(request.getId(), JsonRpcError.NO_MINING_WORK_FOUND);
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
}
@Test
public void shouldFailIfMissingArguments() {
final JsonRpcRequest request = requestWithParams();
final EthHashSolverInputs values =
new EthHashSolverInputs(
UInt256.fromHexString(hexValue), BaseEncoding.base16().lowerCase().decode(hexValue), 0);
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(values));
assertThatThrownBy(
() -> method.response(request), "Missing required json rpc parameter at index 0")
.isInstanceOf(InvalidJsonRpcParameters.class);
}
@Test
public void shouldReturnTrueIfGivenCorrectResult() {
final EthHashSolverInputs firstInputs =
new EthHashSolverInputs(
UInt256.fromHexString(
"0x0083126e978d4fdf3b645a1cac083126e978d4fdf3b645a1cac083126e978d4f"),
new byte[] {
15, -114, -104, 87, -95, -36, -17, 120, 52, 1, 124, 61, -6, -66, 78, -27, -57, 118,
-18, -64, -103, -91, -74, -121, 42, 91, -14, -98, 101, 86, -43, -51
},
468);
final EthHashSolution expectedFirstOutput =
new EthHashSolution(
-6506032554016940193L,
Hash.fromHexString(
"0xc5e3c33c86d64d0641dd3c86e8ce4628fe0aac0ef7b4c087c5fcaa45d5046d90"),
firstInputs.getPrePowHash());
final JsonRpcRequest request =
requestWithParams(
BytesValues.toMinimalBytes(expectedFirstOutput.getNonce()).getHexString(),
BytesValue.wrap(expectedFirstOutput.getPowHash()).getHexString(),
expectedFirstOutput.getMixHash().getHexString());
final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), true);
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(firstInputs));
// potentially could use a real miner here.
when(miningCoordinator.submitWork(expectedFirstOutput)).thenReturn(true);
final 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());
final JsonRpcResponse actualResponse = method.response(request);
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
}
private JsonRpcRequest requestWithParams(final Object... params) {
return new JsonRpcRequest("2.0", ETH_METHOD, params);
}
}

@ -15,6 +15,7 @@
package org.hyperledger.besu.ethereum.blockcreation;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.BlockHeader;
@ -70,9 +71,11 @@ public abstract class AbstractMinerExecutor<
}
public Optional<M> startAsyncMining(
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
final Subscribers<MinedBlockObserver> observers,
final Subscribers<EthHashObserver> ethHashObservers,
final BlockHeader parentHeader) {
try {
final M currentRunningMiner = createMiner(observers, parentHeader);
final M currentRunningMiner = createMiner(observers, ethHashObservers, parentHeader);
executorService.execute(currentRunningMiner);
return Optional.of(currentRunningMiner);
} catch (RejectedExecutionException e) {
@ -94,7 +97,9 @@ public abstract class AbstractMinerExecutor<
}
public abstract M createMiner(
final Subscribers<MinedBlockObserver> subscribers, final BlockHeader parentHeader);
final Subscribers<MinedBlockObserver> subscribers,
final Subscribers<EthHashObserver> ethHashObservers,
final BlockHeader parentHeader);
public void setExtraData(final BytesValue extraData) {
this.extraData = extraData.copy();

@ -19,6 +19,7 @@ import static org.apache.logging.log4j.LogManager.getLogger;
import org.hyperledger.besu.ethereum.chain.BlockAddedEvent;
import org.hyperledger.besu.ethereum.chain.BlockAddedObserver;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Block;
@ -48,6 +49,7 @@ public abstract class AbstractMiningCoordinator<
private static final Logger LOG = getLogger();
private final Subscribers<MinedBlockObserver> minedBlockObservers = Subscribers.create();
private final Subscribers<EthHashObserver> ethHashObservers = Subscribers.create();
private final AbstractMinerExecutor<C, M> executor;
private final SyncState syncState;
private final Blockchain blockchain;
@ -72,7 +74,7 @@ public abstract class AbstractMiningCoordinator<
final BlockHeader parentHeader,
final List<Transaction> transactions,
final List<BlockHeader> ommers) {
final M miner = executor.createMiner(Subscribers.none(), parentHeader);
final M miner = executor.createMiner(minedBlockObservers, ethHashObservers, parentHeader);
return Optional.of(miner.createBlock(parentHeader, transactions, ommers));
}
@ -146,7 +148,8 @@ public abstract class AbstractMiningCoordinator<
private void startAsyncMiningOperation() {
final BlockHeader parentHeader = blockchain.getChainHeadHeader();
currentRunningMiner = executor.startAsyncMining(minedBlockObservers, parentHeader);
currentRunningMiner =
executor.startAsyncMining(minedBlockObservers, ethHashObservers, parentHeader);
}
private synchronized boolean haltCurrentMiningOperation() {
@ -190,6 +193,11 @@ public abstract class AbstractMiningCoordinator<
minedBlockObservers.subscribe(obs);
}
@Override
public void addEthHashObserver(final EthHashObserver obs) {
ethHashObservers.subscribe(obs);
}
@Override
public Wei getMinTransactionGasPrice() {
return executor.getMinTransactionGasPrice();

@ -15,6 +15,7 @@
package org.hyperledger.besu.ethereum.blockcreation;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.BlockHeader;
@ -31,6 +32,7 @@ import java.util.function.Function;
public class EthHashMinerExecutor extends AbstractMinerExecutor<Void, EthHashBlockMiner> {
private volatile Optional<Address> coinbase;
private boolean stratumMiningEnabled;
public EthHashMinerExecutor(
final ProtocolContext<Void> protocolContext,
@ -51,18 +53,26 @@ public class EthHashMinerExecutor extends AbstractMinerExecutor<Void, EthHashBlo
@Override
public Optional<EthHashBlockMiner> startAsyncMining(
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
final Subscribers<MinedBlockObserver> observers,
final Subscribers<EthHashObserver> ethHashObservers,
final BlockHeader parentHeader) {
if (!coinbase.isPresent()) {
throw new CoinbaseNotSetException("Unable to start mining without a coinbase.");
}
return super.startAsyncMining(observers, parentHeader);
return super.startAsyncMining(observers, ethHashObservers, parentHeader);
}
@Override
public EthHashBlockMiner createMiner(
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
final Subscribers<MinedBlockObserver> observers,
final Subscribers<EthHashObserver> ethHashObservers,
final BlockHeader parentHeader) {
final EthHashSolver solver =
new EthHashSolver(new RandomNonceGenerator(), new EthHasher.Light());
new EthHashSolver(
new RandomNonceGenerator(),
new EthHasher.Light(),
stratumMiningEnabled,
ethHashObservers);
final Function<BlockHeader, EthHashBlockCreator> blockCreator =
(header) ->
new EthHashBlockCreator(
@ -88,6 +98,10 @@ public class EthHashMinerExecutor extends AbstractMinerExecutor<Void, EthHashBlo
}
}
void setStratumMiningEnabled(final boolean stratumMiningEnabled) {
this.stratumMiningEnabled = stratumMiningEnabled;
}
@Override
public Optional<Address> getCoinbase() {
return coinbase;

@ -45,6 +45,10 @@ public class EthHashMiningCoordinator extends AbstractMiningCoordinator<Void, Et
executor.setCoinbase(coinbase);
}
public void setStratumMiningEnabled(final boolean stratumMiningEnabled) {
executor.setStratumMiningEnabled(stratumMiningEnabled);
}
@Override
public Optional<Long> hashesPerSecond() {
final Optional<Long> currentHashesPerSecond =

@ -14,6 +14,7 @@
*/
package org.hyperledger.besu.ethereum.blockcreation;
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
@ -87,4 +88,6 @@ public interface MiningCoordinator {
final BlockHeader parentHeader,
final List<Transaction> transactions,
final List<BlockHeader> ommers);
default void addEthHashObserver(final EthHashObserver observer) {}
}

@ -52,7 +52,7 @@ public class AbstractMiningCoordinatorTest {
@Before
public void setUp() {
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
}
@Test
@ -68,7 +68,7 @@ public class AbstractMiningCoordinatorTest {
when(syncState.isInSync()).thenReturn(true);
miningCoordinator.enable();
miningCoordinator.start();
verify(minerExecutor).startAsyncMining(any(), any());
verify(minerExecutor).startAsyncMining(any(), any(), any());
verifyNoMoreInteractions(minerExecutor, blockMiner);
}
@ -77,12 +77,12 @@ public class AbstractMiningCoordinatorTest {
when(syncState.isInSync()).thenReturn(false);
miningCoordinator.enable();
miningCoordinator.start();
verify(minerExecutor, never()).startAsyncMining(any(), any());
verify(minerExecutor, never()).startAsyncMining(any(), any(), any());
when(syncState.isInSync()).thenReturn(true);
miningCoordinator.inSyncChanged(true);
verify(minerExecutor).startAsyncMining(any(), any());
verify(minerExecutor).startAsyncMining(any(), any(), any());
verifyNoMoreInteractions(minerExecutor, blockMiner);
}
@ -91,7 +91,7 @@ public class AbstractMiningCoordinatorTest {
when(syncState.isInSync()).thenReturn(true);
miningCoordinator.enable();
miningCoordinator.start();
verify(minerExecutor).startAsyncMining(any(), any());
verify(minerExecutor).startAsyncMining(any(), any(), any());
miningCoordinator.inSyncChanged(false);
@ -121,7 +121,7 @@ public class AbstractMiningCoordinatorTest {
BlockAddedEvent.createForHeadAdvancement(BLOCK, Collections.emptyList()), blockchain);
verify(blockMiner).cancel();
verify(minerExecutor, times(2)).startAsyncMining(any(), any());
verify(minerExecutor, times(2)).startAsyncMining(any(), any(), any());
verifyNoMoreInteractions(minerExecutor, blockMiner);
}

@ -35,6 +35,7 @@ import org.hyperledger.besu.ethereum.mainnet.ValidationTestUtils;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.testutil.TestClock;
import org.hyperledger.besu.util.Subscribers;
import org.hyperledger.besu.util.bytes.BytesValue;
import org.hyperledger.besu.util.uint.UInt256;
@ -74,7 +75,8 @@ public class EthHashBlockCreatorTest {
.build();
final EthHashSolver solver =
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
new EthHashSolver(
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
final PendingTransactions pendingTransactions =
new PendingTransactions(
@ -122,7 +124,8 @@ public class EthHashBlockCreatorTest {
.build();
final EthHashSolver solver =
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
new EthHashSolver(
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
final PendingTransactions pendingTransactions =
new PendingTransactions(
@ -165,7 +168,8 @@ public class EthHashBlockCreatorTest {
.build();
final EthHashSolver solver =
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
new EthHashSolver(
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
final PendingTransactions pendingTransactions =
new PendingTransactions(
@ -224,7 +228,8 @@ public class EthHashBlockCreatorTest {
.build();
final EthHashSolver solver =
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
new EthHashSolver(
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
final PendingTransactions pendingTransactions =
new PendingTransactions(

@ -54,7 +54,7 @@ public class EthHashMinerExecutorTest {
Function.identity());
assertThatExceptionOfType(CoinbaseNotSetException.class)
.isThrownBy(() -> executor.startAsyncMining(Subscribers.create(), null))
.isThrownBy(() -> executor.startAsyncMining(Subscribers.create(), Subscribers.none(), null))
.withMessageContaining("Unable to start mining without a coinbase.");
}

@ -63,7 +63,7 @@ public class EthHashMiningCoordinatorTest {
when(miner.getHashesPerSecond()).thenReturn(hashRate1, hashRate2, hashRate3);
when(executor.startAsyncMining(any(), any())).thenReturn(Optional.of(miner));
when(executor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(miner));
final EthHashMiningCoordinator miningCoordinator =
new EthHashMiningCoordinator(executionContext.getBlockchain(), executor, syncState);

@ -0,0 +1,39 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.chain;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import java.util.function.Function;
/** Observer of new work for the EthHash PoW algorithm */
public interface EthHashObserver {
/**
* Send a new proof-of-work job to observers
*
* @param jobInput the proof-of-work job
*/
void newJob(EthHashSolverInputs jobInput);
/**
* Sets a callback for the observer to provide solutions to jobs.
*
* @param submitSolutionCallback the callback to set on the observer, consuming a solution and
* returning true if the solution is accepted, false if rejected.
*/
void setSubmitWorkCallback(Function<EthHashSolution, Boolean> submitSolutionCallback);
}

@ -19,24 +19,42 @@ import org.hyperledger.besu.util.bytes.BytesValue;
import java.util.Objects;
import java.util.Optional;
import com.google.common.base.MoreObjects;
public class MiningParameters {
private final Optional<Address> coinbase;
private final Wei minTransactionGasPrice;
private final BytesValue extraData;
private final Boolean enabled;
private final boolean enabled;
private final boolean stratumMiningEnabled;
private final String stratumNetworkInterface;
private final int stratumPort;
private final String stratumExtranonce;
public MiningParameters(
final Address coinbase,
final Wei minTransactionGasPrice,
final BytesValue extraData,
final Boolean enabled) {
final boolean enabled) {
this(coinbase, minTransactionGasPrice, extraData, enabled, false, "0.0.0.0", 8008, "080c");
}
public MiningParameters(
final Address coinbase,
final Wei minTransactionGasPrice,
final BytesValue extraData,
final boolean enabled,
final boolean stratumMiningEnabled,
final String stratumNetworkInterface,
final int stratumPort,
final String stratumExtranonce) {
this.coinbase = Optional.ofNullable(coinbase);
this.minTransactionGasPrice = minTransactionGasPrice;
this.extraData = extraData;
this.enabled = enabled;
this.stratumMiningEnabled = stratumMiningEnabled;
this.stratumNetworkInterface = stratumNetworkInterface;
this.stratumPort = stratumPort;
this.stratumExtranonce = stratumExtranonce;
}
public Optional<Address> getCoinbase() {
@ -51,37 +69,75 @@ public class MiningParameters {
return extraData;
}
public Boolean isMiningEnabled() {
public boolean isMiningEnabled() {
return enabled;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
public boolean isStratumMiningEnabled() {
return stratumMiningEnabled;
}
if (o == null || getClass() != o.getClass()) {
return false;
public String getStratumNetworkInterface() {
return stratumNetworkInterface;
}
public int getStratumPort() {
return stratumPort;
}
public String getStratumExtranonce() {
return stratumExtranonce;
}
final MiningParameters that = (MiningParameters) o;
return Objects.equals(coinbase, that.coinbase)
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MiningParameters that = (MiningParameters) o;
return stratumPort == that.stratumPort
&& Objects.equals(coinbase, that.coinbase)
&& Objects.equals(minTransactionGasPrice, that.minTransactionGasPrice)
&& Objects.equals(extraData, that.extraData)
&& Objects.equals(enabled, that.enabled);
&& Objects.equals(enabled, that.enabled)
&& Objects.equals(stratumMiningEnabled, that.stratumMiningEnabled)
&& Objects.equals(stratumNetworkInterface, that.stratumNetworkInterface)
&& Objects.equals(stratumExtranonce, that.stratumExtranonce);
}
@Override
public int hashCode() {
return Objects.hash(coinbase, minTransactionGasPrice, extraData, enabled);
return Objects.hash(
coinbase,
minTransactionGasPrice,
extraData,
enabled,
stratumMiningEnabled,
stratumNetworkInterface,
stratumPort,
stratumExtranonce);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("coinbase", coinbase)
.add("minTransactionGasPrice", minTransactionGasPrice)
.add("extraData", extraData)
.add("enabled", enabled)
.toString();
return "MiningParameters{"
+ "coinbase="
+ coinbase
+ ", minTransactionGasPrice="
+ minTransactionGasPrice
+ ", extraData="
+ extraData
+ ", enabled="
+ enabled
+ ", stratumMiningEnabled="
+ stratumMiningEnabled
+ ", stratumNetworkInterface='"
+ stratumNetworkInterface
+ '\''
+ ", stratumPort="
+ stratumPort
+ ", stratumExtranonce='"
+ stratumExtranonce
+ '\''
+ '}';
}
}

@ -16,6 +16,9 @@ package org.hyperledger.besu.ethereum.mainnet;
import org.hyperledger.besu.ethereum.core.Hash;
import java.util.Arrays;
import java.util.Objects;
public class EthHashSolution {
private final long nonce;
private final Hash mixHash;
@ -38,4 +41,21 @@ public class EthHashSolution {
public byte[] getPowHash() {
return powHash;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EthHashSolution that = (EthHashSolution) o;
return nonce == that.nonce
&& Objects.equals(mixHash, that.mixHash)
&& Arrays.equals(powHash, that.powHash);
}
@Override
public int hashCode() {
int result = Objects.hash(nonce, mixHash);
result = 31 * result + Arrays.hashCode(powHash);
return result;
}
}

@ -14,7 +14,11 @@
*/
package org.hyperledger.besu.ethereum.mainnet;
import static org.apache.logging.log4j.LogManager.getLogger;
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.util.Subscribers;
import org.hyperledger.besu.util.bytes.Bytes32;
import org.hyperledger.besu.util.bytes.BytesValue;
import org.hyperledger.besu.util.uint.UInt256;
@ -26,9 +30,12 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Stopwatch;
import org.apache.logging.log4j.Logger;
public class EthHashSolver {
private static final Logger LOG = getLogger();
public static class EthHashSolverJob {
private final EthHashSolverInputs inputs;
@ -74,18 +81,30 @@ public class EthHashSolver {
private final Iterable<Long> nonceGenerator;
private final EthHasher ethHasher;
private volatile long hashesPerSecond = NO_MINING_CONDUCTED;
private final Boolean stratumMiningEnabled;
private final Subscribers<EthHashObserver> ethHashObservers;
private volatile Optional<EthHashSolverJob> currentJob = Optional.empty();
public EthHashSolver(final Iterable<Long> nonceGenerator, final EthHasher ethHasher) {
public EthHashSolver(
final Iterable<Long> nonceGenerator,
final EthHasher ethHasher,
final Boolean stratumMiningEnabled,
final Subscribers<EthHashObserver> ethHashObservers) {
this.nonceGenerator = nonceGenerator;
this.ethHasher = ethHasher;
this.stratumMiningEnabled = stratumMiningEnabled;
this.ethHashObservers = ethHashObservers;
ethHashObservers.forEach(observer -> observer.setSubmitWorkCallback(this::submitSolution));
}
public EthHashSolution solveFor(final EthHashSolverJob job)
throws InterruptedException, ExecutionException {
currentJob = Optional.of(job);
if (stratumMiningEnabled) {
ethHashObservers.forEach(observer -> observer.newJob(job.inputs));
} else {
findValidNonce();
}
return currentJob.get().getSolution();
}
@ -140,12 +159,14 @@ public class EthHashSolver {
public boolean submitSolution(final EthHashSolution solution) {
final Optional<EthHashSolverJob> jobSnapshot = currentJob;
if (jobSnapshot.isEmpty()) {
LOG.debug("No current job, rejecting miner work");
return false;
}
final EthHashSolverJob job = jobSnapshot.get();
final EthHashSolverInputs inputs = job.getInputs();
if (!Arrays.equals(inputs.getPrePowHash(), solution.getPowHash())) {
LOG.debug("Miner's solution does not match current job");
return false;
}
final byte[] hashBuffer = new byte[64];
@ -153,9 +174,11 @@ public class EthHashSolver {
testNonce(inputs, solution.getNonce(), hashBuffer);
if (calculatedSolution.isPresent()) {
currentJob.get().solvedWith(solution);
LOG.debug("Accepting a solution from a miner");
currentJob.get().solvedWith(calculatedSolution.get());
return true;
}
LOG.debug("Rejecting a solution from a miner");
return false;
}

@ -16,6 +16,8 @@ package org.hyperledger.besu.ethereum.mainnet;
import org.hyperledger.besu.util.uint.UInt256;
import java.util.Arrays;
public class EthHashSolverInputs {
private final UInt256 target;
private final byte[] prePowHash;
@ -39,4 +41,16 @@ public class EthHashSolverInputs {
public long getBlockNumber() {
return blockNumber;
}
@Override
public String toString() {
return "EthHashSolverInputs{"
+ "target="
+ target
+ ", prePowHash="
+ Arrays.toString(prePowHash)
+ ", blockNumber="
+ blockNumber
+ '}';
}
}

@ -21,6 +21,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.util.Subscribers;
import org.hyperledger.besu.util.uint.UInt256;
import java.util.Arrays;
@ -38,7 +39,8 @@ public class EthHashSolverTest {
@Test
public void emptyHashRateAndWorkDefinitionIsReportedPriorToSolverStarting() {
final List<Long> noncesToTry = Arrays.asList(1L, 1L, 1L, 1L, 1L, 1L, 0L);
final EthHashSolver solver = new EthHashSolver(noncesToTry, new EthHasher.Light());
final EthHashSolver solver =
new EthHashSolver(noncesToTry, new EthHasher.Light(), false, Subscribers.none());
assertThat(solver.hashesPerSecond()).isEqualTo(Optional.empty());
assertThat(solver.getWorkDefinition()).isEqualTo(Optional.empty());
@ -60,7 +62,7 @@ public class EthHashSolverTest {
.when(hasher)
.hash(any(), anyLong(), anyLong(), any());
final EthHashSolver solver = new EthHashSolver(noncesToTry, hasher);
final EthHashSolver solver = new EthHashSolver(noncesToTry, hasher, false, Subscribers.none());
final Stopwatch operationTimer = Stopwatch.createStarted();
final EthHashSolverInputs inputs = new EthHashSolverInputs(UInt256.ONE, new byte[0], 5);
@ -118,7 +120,9 @@ public class EthHashSolverTest {
final EthHashSolver solver =
new EthHashSolver(
Lists.newArrayList(expectedFirstOutput.getNonce(), 0L, expectedSecondOutput.getNonce()),
new EthHasher.Light());
new EthHasher.Light(),
false,
Subscribers.none());
EthHashSolution soln =
solver.solveFor(EthHashSolver.EthHashSolverJob.createFromInputs(firstInputs));

@ -54,6 +54,7 @@ import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage;
import org.hyperledger.besu.util.Subscribers;
import java.util.Optional;
import java.util.concurrent.locks.ReentrantLock;
@ -148,8 +149,8 @@ public class RetestethContext {
final Iterable<Long> nonceGenerator = new IncrementingNonceGenerator(0);
ethHashSolver =
("NoProof".equals(sealengine) || "NoReward".equals(sealEngine))
? new EthHashSolver(nonceGenerator, NO_WORK_HASHER)
: new EthHashSolver(nonceGenerator, new EthHasher.Light());
? new EthHashSolver(nonceGenerator, NO_WORK_HASHER, false, Subscribers.none())
: new EthHashSolver(nonceGenerator, new EthHasher.Light(), false, Subscribers.none());
blockReplay =
new BlockReplay(

@ -0,0 +1,45 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'java-library'
jar {
baseName 'besu-ethereum-stratum'
manifest {
attributes(
'Specification-Title': baseName,
'Specification-Version': project.version,
'Implementation-Title': baseName,
'Implementation-Version': calculateVersion()
)
}
}
dependencies {
api project(':plugin-api')
api project(':util')
implementation project(':ethereum:api')
implementation project(':ethereum:core')
implementation 'com.google.guava:guava'
implementation 'io.vertx:vertx-core'
testImplementation project(':testutil')
testImplementation 'org.assertj:assertj-core'
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
testImplementation 'junit:junit'
}

@ -0,0 +1,137 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.stratum;
import static org.apache.logging.log4j.LogManager.getLogger;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import org.hyperledger.besu.util.bytes.BytesValue;
import java.io.IOException;
import java.util.Arrays;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.common.io.BaseEncoding;
import io.vertx.core.json.JsonObject;
import org.apache.logging.log4j.Logger;
/**
* Implementation of the stratum1+tcp protocol.
*
* <p>This protocol allows miners to submit EthHash solutions over a persistent TCP connection.
*/
public class Stratum1EthProxyProtocol implements StratumProtocol {
private static final Logger LOG = getLogger();
private static final JsonMapper mapper = new JsonMapper();
private EthHashSolverInputs currentInput;
private Function<EthHashSolution, Boolean> submitCallback;
@Override
public boolean canHandle(final String initialMessage, final StratumConnection conn) {
JsonRpcRequest req;
try {
req = new JsonObject(initialMessage).mapTo(JsonRpcRequest.class);
} catch (IllegalArgumentException e) {
LOG.debug(e.getMessage(), e);
return false;
}
if (!"eth_submitLogin".equals(req.getMethod())) {
LOG.debug("Invalid first message method: {}", req.getMethod());
return false;
}
try {
String response = mapper.writeValueAsString(new JsonRpcSuccessResponse(req.getId(), true));
conn.send(response + "\n");
} catch (JsonProcessingException e) {
LOG.debug(e.getMessage(), e);
conn.close(null);
}
return true;
}
private void sendNewWork(final StratumConnection conn, final Object id) {
byte[] dagSeed = DirectAcyclicGraphSeed.dagSeed(currentInput.getBlockNumber());
final String[] result = {
"0x" + BaseEncoding.base16().lowerCase().encode(currentInput.getPrePowHash()),
"0x" + BaseEncoding.base16().lowerCase().encode(dagSeed),
currentInput.getTarget().toHexString()
};
JsonRpcSuccessResponse req = new JsonRpcSuccessResponse(id, result);
try {
conn.send(mapper.writeValueAsString(req) + "\n");
} catch (JsonProcessingException e) {
LOG.debug(e.getMessage(), e);
}
}
@Override
public void onClose(final StratumConnection conn) {}
@Override
public void handle(final StratumConnection conn, final String message) {
try {
JsonRpcRequest req = new JsonObject(message).mapTo(JsonRpcRequest.class);
if ("eth_getWork".equals(req.getMethod())) {
sendNewWork(conn, req.getId());
} else if ("eth_submitWork".equals(req.getMethod())) {
handleMiningSubmit(conn, req);
}
} catch (IllegalArgumentException | IOException e) {
LOG.debug(e.getMessage(), e);
conn.close(null);
}
}
private void handleMiningSubmit(final StratumConnection conn, final JsonRpcRequest req)
throws IOException {
LOG.debug("Miner submitted solution {}", req);
boolean result = false;
JsonRpcParameter parameters = new JsonRpcParameter();
final EthHashSolution solution =
new EthHashSolution(
BytesValue.fromHexString(parameters.required(req.getParams(), 0, String.class))
.getLong(0),
parameters.required(req.getParams(), 2, Hash.class),
BytesValue.fromHexString(parameters.required(req.getParams(), 1, String.class))
.getArrayUnsafe());
if (Arrays.equals(currentInput.getPrePowHash(), solution.getPowHash())) {
result = submitCallback.apply(solution);
}
String response = mapper.writeValueAsString(new JsonRpcSuccessResponse(req.getId(), result));
conn.send(response + "\n");
}
@Override
public void setCurrentWorkTask(final EthHashSolverInputs input) {
this.currentInput = input;
}
@Override
public void setSubmitCallback(final Function<EthHashSolution, Boolean> submitSolutionCallback) {
this.submitCallback = submitSolutionCallback;
}
}

@ -0,0 +1,208 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.stratum;
import static org.apache.logging.log4j.LogManager.getLogger;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import org.hyperledger.besu.util.bytes.BytesValue;
import org.hyperledger.besu.util.bytes.BytesValues;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.function.Supplier;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.vertx.core.json.JsonObject;
import org.apache.logging.log4j.Logger;
/**
* Implementation of the stratum+tcp protocol.
*
* <p>This protocol allows miners to submit EthHash solutions over a persistent TCP connection.
*/
public class Stratum1Protocol implements StratumProtocol {
private static final Logger LOG = getLogger();
private static final JsonMapper mapper = new JsonMapper();
private static final String STRATUM_1 = "EthereumStratum/1.0.0";
private static String createSubscriptionID() {
byte[] subscriptionBytes = new byte[16];
new Random().nextBytes(subscriptionBytes);
return BytesValue.wrap(subscriptionBytes).toUnprefixedString();
}
private final String extranonce;
private EthHashSolverInputs currentInput;
private Function<EthHashSolution, Boolean> submitCallback;
private final Supplier<String> jobIdSupplier;
private final Supplier<String> subscriptionIdCreator;
private final List<StratumConnection> activeConnections = new ArrayList<>();
public Stratum1Protocol(final String extranonce) {
this(
extranonce,
() -> {
BytesValue timeValue = BytesValues.toMinimalBytes(Instant.now().toEpochMilli());
return timeValue.slice(timeValue.size() - 4, 4).toUnprefixedString();
},
Stratum1Protocol::createSubscriptionID);
}
Stratum1Protocol(
final String extranonce,
final Supplier<String> jobIdSupplier,
final Supplier<String> subscriptionIdCreator) {
this.extranonce = extranonce;
this.jobIdSupplier = jobIdSupplier;
this.subscriptionIdCreator = subscriptionIdCreator;
}
@Override
public boolean canHandle(final String initialMessage, final StratumConnection conn) {
JsonRpcRequest req;
try {
req = new JsonObject(initialMessage).mapTo(JsonRpcRequest.class);
} catch (IllegalArgumentException e) {
LOG.debug(e.getMessage(), e);
return false;
}
if (!"mining.subscribe".equals(req.getMethod())) {
LOG.debug("Invalid first message method: {}", req.getMethod());
return false;
}
try {
String notify =
mapper.writeValueAsString(
new JsonRpcSuccessResponse(
req.getId(),
new Object[] {
new String[] {
"mining.notify",
subscriptionIdCreator.get(), // subscription ID, never reused.
STRATUM_1
},
extranonce
}));
conn.send(notify + "\n");
} catch (JsonProcessingException e) {
LOG.debug(e.getMessage(), e);
conn.close(null);
}
return true;
}
private void registerConnection(final StratumConnection conn) {
activeConnections.add(conn);
if (currentInput != null) {
sendNewWork(conn);
}
}
private void sendNewWork(final StratumConnection conn) {
byte[] dagSeed = DirectAcyclicGraphSeed.dagSeed(currentInput.getBlockNumber());
Object[] params =
new Object[] {
jobIdSupplier.get(),
BytesValue.wrap(currentInput.getPrePowHash()).getHexString(),
BytesValue.wrap(dagSeed).getHexString(),
currentInput.getTarget().toHexString(),
true
};
JsonRpcRequest req = new JsonRpcRequest("2.0", "mining.notify", params);
try {
conn.send(mapper.writeValueAsString(req) + "\n");
} catch (JsonProcessingException e) {
LOG.debug(e.getMessage(), e);
}
}
@Override
public void onClose(final StratumConnection conn) {
activeConnections.remove(conn);
}
@Override
public void handle(final StratumConnection conn, final String message) {
try {
JsonRpcRequest req = new JsonObject(message).mapTo(JsonRpcRequest.class);
if ("mining.authorize".equals(req.getMethod())) {
handleMiningAuthorize(conn, req);
} else if ("mining.submit".equals(req.getMethod())) {
handleMiningSubmit(conn, req);
}
} catch (IllegalArgumentException | IOException e) {
LOG.debug(e.getMessage(), e);
conn.close(null);
}
}
private void handleMiningSubmit(final StratumConnection conn, final JsonRpcRequest message)
throws IOException {
LOG.debug("Miner submitted solution {}", message);
boolean result = false;
JsonRpcParameter parameters = new JsonRpcParameter();
final EthHashSolution solution =
new EthHashSolution(
BytesValue.fromHexString(parameters.required(message.getParams(), 2, String.class))
.getLong(0),
Hash.fromHexString(parameters.required(message.getParams(), 4, String.class)),
BytesValue.fromHexString(parameters.required(message.getParams(), 3, String.class))
.getArrayUnsafe());
if (Arrays.equals(currentInput.getPrePowHash(), solution.getPowHash())) {
result = submitCallback.apply(solution);
}
String response =
mapper.writeValueAsString(new JsonRpcSuccessResponse(message.getId(), result));
conn.send(response + "\n");
}
private void handleMiningAuthorize(final StratumConnection conn, final JsonRpcRequest message)
throws IOException {
// discard message contents as we don't care for username/password.
// send confirmation
String confirm = mapper.writeValueAsString(new JsonRpcSuccessResponse(message.getId(), true));
conn.send(confirm + "\n");
// ready for work.
registerConnection(conn);
}
@Override
public void setCurrentWorkTask(final EthHashSolverInputs input) {
this.currentInput = input;
LOG.debug("Sending new work to miners: {}", input);
for (StratumConnection conn : activeConnections) {
sendNewWork(conn);
}
}
@Override
public void setSubmitCallback(final Function<EthHashSolution, Boolean> submitSolutionCallback) {
this.submitCallback = submitSolutionCallback;
}
}

@ -0,0 +1,105 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.stratum;
import static org.apache.logging.log4j.LogManager.getLogger;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.function.Consumer;
import com.google.common.base.Splitter;
import io.vertx.core.buffer.Buffer;
import org.apache.logging.log4j.Logger;
/**
* Persistent TCP connection using a variant of the Stratum protocol, connecting the client to
* miners.
*/
final class StratumConnection {
private static final Logger LOG = getLogger();
private String incompleteMessage = "";
private final StratumProtocol[] protocols;
private final Runnable closeHandle;
private final Consumer<String> sender;
private StratumProtocol protocol;
StratumConnection(
final StratumProtocol[] protocols,
final Runnable closeHandle,
final Consumer<String> sender) {
this.protocols = protocols;
this.closeHandle = closeHandle;
this.sender = sender;
}
void handleBuffer(final Buffer buffer) {
LOG.trace("Buffer received {}", buffer);
Splitter splitter = Splitter.on('\n');
boolean firstMessage = false;
String messagesString;
try {
messagesString = buffer.toString(StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
LOG.debug("Invalid message with non UTF-8 characters: " + e.getMessage(), e);
closeHandle.run();
return;
}
Iterator<String> messages = splitter.split(messagesString).iterator();
while (messages.hasNext()) {
String message = messages.next();
if (!firstMessage) {
message = incompleteMessage + message;
firstMessage = true;
}
if (!messages.hasNext()) {
incompleteMessage = message;
} else {
LOG.trace("Dispatching message {}", message);
handleMessage(message);
}
}
}
void close(final Void aVoid) {
if (protocol != null) {
protocol.onClose(this);
}
}
private void handleMessage(final String message) {
if (protocol == null) {
for (StratumProtocol protocol : protocols) {
if (protocol.canHandle(message, this)) {
this.protocol = protocol;
}
}
if (protocol == null) {
LOG.debug("Invalid first message: {}", message);
closeHandle.run();
}
} else {
protocol.handle(this, message);
}
}
public void send(final String message) {
LOG.debug("Sending message {}", message);
sender.accept(message);
}
}

@ -0,0 +1,62 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.stratum;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import java.util.function.Function;
/**
* Stratum protocol handler.
*
* <p>Manages the lifecycle of a TCP connection according to a particular variant of the Stratum
* protocol.
*/
public interface StratumProtocol {
/**
* Checks if the protocol can handle a TCP connection, based on the initial message.
*
* @param initialMessage the initial message sent over the TCP connection.
* @param conn the connection itself
* @return true if the protocol can handle this connection
*/
boolean canHandle(String initialMessage, StratumConnection conn);
/**
* Callback when a stratum connection is closed.
*
* @param conn the connection that just closed
*/
void onClose(StratumConnection conn);
/**
* Handle a message over an established Stratum connection
*
* @param conn the Stratum connection
* @param message the message to handle
*/
void handle(StratumConnection conn, String message);
/**
* Sets the current proof-of-work job.
*
* @param input the new proof-of-work job to send to miners
*/
void setCurrentWorkTask(EthHashSolverInputs input);
void setSubmitCallback(Function<EthHashSolution, Boolean> submitSolutionCallback);
}

@ -0,0 +1,130 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.stratum;
import static org.apache.logging.log4j.LogManager.getLogger;
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetServer;
import io.vertx.core.net.NetServerOptions;
import io.vertx.core.net.NetSocket;
import org.apache.logging.log4j.Logger;
/**
* TCP server allowing miners to connect to the client over persistent TCP connections, using the
* various Stratum protocols.
*/
public class StratumServer implements EthHashObserver {
private static final Logger logger = getLogger();
private final Vertx vertx;
private final int port;
private final String networkInterface;
private final AtomicBoolean started = new AtomicBoolean(false);
private final StratumProtocol[] protocols;
private NetServer server;
public StratumServer(
final Vertx vertx, final int port, final String networkInterface, final String extraNonce) {
this.vertx = vertx;
this.port = port;
this.networkInterface = networkInterface;
protocols =
new StratumProtocol[] {new Stratum1Protocol(extraNonce), new Stratum1EthProxyProtocol()};
}
public CompletableFuture<?> start() {
if (started.compareAndSet(false, true)) {
logger.info("Starting stratum server on {}:{}", networkInterface, port);
server =
vertx.createNetServer(
new NetServerOptions().setPort(port).setHost(networkInterface).setTcpKeepAlive(true));
CompletableFuture<?> result = new CompletableFuture<>();
server.connectHandler(this::handle);
server.listen(
res -> {
if (res.failed()) {
result.completeExceptionally(
new StratumServerException(
String.format(
"Failed to bind Stratum Server listener to %s:%s: %s",
networkInterface, port, res.cause().getMessage())));
} else {
result.complete(null);
}
});
return result;
}
return CompletableFuture.completedFuture(null);
}
private void handle(final NetSocket socket) {
StratumConnection conn =
new StratumConnection(
protocols, socket::close, bytes -> socket.write(Buffer.buffer(bytes)));
socket.handler(conn::handleBuffer);
socket.closeHandler(conn::close);
}
public CompletableFuture<?> stop() {
if (started.compareAndSet(true, false)) {
CompletableFuture<?> result = new CompletableFuture<>();
server.close(
res -> {
if (res.failed()) {
result.completeExceptionally(
new StratumServerException(
String.format(
"Failed to bind Stratum Server listener to %s:%s: %s",
networkInterface, port, res.cause().getMessage())));
} else {
result.complete(null);
}
});
return result;
}
logger.debug("Stopping StratumServer that was not running");
return CompletableFuture.completedFuture(null);
}
@Override
public void newJob(final EthHashSolverInputs ethHashSolverInputs) {
if (!started.get()) {
logger.debug("Discarding {} as stratum server is not started", ethHashSolverInputs);
return;
}
for (StratumProtocol protocol : protocols) {
protocol.setCurrentWorkTask(ethHashSolverInputs);
}
}
@Override
public void setSubmitWorkCallback(
final Function<EthHashSolution, Boolean> submitSolutionCallback) {
for (StratumProtocol protocol : protocols) {
protocol.setSubmitCallback(submitSolutionCallback);
}
}
}

@ -0,0 +1,23 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.stratum;
/** Class of exception occurring while launching the Stratum server. */
public class StratumServerException extends RuntimeException {
public StratumServerException(final String message) {
super(message);
}
}

@ -0,0 +1,118 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.stratum;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
import org.hyperledger.besu.util.bytes.BytesValue;
import org.hyperledger.besu.util.uint.UInt256;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import io.vertx.core.buffer.Buffer;
import org.junit.Test;
public class StratumConnectionTest {
@Test
public void testNoSuitableProtocol() {
AtomicBoolean called = new AtomicBoolean(false);
StratumConnection conn =
new StratumConnection(new StratumProtocol[] {}, () -> called.set(true), bytes -> {});
conn.handleBuffer(Buffer.buffer("{}\n"));
assertThat(called.get()).isTrue();
}
@Test
public void testStratum1WithoutMatches() {
AtomicBoolean called = new AtomicBoolean(false);
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {new Stratum1Protocol("")}, () -> called.set(true), bytes -> {});
conn.handleBuffer(Buffer.buffer("{}\n"));
assertThat(called.get()).isTrue();
}
@Test
public void testStratum1Matches() {
AtomicBoolean called = new AtomicBoolean(false);
AtomicReference<String> message = new AtomicReference<>();
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {new Stratum1Protocol("", () -> "abcd", () -> "abcd")},
() -> called.set(true),
message::set);
conn.handleBuffer(
Buffer.buffer(
"{"
+ " \"id\": 23,"
+ " \"method\": \"mining.subscribe\", "
+ " \"params\": [ "
+ " \"MinerName/1.0.0\", \"EthereumStratum/1.0.0\" "
+ " ]"
+ "}\n"));
assertThat(called.get()).isFalse();
assertThat(message.get())
.isEqualTo(
"{\"jsonrpc\":\"2.0\",\"id\":23,\"result\":[[\"mining.notify\",\"abcd\",\"EthereumStratum/1.0.0\"],\"\"]}\n");
}
@Test
public void testStratum1SendWork() {
AtomicBoolean called = new AtomicBoolean(false);
AtomicReference<String> message = new AtomicReference<>();
Stratum1Protocol protocol = new Stratum1Protocol("", () -> "abcd", () -> "abcd");
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {protocol}, () -> called.set(true), message::set);
conn.handleBuffer(
Buffer.buffer(
"{"
+ " \"id\": 23,"
+ " \"method\": \"mining.subscribe\", "
+ " \"params\": [ "
+ " \"MinerName/1.0.0\", \"EthereumStratum/1.0.0\" "
+ " ]"
+ "}\n"));
conn.handleBuffer(
Buffer.buffer(
"{"
+ " \"id\": null,"
+ " \"method\": \"mining.authorize\", "
+ " \"params\": [ "
+ " \"someusername\", \"password\" "
+ " ]"
+ "}\n"));
assertThat(called.get()).isFalse();
// now send work without waiting.
protocol.setCurrentWorkTask(
new EthHashSolverInputs(
UInt256.of(3), BytesValue.fromHexString("deadbeef").getArrayUnsafe(), 42));
assertThat(message.get())
.isEqualTo(
"{\"jsonrpc\":\"2.0\",\"method\":\"mining.notify\",\"params\":[\"abcd\",\"0xdeadbeef\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x0000000000000000000000000000000000000000000000000000000000000003\",true],\"id\":null}\n");
}
}

@ -35,6 +35,7 @@ include 'ethereum:referencetests'
include 'ethereum:retesteth'
include 'ethereum:rlp'
include 'ethereum:trie'
include 'ethereum:stratum'
include 'metrics:core'
include 'metrics:rocksdb'
include 'nat'

Loading…
Cancel
Save