From 55e24759b710afb58282cb79aa954f2b8d327d70 Mon Sep 17 00:00:00 2001 From: Antoine Toulme Date: Tue, 12 Nov 2019 13:08:12 -0800 Subject: [PATCH] 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 --- .../dsl/node/ProcessBesuNodeRunner.java | 7 + besu/build.gradle | 1 + .../java/org/hyperledger/besu/Runner.java | 6 + .../org/hyperledger/besu/RunnerBuilder.java | 16 ++ .../org/hyperledger/besu/cli/BesuCommand.java | 47 +++- .../besu/controller/BesuController.java | 8 + .../controller/BesuControllerBuilder.java | 1 + .../MainnetBesuControllerBuilder.java | 1 + .../chainimport/JsonBlockImporterTest.java | 2 +- .../hyperledger/besu/cli/BesuCommandTest.java | 32 ++- .../src/test/resources/everything_config.toml | 3 + .../blockcreation/CliqueMinerExecutor.java | 5 +- .../CliqueMiningCoordinatorTest.java | 30 +-- .../besu/ethereum/api/jsonrpc/RpcMethod.java | 1 + .../internal/methods/EthSubmitWork.java | 70 ++++++ .../jsonrpc/methods/EthJsonRpcMethods.java | 2 + .../internal/methods/EthSubmitWorkTest.java | 132 +++++++++++ .../blockcreation/AbstractMinerExecutor.java | 11 +- .../AbstractMiningCoordinator.java | 12 +- .../blockcreation/EthHashMinerExecutor.java | 22 +- .../EthHashMiningCoordinator.java | 4 + .../blockcreation/MiningCoordinator.java | 3 + .../AbstractMiningCoordinatorTest.java | 12 +- .../EthHashBlockCreatorTest.java | 13 +- .../EthHashMinerExecutorTest.java | 2 +- .../EthHashMiningCoordinatorTest.java | 2 +- .../besu/ethereum/chain/EthHashObserver.java | 39 ++++ .../besu/ethereum/core/MiningParameters.java | 98 +++++++-- .../ethereum/mainnet/EthHashSolution.java | 20 ++ .../besu/ethereum/mainnet/EthHashSolver.java | 31 ++- .../ethereum/mainnet/EthHashSolverInputs.java | 14 ++ .../ethereum/mainnet/EthHashSolverTest.java | 10 +- .../ethereum/retesteth/RetestethContext.java | 5 +- ethereum/stratum/build.gradle | 45 ++++ .../stratum/Stratum1EthProxyProtocol.java | 137 ++++++++++++ .../ethereum/stratum/Stratum1Protocol.java | 208 ++++++++++++++++++ .../ethereum/stratum/StratumConnection.java | 105 +++++++++ .../ethereum/stratum/StratumProtocol.java | 62 ++++++ .../besu/ethereum/stratum/StratumServer.java | 130 +++++++++++ .../stratum/StratumServerException.java | 23 ++ .../stratum/StratumConnectionTest.java | 118 ++++++++++ settings.gradle | 1 + 42 files changed, 1417 insertions(+), 74 deletions(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWork.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWorkTest.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/EthHashObserver.java create mode 100644 ethereum/stratum/build.gradle create mode 100644 ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1EthProxyProtocol.java create mode 100644 ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1Protocol.java create mode 100644 ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumConnection.java create mode 100644 ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumProtocol.java create mode 100644 ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServer.java create mode 100644 ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServerException.java create mode 100644 ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/StratumConnectionTest.java diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java index a9e6fb8b84..5032a8cc0b 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java @@ -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()) { diff --git a/besu/build.gradle b/besu/build.gradle index aab9210ddf..df93e47868 100644 --- a/besu/build.gradle +++ b/besu/build.gradle @@ -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') diff --git a/besu/src/main/java/org/hyperledger/besu/Runner.java b/besu/src/main/java/org/hyperledger/besu/Runner.java index d7229c5661..d87f3cc76e 100644 --- a/besu/src/main/java/org/hyperledger/besu/Runner.java +++ b/besu/src/main/java/org/hyperledger/besu/Runner.java @@ -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; Runner( final Vertx vertx, @@ -64,6 +66,7 @@ public class Runner implements AutoCloseable { final Optional jsonRpc, final Optional graphQLHttp, final Optional websocketRpc, + final Optional stratumServer, final Optional 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); diff --git a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java index 6251f1cfbf..609330e6da 100644 --- a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -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 = 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); diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 5863f766cd..e6de377a15 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -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
)"); } + 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()) diff --git a/besu/src/main/java/org/hyperledger/besu/controller/BesuController.java b/besu/src/main/java/org/hyperledger/besu/controller/BesuController.java index 452302ae48..f14461974f 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/BesuController.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/BesuController.java @@ -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 implements java.io.Closeable { private final MiningCoordinator miningCoordinator; private final PrivacyParameters privacyParameters; private final List closeables; + private final MiningParameters miningParameters; private final PluginServiceFactory additionalPluginServices; private final SyncState syncState; @@ -72,6 +74,7 @@ public class BesuController implements java.io.Closeable { final TransactionPool transactionPool, final MiningCoordinator miningCoordinator, final PrivacyParameters privacyParameters, + final MiningParameters miningParameters, final JsonRpcMethods additionalJsonRpcMethodsFactory, final KeyPair keyPair, final List closeables, @@ -89,6 +92,7 @@ public class BesuController 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 implements java.io.Closeable { return privacyParameters; } + public MiningParameters getMiningParameters() { + return miningParameters; + } + public Map getAdditionalJsonRpcMethods( final Collection enabledRpcApis) { return additionalJsonRpcMethodsFactory.create(enabledRpcApis); diff --git a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java index 435cc4cb64..8708dcd75e 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java @@ -306,6 +306,7 @@ public abstract class BesuControllerBuilder { transactionPool, miningCoordinator, privacyParameters, + miningParameters, additionalJsonRpcMethodFactory, nodeKeys, closeables, diff --git a/besu/src/main/java/org/hyperledger/besu/controller/MainnetBesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/MainnetBesuControllerBuilder.java index 7e0ae13852..bdcf347fdf 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/MainnetBesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/MainnetBesuControllerBuilder.java @@ -54,6 +54,7 @@ public class MainnetBesuControllerBuilder extends BesuControllerBuilder { final EthHashMiningCoordinator miningCoordinator = new EthHashMiningCoordinator(protocolContext.getBlockchain(), executor, syncState); miningCoordinator.addMinedBlockObserver(ethProtocolManager); + miningCoordinator.setStratumMiningEnabled(miningParameters.isStratumMiningEnabled()); if (miningParameters.isMiningEnabled()) { miningCoordinator.enable(); } diff --git a/besu/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java b/besu/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java index 268bd08843..8f6aa94b3e 100644 --- a/besu/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java +++ b/besu/src/test/java/org/hyperledger/besu/chainimport/JsonBlockImporterTest.java @@ -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()) diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index 0e973979b4..bd627dd82a 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -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 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 diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index b480d4f125..602b286651 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -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 diff --git a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java index f1d97a634b..588fe24d63 100644 --- a/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java +++ b/consensus/clique/src/main/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMinerExecutor.java @@ -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 observers, final BlockHeader parentHeader) { + final Subscribers observers, + final Subscribers ethHashObservers, + final BlockHeader parentHeader) { final Function blockCreator = (header) -> new CliqueBlockCreator( diff --git a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java index bfaec897c0..9b76fcef9d 100644 --- a/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java +++ b/consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueMiningCoordinatorTest.java @@ -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 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 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 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 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()); } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index a09e056b93..bb51c2c3b6 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -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"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWork.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWork.java new file mode 100644 index 0000000000..26905a31c8 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWork.java @@ -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 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); + } + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/EthJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/EthJsonRpcMethods.java index 86197aa9f8..2e389f4e2d 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/EthJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/EthJsonRpcMethods.java @@ -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())); } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWorkTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWorkTest.java new file mode 100644 index 0000000000..4551b499b4 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitWorkTest.java @@ -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); + } +} diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMinerExecutor.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMinerExecutor.java index 9c58697c69..7575b7474f 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMinerExecutor.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMinerExecutor.java @@ -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 startAsyncMining( - final Subscribers observers, final BlockHeader parentHeader) { + final Subscribers observers, + final Subscribers 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 subscribers, final BlockHeader parentHeader); + final Subscribers subscribers, + final Subscribers ethHashObservers, + final BlockHeader parentHeader); public void setExtraData(final BytesValue extraData) { this.extraData = extraData.copy(); diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinator.java index b211763e7c..da0db43bd6 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinator.java @@ -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 minedBlockObservers = Subscribers.create(); + private final Subscribers ethHashObservers = Subscribers.create(); private final AbstractMinerExecutor executor; private final SyncState syncState; private final Blockchain blockchain; @@ -72,7 +74,7 @@ public abstract class AbstractMiningCoordinator< final BlockHeader parentHeader, final List transactions, final List 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(); diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutor.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutor.java index 063dccf470..d3ba63479e 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutor.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutor.java @@ -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 { private volatile Optional
coinbase; + private boolean stratumMiningEnabled; public EthHashMinerExecutor( final ProtocolContext protocolContext, @@ -51,18 +53,26 @@ public class EthHashMinerExecutor extends AbstractMinerExecutor startAsyncMining( - final Subscribers observers, final BlockHeader parentHeader) { + final Subscribers observers, + final Subscribers 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 observers, final BlockHeader parentHeader) { + final Subscribers observers, + final Subscribers 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 blockCreator = (header) -> new EthHashBlockCreator( @@ -88,6 +98,10 @@ public class EthHashMinerExecutor extends AbstractMinerExecutor getCoinbase() { return coinbase; diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinator.java index 967888be45..164dcda4c9 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinator.java @@ -45,6 +45,10 @@ public class EthHashMiningCoordinator extends AbstractMiningCoordinator hashesPerSecond() { final Optional currentHashesPerSecond = diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/MiningCoordinator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/MiningCoordinator.java index 6bf79cfc2c..e9355ec2d6 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/MiningCoordinator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/MiningCoordinator.java @@ -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 transactions, final List ommers); + + default void addEthHashObserver(final EthHashObserver observer) {} } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinatorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinatorTest.java index df02770b83..989579732b 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinatorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractMiningCoordinatorTest.java @@ -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); } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashBlockCreatorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashBlockCreatorTest.java index 6b1f1fdc6f..0db8d57367 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashBlockCreatorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashBlockCreatorTest.java @@ -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( diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutorTest.java index 1a3a99550c..4809752c20 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMinerExecutorTest.java @@ -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."); } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinatorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinatorTest.java index 26fc1eb9cc..dcdf30f947 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinatorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/EthHashMiningCoordinatorTest.java @@ -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); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/EthHashObserver.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/EthHashObserver.java new file mode 100644 index 0000000000..402222f931 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/EthHashObserver.java @@ -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 submitSolutionCallback); +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningParameters.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningParameters.java index 58ff10e393..d01a7e027a 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningParameters.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningParameters.java @@ -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
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) { + 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 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
getCoinbase() { @@ -51,37 +69,75 @@ public class MiningParameters { return extraData; } - public Boolean isMiningEnabled() { + public boolean isMiningEnabled() { return enabled; } + public boolean isStratumMiningEnabled() { + return stratumMiningEnabled; + } + + public String getStratumNetworkInterface() { + return stratumNetworkInterface; + } + + public int getStratumPort() { + return stratumPort; + } + + public String getStratumExtranonce() { + return stratumExtranonce; + } + @Override public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final MiningParameters that = (MiningParameters) o; - return Objects.equals(coinbase, that.coinbase) + 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 + + '\'' + + '}'; } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolution.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolution.java index 1ee251b8bb..f1dd18cfdb 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolution.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolution.java @@ -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; + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolver.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolver.java index def9f0b544..199d86ab79 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolver.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolver.java @@ -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 nonceGenerator; private final EthHasher ethHasher; private volatile long hashesPerSecond = NO_MINING_CONDUCTED; - + private final Boolean stratumMiningEnabled; + private final Subscribers ethHashObservers; private volatile Optional currentJob = Optional.empty(); - public EthHashSolver(final Iterable nonceGenerator, final EthHasher ethHasher) { + public EthHashSolver( + final Iterable nonceGenerator, + final EthHasher ethHasher, + final Boolean stratumMiningEnabled, + final Subscribers 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); - findValidNonce(); + 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 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; } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverInputs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverInputs.java index 2ebf6fbe93..fa0dd170d3 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverInputs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverInputs.java @@ -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 + + '}'; + } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverTest.java index 5ab41e135c..73dfbe9051 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/EthHashSolverTest.java @@ -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 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)); diff --git a/ethereum/retesteth/src/main/java/org/hyperledger/besu/ethereum/retesteth/RetestethContext.java b/ethereum/retesteth/src/main/java/org/hyperledger/besu/ethereum/retesteth/RetestethContext.java index 420bb2a6fb..98a3bf261b 100644 --- a/ethereum/retesteth/src/main/java/org/hyperledger/besu/ethereum/retesteth/RetestethContext.java +++ b/ethereum/retesteth/src/main/java/org/hyperledger/besu/ethereum/retesteth/RetestethContext.java @@ -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 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( diff --git a/ethereum/stratum/build.gradle b/ethereum/stratum/build.gradle new file mode 100644 index 0000000000..05c3977cbd --- /dev/null +++ b/ethereum/stratum/build.gradle @@ -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' +} diff --git a/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1EthProxyProtocol.java b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1EthProxyProtocol.java new file mode 100644 index 0000000000..e6892b4020 --- /dev/null +++ b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1EthProxyProtocol.java @@ -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. + * + *

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 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 submitSolutionCallback) { + this.submitCallback = submitSolutionCallback; + } +} diff --git a/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1Protocol.java b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1Protocol.java new file mode 100644 index 0000000000..887fb6cbc7 --- /dev/null +++ b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1Protocol.java @@ -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. + * + *

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 submitCallback; + private final Supplier jobIdSupplier; + private final Supplier subscriptionIdCreator; + private final List 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 jobIdSupplier, + final Supplier 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 submitSolutionCallback) { + this.submitCallback = submitSolutionCallback; + } +} diff --git a/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumConnection.java b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumConnection.java new file mode 100644 index 0000000000..f976d3f71a --- /dev/null +++ b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumConnection.java @@ -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 sender; + + private StratumProtocol protocol; + + StratumConnection( + final StratumProtocol[] protocols, + final Runnable closeHandle, + final Consumer 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 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); + } +} diff --git a/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumProtocol.java b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumProtocol.java new file mode 100644 index 0000000000..0150235b7d --- /dev/null +++ b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumProtocol.java @@ -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. + * + *

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 submitSolutionCallback); +} diff --git a/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServer.java b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServer.java new file mode 100644 index 0000000000..353a70e00d --- /dev/null +++ b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServer.java @@ -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 submitSolutionCallback) { + for (StratumProtocol protocol : protocols) { + protocol.setSubmitCallback(submitSolutionCallback); + } + } +} diff --git a/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServerException.java b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServerException.java new file mode 100644 index 0000000000..4d51ed3aa3 --- /dev/null +++ b/ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServerException.java @@ -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); + } +} diff --git a/ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/StratumConnectionTest.java b/ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/StratumConnectionTest.java new file mode 100644 index 0000000000..acef6116c2 --- /dev/null +++ b/ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/StratumConnectionTest.java @@ -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 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 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"); + } +} diff --git a/settings.gradle b/settings.gradle index 96723f9f70..4571f8c2f1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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'