mirror of
https://github.com/vacp2p/linea-besu.git
synced 2026-01-08 20:47:59 -05:00
Stratum Server Support (#140)
Add support for external GPU mining via the stratum protocol. Three new CLI Options support this: `--miner-stratum-enabled`, `--miner-stratum-host`, and `--miner-stratum-port`. To use stratum first use the `--miner-enabled` option and add the `--miner-stratum-enabled` option. This disables local CPU mining and opens up a stratum server, configurable via `--miner-stratum-host` (default is `0.0.0.0`) and `--miner-stratum-port` (default is 8008). This server supports `stratum+tcp` mining and the JSON-RPC services (if enabled) will support the `eth_getWork` and `eth_submitWork` calls as well (supporting `getwork` or `http` schemes). This is known to work with ethminer. Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>
This commit is contained in:
committed by
Danno Ferrin
parent
dd46332530
commit
55e24759b7
@@ -86,6 +86,13 @@ public class ProcessBesuNodeRunner implements BesuNodeRunner {
|
||||
params.add("--miner-enabled");
|
||||
params.add("--miner-coinbase");
|
||||
params.add(node.getMiningParameters().getCoinbase().get().toString());
|
||||
params.add("--miner-stratum-port");
|
||||
params.add(Integer.toString(node.getMiningParameters().getStratumPort()));
|
||||
params.add("--miner-stratum-host");
|
||||
params.add(node.getMiningParameters().getStratumNetworkInterface());
|
||||
}
|
||||
if (node.getMiningParameters().isStratumMiningEnabled()) {
|
||||
params.add("--miner-stratum-enabled");
|
||||
}
|
||||
|
||||
if (node.getPrivacyParameters().isEnabled()) {
|
||||
|
||||
@@ -44,6 +44,7 @@ dependencies {
|
||||
implementation project(':ethereum:p2p')
|
||||
implementation project(':ethereum:retesteth')
|
||||
implementation project(':ethereum:rlp')
|
||||
implementation project(':ethereum:stratum')
|
||||
implementation project(':metrics:core')
|
||||
implementation project(':nat')
|
||||
implementation project(':plugin-api')
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcHttpService;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketService;
|
||||
import org.hyperledger.besu.ethereum.p2p.network.NetworkRunner;
|
||||
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
|
||||
import org.hyperledger.besu.ethereum.stratum.StratumServer;
|
||||
import org.hyperledger.besu.metrics.prometheus.MetricsService;
|
||||
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
|
||||
|
||||
@@ -56,6 +57,7 @@ public class Runner implements AutoCloseable {
|
||||
|
||||
private final BesuController<?> besuController;
|
||||
private final Path dataDir;
|
||||
private final Optional<StratumServer> stratumServer;
|
||||
|
||||
Runner(
|
||||
final Vertx vertx,
|
||||
@@ -64,6 +66,7 @@ public class Runner implements AutoCloseable {
|
||||
final Optional<JsonRpcHttpService> jsonRpc,
|
||||
final Optional<GraphQLHttpService> graphQLHttp,
|
||||
final Optional<WebSocketService> websocketRpc,
|
||||
final Optional<StratumServer> stratumServer,
|
||||
final Optional<MetricsService> metrics,
|
||||
final BesuController<?> besuController,
|
||||
final Path dataDir) {
|
||||
@@ -76,6 +79,7 @@ public class Runner implements AutoCloseable {
|
||||
this.metrics = metrics;
|
||||
this.besuController = besuController;
|
||||
this.dataDir = dataDir;
|
||||
this.stratumServer = stratumServer;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -87,6 +91,7 @@ public class Runner implements AutoCloseable {
|
||||
besuController.getSynchronizer().start();
|
||||
}
|
||||
besuController.getMiningCoordinator().start();
|
||||
stratumServer.ifPresent(server -> waitForServiceToStart("stratum", server.start()));
|
||||
vertx.setPeriodic(
|
||||
TimeUnit.MINUTES.toMillis(1),
|
||||
time ->
|
||||
@@ -111,6 +116,7 @@ public class Runner implements AutoCloseable {
|
||||
|
||||
besuController.getMiningCoordinator().stop();
|
||||
waitForServiceToStop("Mining Coordinator", besuController.getMiningCoordinator()::awaitStop);
|
||||
stratumServer.ifPresent(server -> waitForServiceToStop("Stratum", server::stop));
|
||||
if (networkRunner.getNetwork().isP2pEnabled()) {
|
||||
besuController.getSynchronizer().stop();
|
||||
waitForServiceToStop("Synchronizer", besuController.getSynchronizer()::awaitStop);
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.syncing.
|
||||
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
|
||||
import org.hyperledger.besu.ethereum.chain.Blockchain;
|
||||
import org.hyperledger.besu.ethereum.core.MiningParameters;
|
||||
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
|
||||
import org.hyperledger.besu.ethereum.core.Synchronizer;
|
||||
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
|
||||
@@ -79,6 +80,7 @@ import org.hyperledger.besu.ethereum.permissioning.account.AccountPermissioningC
|
||||
import org.hyperledger.besu.ethereum.permissioning.node.InsufficientPeersPermissioningProvider;
|
||||
import org.hyperledger.besu.ethereum.permissioning.node.NodePermissioningController;
|
||||
import org.hyperledger.besu.ethereum.permissioning.node.PeerPermissionsAdapter;
|
||||
import org.hyperledger.besu.ethereum.stratum.StratumServer;
|
||||
import org.hyperledger.besu.ethereum.transaction.TransactionSimulator;
|
||||
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
|
||||
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
|
||||
@@ -360,6 +362,19 @@ public class RunnerBuilder {
|
||||
|
||||
final P2PNetwork peerNetwork = networkRunner.getNetwork();
|
||||
|
||||
final MiningParameters miningParameters = besuController.getMiningParameters();
|
||||
Optional<StratumServer> stratumServer = Optional.empty();
|
||||
if (miningParameters.isStratumMiningEnabled()) {
|
||||
stratumServer =
|
||||
Optional.of(
|
||||
new StratumServer(
|
||||
vertx,
|
||||
miningParameters.getStratumPort(),
|
||||
miningParameters.getStratumNetworkInterface(),
|
||||
miningParameters.getStratumExtranonce()));
|
||||
miningCoordinator.addEthHashObserver(stratumServer.get());
|
||||
}
|
||||
|
||||
staticNodes.stream()
|
||||
.map(DefaultPeer::fromEnodeURL)
|
||||
.forEach(peerNetwork::addMaintainConnectionPeer);
|
||||
@@ -484,6 +499,7 @@ public class RunnerBuilder {
|
||||
jsonRpcHttpService,
|
||||
graphQLHttpService,
|
||||
webSocketService,
|
||||
stratumServer,
|
||||
metricsService,
|
||||
besuController,
|
||||
dataDir);
|
||||
|
||||
@@ -562,6 +562,29 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
|
||||
description = "Set if node will perform mining (default: ${DEFAULT-VALUE})")
|
||||
private final Boolean isMiningEnabled = false;
|
||||
|
||||
@Option(
|
||||
names = {"--miner-stratum-enabled"},
|
||||
description = "Set if node will perform Stratum mining (default: ${DEFAULT-VALUE})")
|
||||
private final Boolean iStratumMiningEnabled = false;
|
||||
|
||||
@SuppressWarnings("FieldMayBeFinal") // Because PicoCLI requires Strings to not be final.
|
||||
@Option(
|
||||
names = {"--miner-stratum-host"},
|
||||
description = "Host for Stratum network mining service (default: ${DEFAULT-VALUE})")
|
||||
private String stratumNetworkInterface = "0.0.0.0";
|
||||
|
||||
@Option(
|
||||
names = {"--miner-stratum-port"},
|
||||
description = "Stratum port binding (default: ${DEFAULT-VALUE})")
|
||||
private final Integer stratumPort = 8008;
|
||||
|
||||
@SuppressWarnings("FieldMayBeFinal") // Because PicoCLI requires Strings to not be final.
|
||||
@Option(
|
||||
hidden = true,
|
||||
names = {"--Xminer-stratum-extranonce"},
|
||||
description = "Extranonce for Stratum network miners (default: ${DEFAULT-VALUE})")
|
||||
private String stratumExtranonce = "080c";
|
||||
|
||||
@Option(
|
||||
names = {"--miner-coinbase"},
|
||||
description =
|
||||
@@ -940,14 +963,20 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void validateMiningParams() {
|
||||
// noinspection ConstantConditions
|
||||
if (isMiningEnabled && coinbase == null) {
|
||||
throw new ParameterException(
|
||||
this.commandLine,
|
||||
"Unable to mine without a valid coinbase. Either disable mining (remove --miner-enabled)"
|
||||
+ "or specify the beneficiary of mining (via --miner-coinbase <Address>)");
|
||||
}
|
||||
if (!isMiningEnabled && iStratumMiningEnabled) {
|
||||
throw new ParameterException(
|
||||
this.commandLine,
|
||||
"Unable to mine with Stratum if mining is disabled. Either disable Stratum mining (remove --miner-stratum-enabled)"
|
||||
+ "or specify mining is enabled (--miner-enabled)");
|
||||
}
|
||||
}
|
||||
|
||||
protected void validateP2PInterface(final String p2pInterface) {
|
||||
@@ -984,7 +1013,11 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
|
||||
commandLine,
|
||||
"--miner-enabled",
|
||||
!isMiningEnabled,
|
||||
asList("--miner-coinbase", "--min-gas-price", "--miner-extra-data"));
|
||||
asList(
|
||||
"--miner-coinbase",
|
||||
"--min-gas-price",
|
||||
"--miner-extra-data",
|
||||
"--miner-stratum-enabled"));
|
||||
|
||||
CommandLineUtils.checkOptionDependencies(
|
||||
logger,
|
||||
@@ -1058,7 +1091,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
|
||||
.ethProtocolConfiguration(ethProtocolOptions.toDomainObject())
|
||||
.dataDirectory(dataDir())
|
||||
.miningParameters(
|
||||
new MiningParameters(coinbase, minTransactionGasPrice, extraData, isMiningEnabled))
|
||||
new MiningParameters(
|
||||
coinbase,
|
||||
minTransactionGasPrice,
|
||||
extraData,
|
||||
isMiningEnabled,
|
||||
iStratumMiningEnabled,
|
||||
stratumNetworkInterface,
|
||||
stratumPort,
|
||||
stratumExtranonce))
|
||||
.transactionPoolConfiguration(buildTransactionPoolConfiguration())
|
||||
.nodePrivateKeyFile(nodePrivateKeyFile())
|
||||
.metricsSystem(metricsSystem.get())
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethods;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
|
||||
import org.hyperledger.besu.ethereum.core.MiningParameters;
|
||||
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
|
||||
import org.hyperledger.besu.ethereum.core.Synchronizer;
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManager;
|
||||
@@ -58,6 +59,7 @@ public class BesuController<C> implements java.io.Closeable {
|
||||
private final MiningCoordinator miningCoordinator;
|
||||
private final PrivacyParameters privacyParameters;
|
||||
private final List<Closeable> closeables;
|
||||
private final MiningParameters miningParameters;
|
||||
private final PluginServiceFactory additionalPluginServices;
|
||||
private final SyncState syncState;
|
||||
|
||||
@@ -72,6 +74,7 @@ public class BesuController<C> implements java.io.Closeable {
|
||||
final TransactionPool transactionPool,
|
||||
final MiningCoordinator miningCoordinator,
|
||||
final PrivacyParameters privacyParameters,
|
||||
final MiningParameters miningParameters,
|
||||
final JsonRpcMethods additionalJsonRpcMethodsFactory,
|
||||
final KeyPair keyPair,
|
||||
final List<Closeable> closeables,
|
||||
@@ -89,6 +92,7 @@ public class BesuController<C> implements java.io.Closeable {
|
||||
this.miningCoordinator = miningCoordinator;
|
||||
this.privacyParameters = privacyParameters;
|
||||
this.closeables = closeables;
|
||||
this.miningParameters = miningParameters;
|
||||
this.additionalPluginServices = additionalPluginServices;
|
||||
}
|
||||
|
||||
@@ -145,6 +149,10 @@ public class BesuController<C> implements java.io.Closeable {
|
||||
return privacyParameters;
|
||||
}
|
||||
|
||||
public MiningParameters getMiningParameters() {
|
||||
return miningParameters;
|
||||
}
|
||||
|
||||
public Map<String, JsonRpcMethod> getAdditionalJsonRpcMethods(
|
||||
final Collection<RpcApi> enabledRpcApis) {
|
||||
return additionalJsonRpcMethodsFactory.create(enabledRpcApis);
|
||||
|
||||
@@ -306,6 +306,7 @@ public abstract class BesuControllerBuilder<C> {
|
||||
transactionPool,
|
||||
miningCoordinator,
|
||||
privacyParameters,
|
||||
miningParameters,
|
||||
additionalJsonRpcMethodFactory,
|
||||
nodeKeys,
|
||||
closeables,
|
||||
|
||||
@@ -54,6 +54,7 @@ public class MainnetBesuControllerBuilder extends BesuControllerBuilder<Void> {
|
||||
final EthHashMiningCoordinator miningCoordinator =
|
||||
new EthHashMiningCoordinator(protocolContext.getBlockchain(), executor, syncState);
|
||||
miningCoordinator.addMinedBlockObserver(ethProtocolManager);
|
||||
miningCoordinator.setStratumMiningEnabled(miningParameters.isStratumMiningEnabled());
|
||||
if (miningParameters.isMiningEnabled()) {
|
||||
miningCoordinator.enable();
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ public abstract class JsonBlockImporterTest {
|
||||
.miningParameters(
|
||||
new MiningParametersTestBuilder()
|
||||
.minTransactionGasPrice(Wei.ZERO)
|
||||
.enabled(false)
|
||||
.enabled(true)
|
||||
.build())
|
||||
.nodeKeys(KeyPair.generate())
|
||||
.metricsSystem(new NoOpMetricsSystem())
|
||||
|
||||
@@ -2284,6 +2284,25 @@ public class BesuCommandTest extends CommandTestAbstract {
|
||||
.isEqualTo(Optional.of(Address.fromHexString(coinbaseStr)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stratumMiningIsEnabledWhenSpecified() throws Exception {
|
||||
final String coinbaseStr = String.format("%040x", 1);
|
||||
parseCommand("--miner-enabled", "--miner-coinbase=" + coinbaseStr, "--miner-stratum-enabled");
|
||||
|
||||
final ArgumentCaptor<MiningParameters> miningArg =
|
||||
ArgumentCaptor.forClass(MiningParameters.class);
|
||||
|
||||
verify(mockControllerBuilder).miningParameters(miningArg.capture());
|
||||
verify(mockControllerBuilder).build();
|
||||
|
||||
assertThat(commandOutput.toString()).isEmpty();
|
||||
assertThat(commandErrorOutput.toString()).isEmpty();
|
||||
assertThat(miningArg.getValue().isMiningEnabled()).isTrue();
|
||||
assertThat(miningArg.getValue().getCoinbase())
|
||||
.isEqualTo(Optional.of(Address.fromHexString(coinbaseStr)));
|
||||
assertThat(miningArg.getValue().isStratumMiningEnabled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void miningOptionsRequiresServiceToBeEnabled() {
|
||||
|
||||
@@ -2294,13 +2313,20 @@ public class BesuCommandTest extends CommandTestAbstract {
|
||||
"--min-gas-price",
|
||||
"42",
|
||||
"--miner-extra-data",
|
||||
"0x1122334455667788990011223344556677889900112233445566778899001122");
|
||||
"0x1122334455667788990011223344556677889900112233445566778899001122",
|
||||
"--miner-stratum-enabled");
|
||||
|
||||
verifyOptionsConstraintLoggerCall(
|
||||
"--miner-enabled", "--miner-coinbase", "--min-gas-price", "--miner-extra-data");
|
||||
"--miner-enabled",
|
||||
"--miner-coinbase",
|
||||
"--min-gas-price",
|
||||
"--miner-extra-data",
|
||||
"--miner-stratum-enabled");
|
||||
|
||||
assertThat(commandOutput.toString()).isEmpty();
|
||||
assertThat(commandErrorOutput.toString()).isEmpty();
|
||||
assertThat(commandErrorOutput.toString())
|
||||
.startsWith(
|
||||
"Unable to mine with Stratum if mining is disabled. Either disable Stratum mining (remove --miner-stratum-enabled)or specify mining is enabled (--miner-enabled)");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -79,9 +79,12 @@ metrics-push-prometheus-job="besu-everything"
|
||||
|
||||
# Mining
|
||||
miner-enabled=false
|
||||
miner-stratum-enabled=false
|
||||
miner-coinbase="0x0000000000000000000000000000000000000002"
|
||||
miner-extra-data="0x444F4E27542050414E4943202120484F444C2C20484F444C2C20484F444C2021"
|
||||
min-gas-price=1
|
||||
miner-stratum-host="0.0.0.0"
|
||||
miner-stratum-port=8008
|
||||
|
||||
# Pruning
|
||||
pruning-enabled=true
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.hyperledger.besu.crypto.SECP256K1.KeyPair;
|
||||
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.AbstractBlockScheduler;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.AbstractMinerExecutor;
|
||||
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
|
||||
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
|
||||
import org.hyperledger.besu.ethereum.core.Address;
|
||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||
@@ -69,7 +70,9 @@ public class CliqueMinerExecutor extends AbstractMinerExecutor<CliqueContext, Cl
|
||||
|
||||
@Override
|
||||
public CliqueBlockMiner createMiner(
|
||||
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
|
||||
final Subscribers<MinedBlockObserver> observers,
|
||||
final Subscribers<EthHashObserver> ethHashObservers,
|
||||
final BlockHeader parentHeader) {
|
||||
final Function<BlockHeader, CliqueBlockCreator> blockCreator =
|
||||
(header) ->
|
||||
new CliqueBlockCreator(
|
||||
|
||||
@@ -90,7 +90,7 @@ public class CliqueMiningCoordinatorTest {
|
||||
|
||||
when(protocolContext.getConsensusState()).thenReturn(cliqueContext);
|
||||
when(protocolContext.getBlockchain()).thenReturn(blockChain);
|
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
when(syncState.isInSync()).thenReturn(true);
|
||||
|
||||
miningTracker = new CliqueMiningTracker(proposerAddress, protocolContext);
|
||||
@@ -111,7 +111,7 @@ public class CliqueMiningCoordinatorTest {
|
||||
coordinator.enable();
|
||||
coordinator.start();
|
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
|
||||
|
||||
reset(minerExecutor);
|
||||
|
||||
@@ -121,7 +121,7 @@ public class CliqueMiningCoordinatorTest {
|
||||
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
verify(minerExecutor, never()).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, never()).startAsyncMining(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -138,10 +138,10 @@ public class CliqueMiningCoordinatorTest {
|
||||
coordinator.enable();
|
||||
coordinator.start();
|
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
|
||||
|
||||
reset(minerExecutor);
|
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
|
||||
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys);
|
||||
|
||||
@@ -150,7 +150,7 @@ public class CliqueMiningCoordinatorTest {
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
|
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
|
||||
}
|
||||
|
||||
@@ -169,10 +169,10 @@ public class CliqueMiningCoordinatorTest {
|
||||
coordinator.enable();
|
||||
coordinator.start();
|
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
|
||||
|
||||
reset(minerExecutor);
|
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
|
||||
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys);
|
||||
|
||||
@@ -181,7 +181,7 @@ public class CliqueMiningCoordinatorTest {
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
|
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
|
||||
}
|
||||
|
||||
@@ -200,10 +200,10 @@ public class CliqueMiningCoordinatorTest {
|
||||
coordinator.enable();
|
||||
coordinator.start();
|
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
|
||||
|
||||
reset(minerExecutor);
|
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
|
||||
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys);
|
||||
|
||||
@@ -212,7 +212,7 @@ public class CliqueMiningCoordinatorTest {
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
|
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
|
||||
}
|
||||
|
||||
@@ -226,10 +226,10 @@ public class CliqueMiningCoordinatorTest {
|
||||
coordinator.enable();
|
||||
coordinator.start();
|
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), any());
|
||||
|
||||
reset(minerExecutor);
|
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
|
||||
final Block importedBlock = createEmptyBlock(1, blockChain.getChainHeadHash(), proposerKeys);
|
||||
blockChain.appendBlock(importedBlock, Lists.emptyList());
|
||||
@@ -237,7 +237,7 @@ public class CliqueMiningCoordinatorTest {
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class);
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture());
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any(), varArgs.capture());
|
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader());
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ public enum RpcMethod {
|
||||
ETH_PROTOCOL_VERSION("eth_protocolVersion"),
|
||||
ETH_SEND_RAW_TRANSACTION("eth_sendRawTransaction"),
|
||||
ETH_SEND_TRANSACTION("eth_sendTransaction"),
|
||||
ETH_SUBMIT_WORK("eth_submitWork"),
|
||||
ETH_SUBSCRIBE("eth_subscribe"),
|
||||
ETH_SYNCING("eth_syncing"),
|
||||
ETH_UNINSTALL_FILTER("eth_uninstallFilter"),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
|
||||
import org.hyperledger.besu.ethereum.core.Hash;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class EthSubmitWork implements JsonRpcMethod {
|
||||
|
||||
private final MiningCoordinator miner;
|
||||
private final JsonRpcParameter parameters;
|
||||
private static final Logger LOG = getLogger();
|
||||
|
||||
public EthSubmitWork(final MiningCoordinator miner, final JsonRpcParameter parameters) {
|
||||
this.miner = miner;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return RpcMethod.ETH_SUBMIT_WORK.getMethodName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonRpcResponse response(final JsonRpcRequest req) {
|
||||
final Optional<EthHashSolverInputs> solver = miner.getWorkDefinition();
|
||||
if (solver.isPresent()) {
|
||||
final EthHashSolution solution =
|
||||
new EthHashSolution(
|
||||
BytesValue.fromHexString(parameters.required(req.getParams(), 0, String.class))
|
||||
.getLong(0),
|
||||
parameters.required(req.getParams(), 2, Hash.class),
|
||||
BytesValue.fromHexString(parameters.required(req.getParams(), 1, String.class))
|
||||
.getArrayUnsafe());
|
||||
final boolean result = miner.submitWork(solution);
|
||||
return new JsonRpcSuccessResponse(req.getId(), result);
|
||||
} else {
|
||||
LOG.trace("Mining is not operational, eth_submitWork request cannot be processed");
|
||||
return new JsonRpcErrorResponse(req.getId(), JsonRpcError.NO_MINING_WORK_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthNewPendingT
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthProtocolVersion;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSendRawTransaction;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSendTransaction;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSubmitWork;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSyncing;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthUninstallFilter;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
|
||||
@@ -157,6 +158,7 @@ public class EthJsonRpcMethods extends ApiGroupJsonRpcMethods {
|
||||
new EthProtocolVersion(supportedCapabilities),
|
||||
new EthGasPrice(miningCoordinator),
|
||||
new EthGetWork(miningCoordinator),
|
||||
new EthSubmitWork(miningCoordinator, parameter),
|
||||
new EthHashrate(miningCoordinator),
|
||||
new EthChainId(protocolSchedule.getChainId()));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.EthHashMiningCoordinator;
|
||||
import org.hyperledger.besu.ethereum.core.Hash;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
import org.hyperledger.besu.util.bytes.BytesValues;
|
||||
import org.hyperledger.besu.util.uint.UInt256;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class EthSubmitWorkTest {
|
||||
|
||||
private EthSubmitWork method;
|
||||
private final String ETH_METHOD = "eth_submitWork";
|
||||
private final String hexValue =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
|
||||
|
||||
@Mock private EthHashMiningCoordinator miningCoordinator;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
method = new EthSubmitWork(miningCoordinator, new JsonRpcParameter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnCorrectMethodName() {
|
||||
assertThat(method.getName()).isEqualTo(ETH_METHOD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailIfNoMiningEnabled() {
|
||||
final JsonRpcRequest request = requestWithParams();
|
||||
final JsonRpcResponse actualResponse = method.response(request);
|
||||
final JsonRpcResponse expectedResponse =
|
||||
new JsonRpcErrorResponse(request.getId(), JsonRpcError.NO_MINING_WORK_FOUND);
|
||||
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailIfMissingArguments() {
|
||||
final JsonRpcRequest request = requestWithParams();
|
||||
final EthHashSolverInputs values =
|
||||
new EthHashSolverInputs(
|
||||
UInt256.fromHexString(hexValue), BaseEncoding.base16().lowerCase().decode(hexValue), 0);
|
||||
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(values));
|
||||
assertThatThrownBy(
|
||||
() -> method.response(request), "Missing required json rpc parameter at index 0")
|
||||
.isInstanceOf(InvalidJsonRpcParameters.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnTrueIfGivenCorrectResult() {
|
||||
final EthHashSolverInputs firstInputs =
|
||||
new EthHashSolverInputs(
|
||||
UInt256.fromHexString(
|
||||
"0x0083126e978d4fdf3b645a1cac083126e978d4fdf3b645a1cac083126e978d4f"),
|
||||
new byte[] {
|
||||
15, -114, -104, 87, -95, -36, -17, 120, 52, 1, 124, 61, -6, -66, 78, -27, -57, 118,
|
||||
-18, -64, -103, -91, -74, -121, 42, 91, -14, -98, 101, 86, -43, -51
|
||||
},
|
||||
468);
|
||||
|
||||
final EthHashSolution expectedFirstOutput =
|
||||
new EthHashSolution(
|
||||
-6506032554016940193L,
|
||||
Hash.fromHexString(
|
||||
"0xc5e3c33c86d64d0641dd3c86e8ce4628fe0aac0ef7b4c087c5fcaa45d5046d90"),
|
||||
firstInputs.getPrePowHash());
|
||||
final JsonRpcRequest request =
|
||||
requestWithParams(
|
||||
BytesValues.toMinimalBytes(expectedFirstOutput.getNonce()).getHexString(),
|
||||
BytesValue.wrap(expectedFirstOutput.getPowHash()).getHexString(),
|
||||
expectedFirstOutput.getMixHash().getHexString());
|
||||
final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), true);
|
||||
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(firstInputs));
|
||||
// potentially could use a real miner here.
|
||||
when(miningCoordinator.submitWork(expectedFirstOutput)).thenReturn(true);
|
||||
|
||||
final JsonRpcResponse actualResponse = method.response(request);
|
||||
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnErrorOnNoneMiningNode() {
|
||||
final JsonRpcRequest request = requestWithParams();
|
||||
final JsonRpcResponse expectedResponse =
|
||||
new JsonRpcErrorResponse(request.getId(), JsonRpcError.NO_MINING_WORK_FOUND);
|
||||
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.empty());
|
||||
|
||||
final JsonRpcResponse actualResponse = method.response(request);
|
||||
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse);
|
||||
}
|
||||
|
||||
private JsonRpcRequest requestWithParams(final Object... params) {
|
||||
return new JsonRpcRequest("2.0", ETH_METHOD, params);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
package org.hyperledger.besu.ethereum.blockcreation;
|
||||
|
||||
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
|
||||
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
|
||||
import org.hyperledger.besu.ethereum.core.Address;
|
||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||
@@ -70,9 +71,11 @@ public abstract class AbstractMinerExecutor<
|
||||
}
|
||||
|
||||
public Optional<M> startAsyncMining(
|
||||
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
|
||||
final Subscribers<MinedBlockObserver> observers,
|
||||
final Subscribers<EthHashObserver> ethHashObservers,
|
||||
final BlockHeader parentHeader) {
|
||||
try {
|
||||
final M currentRunningMiner = createMiner(observers, parentHeader);
|
||||
final M currentRunningMiner = createMiner(observers, ethHashObservers, parentHeader);
|
||||
executorService.execute(currentRunningMiner);
|
||||
return Optional.of(currentRunningMiner);
|
||||
} catch (RejectedExecutionException e) {
|
||||
@@ -94,7 +97,9 @@ public abstract class AbstractMinerExecutor<
|
||||
}
|
||||
|
||||
public abstract M createMiner(
|
||||
final Subscribers<MinedBlockObserver> subscribers, final BlockHeader parentHeader);
|
||||
final Subscribers<MinedBlockObserver> subscribers,
|
||||
final Subscribers<EthHashObserver> ethHashObservers,
|
||||
final BlockHeader parentHeader);
|
||||
|
||||
public void setExtraData(final BytesValue extraData) {
|
||||
this.extraData = extraData.copy();
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
import org.hyperledger.besu.ethereum.chain.BlockAddedEvent;
|
||||
import org.hyperledger.besu.ethereum.chain.BlockAddedObserver;
|
||||
import org.hyperledger.besu.ethereum.chain.Blockchain;
|
||||
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
|
||||
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
|
||||
import org.hyperledger.besu.ethereum.core.Address;
|
||||
import org.hyperledger.besu.ethereum.core.Block;
|
||||
@@ -48,6 +49,7 @@ public abstract class AbstractMiningCoordinator<
|
||||
private static final Logger LOG = getLogger();
|
||||
|
||||
private final Subscribers<MinedBlockObserver> minedBlockObservers = Subscribers.create();
|
||||
private final Subscribers<EthHashObserver> ethHashObservers = Subscribers.create();
|
||||
private final AbstractMinerExecutor<C, M> executor;
|
||||
private final SyncState syncState;
|
||||
private final Blockchain blockchain;
|
||||
@@ -72,7 +74,7 @@ public abstract class AbstractMiningCoordinator<
|
||||
final BlockHeader parentHeader,
|
||||
final List<Transaction> transactions,
|
||||
final List<BlockHeader> ommers) {
|
||||
final M miner = executor.createMiner(Subscribers.none(), parentHeader);
|
||||
final M miner = executor.createMiner(minedBlockObservers, ethHashObservers, parentHeader);
|
||||
return Optional.of(miner.createBlock(parentHeader, transactions, ommers));
|
||||
}
|
||||
|
||||
@@ -146,7 +148,8 @@ public abstract class AbstractMiningCoordinator<
|
||||
|
||||
private void startAsyncMiningOperation() {
|
||||
final BlockHeader parentHeader = blockchain.getChainHeadHeader();
|
||||
currentRunningMiner = executor.startAsyncMining(minedBlockObservers, parentHeader);
|
||||
currentRunningMiner =
|
||||
executor.startAsyncMining(minedBlockObservers, ethHashObservers, parentHeader);
|
||||
}
|
||||
|
||||
private synchronized boolean haltCurrentMiningOperation() {
|
||||
@@ -190,6 +193,11 @@ public abstract class AbstractMiningCoordinator<
|
||||
minedBlockObservers.subscribe(obs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEthHashObserver(final EthHashObserver obs) {
|
||||
ethHashObservers.subscribe(obs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wei getMinTransactionGasPrice() {
|
||||
return executor.getMinTransactionGasPrice();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package org.hyperledger.besu.ethereum.blockcreation;
|
||||
|
||||
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
|
||||
import org.hyperledger.besu.ethereum.chain.MinedBlockObserver;
|
||||
import org.hyperledger.besu.ethereum.core.Address;
|
||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||
@@ -31,6 +32,7 @@ import java.util.function.Function;
|
||||
public class EthHashMinerExecutor extends AbstractMinerExecutor<Void, EthHashBlockMiner> {
|
||||
|
||||
private volatile Optional<Address> coinbase;
|
||||
private boolean stratumMiningEnabled;
|
||||
|
||||
public EthHashMinerExecutor(
|
||||
final ProtocolContext<Void> protocolContext,
|
||||
@@ -51,18 +53,26 @@ public class EthHashMinerExecutor extends AbstractMinerExecutor<Void, EthHashBlo
|
||||
|
||||
@Override
|
||||
public Optional<EthHashBlockMiner> startAsyncMining(
|
||||
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
|
||||
final Subscribers<MinedBlockObserver> observers,
|
||||
final Subscribers<EthHashObserver> ethHashObservers,
|
||||
final BlockHeader parentHeader) {
|
||||
if (!coinbase.isPresent()) {
|
||||
throw new CoinbaseNotSetException("Unable to start mining without a coinbase.");
|
||||
}
|
||||
return super.startAsyncMining(observers, parentHeader);
|
||||
return super.startAsyncMining(observers, ethHashObservers, parentHeader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EthHashBlockMiner createMiner(
|
||||
final Subscribers<MinedBlockObserver> observers, final BlockHeader parentHeader) {
|
||||
final Subscribers<MinedBlockObserver> observers,
|
||||
final Subscribers<EthHashObserver> ethHashObservers,
|
||||
final BlockHeader parentHeader) {
|
||||
final EthHashSolver solver =
|
||||
new EthHashSolver(new RandomNonceGenerator(), new EthHasher.Light());
|
||||
new EthHashSolver(
|
||||
new RandomNonceGenerator(),
|
||||
new EthHasher.Light(),
|
||||
stratumMiningEnabled,
|
||||
ethHashObservers);
|
||||
final Function<BlockHeader, EthHashBlockCreator> blockCreator =
|
||||
(header) ->
|
||||
new EthHashBlockCreator(
|
||||
@@ -88,6 +98,10 @@ public class EthHashMinerExecutor extends AbstractMinerExecutor<Void, EthHashBlo
|
||||
}
|
||||
}
|
||||
|
||||
void setStratumMiningEnabled(final boolean stratumMiningEnabled) {
|
||||
this.stratumMiningEnabled = stratumMiningEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Address> getCoinbase() {
|
||||
return coinbase;
|
||||
|
||||
@@ -45,6 +45,10 @@ public class EthHashMiningCoordinator extends AbstractMiningCoordinator<Void, Et
|
||||
executor.setCoinbase(coinbase);
|
||||
}
|
||||
|
||||
public void setStratumMiningEnabled(final boolean stratumMiningEnabled) {
|
||||
executor.setStratumMiningEnabled(stratumMiningEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Long> hashesPerSecond() {
|
||||
final Optional<Long> currentHashesPerSecond =
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.blockcreation;
|
||||
|
||||
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
|
||||
import org.hyperledger.besu.ethereum.core.Address;
|
||||
import org.hyperledger.besu.ethereum.core.Block;
|
||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||
@@ -87,4 +88,6 @@ public interface MiningCoordinator {
|
||||
final BlockHeader parentHeader,
|
||||
final List<Transaction> transactions,
|
||||
final List<BlockHeader> ommers);
|
||||
|
||||
default void addEthHashObserver(final EthHashObserver observer) {}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class AbstractMiningCoordinatorTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
when(minerExecutor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(blockMiner));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,7 +68,7 @@ public class AbstractMiningCoordinatorTest {
|
||||
when(syncState.isInSync()).thenReturn(true);
|
||||
miningCoordinator.enable();
|
||||
miningCoordinator.start();
|
||||
verify(minerExecutor).startAsyncMining(any(), any());
|
||||
verify(minerExecutor).startAsyncMining(any(), any(), any());
|
||||
verifyNoMoreInteractions(minerExecutor, blockMiner);
|
||||
}
|
||||
|
||||
@@ -77,12 +77,12 @@ public class AbstractMiningCoordinatorTest {
|
||||
when(syncState.isInSync()).thenReturn(false);
|
||||
miningCoordinator.enable();
|
||||
miningCoordinator.start();
|
||||
verify(minerExecutor, never()).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, never()).startAsyncMining(any(), any(), any());
|
||||
|
||||
when(syncState.isInSync()).thenReturn(true);
|
||||
miningCoordinator.inSyncChanged(true);
|
||||
|
||||
verify(minerExecutor).startAsyncMining(any(), any());
|
||||
verify(minerExecutor).startAsyncMining(any(), any(), any());
|
||||
verifyNoMoreInteractions(minerExecutor, blockMiner);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public class AbstractMiningCoordinatorTest {
|
||||
when(syncState.isInSync()).thenReturn(true);
|
||||
miningCoordinator.enable();
|
||||
miningCoordinator.start();
|
||||
verify(minerExecutor).startAsyncMining(any(), any());
|
||||
verify(minerExecutor).startAsyncMining(any(), any(), any());
|
||||
|
||||
miningCoordinator.inSyncChanged(false);
|
||||
|
||||
@@ -121,7 +121,7 @@ public class AbstractMiningCoordinatorTest {
|
||||
BlockAddedEvent.createForHeadAdvancement(BLOCK, Collections.emptyList()), blockchain);
|
||||
|
||||
verify(blockMiner).cancel();
|
||||
verify(minerExecutor, times(2)).startAsyncMining(any(), any());
|
||||
verify(minerExecutor, times(2)).startAsyncMining(any(), any(), any());
|
||||
|
||||
verifyNoMoreInteractions(minerExecutor, blockMiner);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.hyperledger.besu.ethereum.mainnet.ValidationTestUtils;
|
||||
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
|
||||
import org.hyperledger.besu.plugin.services.MetricsSystem;
|
||||
import org.hyperledger.besu.testutil.TestClock;
|
||||
import org.hyperledger.besu.util.Subscribers;
|
||||
import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
import org.hyperledger.besu.util.uint.UInt256;
|
||||
|
||||
@@ -74,7 +75,8 @@ public class EthHashBlockCreatorTest {
|
||||
.build();
|
||||
|
||||
final EthHashSolver solver =
|
||||
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
|
||||
new EthHashSolver(
|
||||
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
|
||||
|
||||
final PendingTransactions pendingTransactions =
|
||||
new PendingTransactions(
|
||||
@@ -122,7 +124,8 @@ public class EthHashBlockCreatorTest {
|
||||
.build();
|
||||
|
||||
final EthHashSolver solver =
|
||||
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
|
||||
new EthHashSolver(
|
||||
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
|
||||
|
||||
final PendingTransactions pendingTransactions =
|
||||
new PendingTransactions(
|
||||
@@ -165,7 +168,8 @@ public class EthHashBlockCreatorTest {
|
||||
.build();
|
||||
|
||||
final EthHashSolver solver =
|
||||
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
|
||||
new EthHashSolver(
|
||||
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
|
||||
|
||||
final PendingTransactions pendingTransactions =
|
||||
new PendingTransactions(
|
||||
@@ -224,7 +228,8 @@ public class EthHashBlockCreatorTest {
|
||||
.build();
|
||||
|
||||
final EthHashSolver solver =
|
||||
new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light());
|
||||
new EthHashSolver(
|
||||
Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light(), false, Subscribers.none());
|
||||
|
||||
final PendingTransactions pendingTransactions =
|
||||
new PendingTransactions(
|
||||
|
||||
@@ -54,7 +54,7 @@ public class EthHashMinerExecutorTest {
|
||||
Function.identity());
|
||||
|
||||
assertThatExceptionOfType(CoinbaseNotSetException.class)
|
||||
.isThrownBy(() -> executor.startAsyncMining(Subscribers.create(), null))
|
||||
.isThrownBy(() -> executor.startAsyncMining(Subscribers.create(), Subscribers.none(), null))
|
||||
.withMessageContaining("Unable to start mining without a coinbase.");
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public class EthHashMiningCoordinatorTest {
|
||||
|
||||
when(miner.getHashesPerSecond()).thenReturn(hashRate1, hashRate2, hashRate3);
|
||||
|
||||
when(executor.startAsyncMining(any(), any())).thenReturn(Optional.of(miner));
|
||||
when(executor.startAsyncMining(any(), any(), any())).thenReturn(Optional.of(miner));
|
||||
|
||||
final EthHashMiningCoordinator miningCoordinator =
|
||||
new EthHashMiningCoordinator(executionContext.getBlockchain(), executor, syncState);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.chain;
|
||||
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/** Observer of new work for the EthHash PoW algorithm */
|
||||
public interface EthHashObserver {
|
||||
|
||||
/**
|
||||
* Send a new proof-of-work job to observers
|
||||
*
|
||||
* @param jobInput the proof-of-work job
|
||||
*/
|
||||
void newJob(EthHashSolverInputs jobInput);
|
||||
|
||||
/**
|
||||
* Sets a callback for the observer to provide solutions to jobs.
|
||||
*
|
||||
* @param submitSolutionCallback the callback to set on the observer, consuming a solution and
|
||||
* returning true if the solution is accepted, false if rejected.
|
||||
*/
|
||||
void setSubmitWorkCallback(Function<EthHashSolution, Boolean> submitSolutionCallback);
|
||||
}
|
||||
@@ -19,24 +19,42 @@ import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
public class MiningParameters {
|
||||
|
||||
private final Optional<Address> coinbase;
|
||||
private final Wei minTransactionGasPrice;
|
||||
private final BytesValue extraData;
|
||||
private final Boolean enabled;
|
||||
private final boolean enabled;
|
||||
private final boolean stratumMiningEnabled;
|
||||
private final String stratumNetworkInterface;
|
||||
private final int stratumPort;
|
||||
private final String stratumExtranonce;
|
||||
|
||||
public MiningParameters(
|
||||
final Address coinbase,
|
||||
final Wei minTransactionGasPrice,
|
||||
final BytesValue extraData,
|
||||
final Boolean enabled) {
|
||||
final boolean enabled) {
|
||||
this(coinbase, minTransactionGasPrice, extraData, enabled, false, "0.0.0.0", 8008, "080c");
|
||||
}
|
||||
|
||||
public MiningParameters(
|
||||
final Address coinbase,
|
||||
final Wei minTransactionGasPrice,
|
||||
final BytesValue extraData,
|
||||
final boolean enabled,
|
||||
final boolean stratumMiningEnabled,
|
||||
final String stratumNetworkInterface,
|
||||
final int stratumPort,
|
||||
final String stratumExtranonce) {
|
||||
this.coinbase = Optional.ofNullable(coinbase);
|
||||
this.minTransactionGasPrice = minTransactionGasPrice;
|
||||
this.extraData = extraData;
|
||||
this.enabled = enabled;
|
||||
this.stratumMiningEnabled = stratumMiningEnabled;
|
||||
this.stratumNetworkInterface = stratumNetworkInterface;
|
||||
this.stratumPort = stratumPort;
|
||||
this.stratumExtranonce = stratumExtranonce;
|
||||
}
|
||||
|
||||
public Optional<Address> getCoinbase() {
|
||||
@@ -51,37 +69,75 @@ public class MiningParameters {
|
||||
return extraData;
|
||||
}
|
||||
|
||||
public Boolean isMiningEnabled() {
|
||||
public boolean isMiningEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
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
|
||||
+ '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ package org.hyperledger.besu.ethereum.mainnet;
|
||||
|
||||
import org.hyperledger.besu.ethereum.core.Hash;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class EthHashSolution {
|
||||
private final long nonce;
|
||||
private final Hash mixHash;
|
||||
@@ -38,4 +41,21 @@ public class EthHashSolution {
|
||||
public byte[] getPowHash() {
|
||||
return powHash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
EthHashSolution that = (EthHashSolution) o;
|
||||
return nonce == that.nonce
|
||||
&& Objects.equals(mixHash, that.mixHash)
|
||||
&& Arrays.equals(powHash, that.powHash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(nonce, mixHash);
|
||||
result = 31 * result + Arrays.hashCode(powHash);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.mainnet;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
|
||||
import org.hyperledger.besu.ethereum.core.Hash;
|
||||
import org.hyperledger.besu.util.Subscribers;
|
||||
import org.hyperledger.besu.util.bytes.Bytes32;
|
||||
import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
import org.hyperledger.besu.util.uint.UInt256;
|
||||
@@ -26,9 +30,12 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class EthHashSolver {
|
||||
|
||||
private static final Logger LOG = getLogger();
|
||||
|
||||
public static class EthHashSolverJob {
|
||||
|
||||
private final EthHashSolverInputs inputs;
|
||||
@@ -74,18 +81,30 @@ public class EthHashSolver {
|
||||
private final Iterable<Long> nonceGenerator;
|
||||
private final EthHasher ethHasher;
|
||||
private volatile long hashesPerSecond = NO_MINING_CONDUCTED;
|
||||
|
||||
private final Boolean stratumMiningEnabled;
|
||||
private final Subscribers<EthHashObserver> ethHashObservers;
|
||||
private volatile Optional<EthHashSolverJob> currentJob = Optional.empty();
|
||||
|
||||
public EthHashSolver(final Iterable<Long> nonceGenerator, final EthHasher ethHasher) {
|
||||
public EthHashSolver(
|
||||
final Iterable<Long> nonceGenerator,
|
||||
final EthHasher ethHasher,
|
||||
final Boolean stratumMiningEnabled,
|
||||
final Subscribers<EthHashObserver> ethHashObservers) {
|
||||
this.nonceGenerator = nonceGenerator;
|
||||
this.ethHasher = ethHasher;
|
||||
this.stratumMiningEnabled = stratumMiningEnabled;
|
||||
this.ethHashObservers = ethHashObservers;
|
||||
ethHashObservers.forEach(observer -> observer.setSubmitWorkCallback(this::submitSolution));
|
||||
}
|
||||
|
||||
public EthHashSolution solveFor(final EthHashSolverJob job)
|
||||
throws InterruptedException, ExecutionException {
|
||||
currentJob = Optional.of(job);
|
||||
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<EthHashSolverJob> jobSnapshot = currentJob;
|
||||
if (jobSnapshot.isEmpty()) {
|
||||
LOG.debug("No current job, rejecting miner work");
|
||||
return false;
|
||||
}
|
||||
|
||||
final EthHashSolverJob job = jobSnapshot.get();
|
||||
final EthHashSolverInputs inputs = job.getInputs();
|
||||
if (!Arrays.equals(inputs.getPrePowHash(), solution.getPowHash())) {
|
||||
LOG.debug("Miner's solution does not match current job");
|
||||
return false;
|
||||
}
|
||||
final byte[] hashBuffer = new byte[64];
|
||||
@@ -153,9 +174,11 @@ public class EthHashSolver {
|
||||
testNonce(inputs, solution.getNonce(), hashBuffer);
|
||||
|
||||
if (calculatedSolution.isPresent()) {
|
||||
currentJob.get().solvedWith(solution);
|
||||
LOG.debug("Accepting a solution from a miner");
|
||||
currentJob.get().solvedWith(calculatedSolution.get());
|
||||
return true;
|
||||
}
|
||||
LOG.debug("Rejecting a solution from a miner");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ package org.hyperledger.besu.ethereum.mainnet;
|
||||
|
||||
import org.hyperledger.besu.util.uint.UInt256;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class EthHashSolverInputs {
|
||||
private final UInt256 target;
|
||||
private final byte[] prePowHash;
|
||||
@@ -39,4 +41,16 @@ public class EthHashSolverInputs {
|
||||
public long getBlockNumber() {
|
||||
return blockNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EthHashSolverInputs{"
|
||||
+ "target="
|
||||
+ target
|
||||
+ ", prePowHash="
|
||||
+ Arrays.toString(prePowHash)
|
||||
+ ", blockNumber="
|
||||
+ blockNumber
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import org.hyperledger.besu.ethereum.core.Hash;
|
||||
import org.hyperledger.besu.util.Subscribers;
|
||||
import org.hyperledger.besu.util.uint.UInt256;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -38,7 +39,8 @@ public class EthHashSolverTest {
|
||||
@Test
|
||||
public void emptyHashRateAndWorkDefinitionIsReportedPriorToSolverStarting() {
|
||||
final List<Long> noncesToTry = Arrays.asList(1L, 1L, 1L, 1L, 1L, 1L, 0L);
|
||||
final EthHashSolver solver = new EthHashSolver(noncesToTry, new EthHasher.Light());
|
||||
final EthHashSolver solver =
|
||||
new EthHashSolver(noncesToTry, new EthHasher.Light(), false, Subscribers.none());
|
||||
|
||||
assertThat(solver.hashesPerSecond()).isEqualTo(Optional.empty());
|
||||
assertThat(solver.getWorkDefinition()).isEqualTo(Optional.empty());
|
||||
@@ -60,7 +62,7 @@ public class EthHashSolverTest {
|
||||
.when(hasher)
|
||||
.hash(any(), anyLong(), anyLong(), any());
|
||||
|
||||
final EthHashSolver solver = new EthHashSolver(noncesToTry, hasher);
|
||||
final EthHashSolver solver = new EthHashSolver(noncesToTry, hasher, false, Subscribers.none());
|
||||
|
||||
final Stopwatch operationTimer = Stopwatch.createStarted();
|
||||
final EthHashSolverInputs inputs = new EthHashSolverInputs(UInt256.ONE, new byte[0], 5);
|
||||
@@ -118,7 +120,9 @@ public class EthHashSolverTest {
|
||||
final EthHashSolver solver =
|
||||
new EthHashSolver(
|
||||
Lists.newArrayList(expectedFirstOutput.getNonce(), 0L, expectedSecondOutput.getNonce()),
|
||||
new EthHasher.Light());
|
||||
new EthHasher.Light(),
|
||||
false,
|
||||
Subscribers.none());
|
||||
|
||||
EthHashSolution soln =
|
||||
solver.solveFor(EthHashSolver.EthHashSolverJob.createFromInputs(firstInputs));
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
|
||||
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
|
||||
import org.hyperledger.besu.plugin.services.MetricsSystem;
|
||||
import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage;
|
||||
import org.hyperledger.besu.util.Subscribers;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -148,8 +149,8 @@ public class RetestethContext {
|
||||
final Iterable<Long> nonceGenerator = new IncrementingNonceGenerator(0);
|
||||
ethHashSolver =
|
||||
("NoProof".equals(sealengine) || "NoReward".equals(sealEngine))
|
||||
? new EthHashSolver(nonceGenerator, NO_WORK_HASHER)
|
||||
: new EthHashSolver(nonceGenerator, new EthHasher.Light());
|
||||
? new EthHashSolver(nonceGenerator, NO_WORK_HASHER, false, Subscribers.none())
|
||||
: new EthHashSolver(nonceGenerator, new EthHasher.Light(), false, Subscribers.none());
|
||||
|
||||
blockReplay =
|
||||
new BlockReplay(
|
||||
|
||||
45
ethereum/stratum/build.gradle
Normal file
45
ethereum/stratum/build.gradle
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'java-library'
|
||||
|
||||
jar {
|
||||
baseName 'besu-ethereum-stratum'
|
||||
manifest {
|
||||
attributes(
|
||||
'Specification-Title': baseName,
|
||||
'Specification-Version': project.version,
|
||||
'Implementation-Title': baseName,
|
||||
'Implementation-Version': calculateVersion()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':plugin-api')
|
||||
api project(':util')
|
||||
|
||||
implementation project(':ethereum:api')
|
||||
implementation project(':ethereum:core')
|
||||
|
||||
implementation 'com.google.guava:guava'
|
||||
implementation 'io.vertx:vertx-core'
|
||||
|
||||
testImplementation project(':testutil')
|
||||
|
||||
testImplementation 'org.assertj:assertj-core'
|
||||
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
testImplementation 'junit:junit'
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.stratum;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import org.hyperledger.besu.ethereum.core.Hash;
|
||||
import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Implementation of the stratum1+tcp protocol.
|
||||
*
|
||||
* <p>This protocol allows miners to submit EthHash solutions over a persistent TCP connection.
|
||||
*/
|
||||
public class Stratum1EthProxyProtocol implements StratumProtocol {
|
||||
private static final Logger LOG = getLogger();
|
||||
private static final JsonMapper mapper = new JsonMapper();
|
||||
|
||||
private EthHashSolverInputs currentInput;
|
||||
private Function<EthHashSolution, Boolean> submitCallback;
|
||||
|
||||
@Override
|
||||
public boolean canHandle(final String initialMessage, final StratumConnection conn) {
|
||||
JsonRpcRequest req;
|
||||
try {
|
||||
req = new JsonObject(initialMessage).mapTo(JsonRpcRequest.class);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
if (!"eth_submitLogin".equals(req.getMethod())) {
|
||||
LOG.debug("Invalid first message method: {}", req.getMethod());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
String response = mapper.writeValueAsString(new JsonRpcSuccessResponse(req.getId(), true));
|
||||
conn.send(response + "\n");
|
||||
} catch (JsonProcessingException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
conn.close(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sendNewWork(final StratumConnection conn, final Object id) {
|
||||
byte[] dagSeed = DirectAcyclicGraphSeed.dagSeed(currentInput.getBlockNumber());
|
||||
final String[] result = {
|
||||
"0x" + BaseEncoding.base16().lowerCase().encode(currentInput.getPrePowHash()),
|
||||
"0x" + BaseEncoding.base16().lowerCase().encode(dagSeed),
|
||||
currentInput.getTarget().toHexString()
|
||||
};
|
||||
JsonRpcSuccessResponse req = new JsonRpcSuccessResponse(id, result);
|
||||
try {
|
||||
conn.send(mapper.writeValueAsString(req) + "\n");
|
||||
} catch (JsonProcessingException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(final StratumConnection conn) {}
|
||||
|
||||
@Override
|
||||
public void handle(final StratumConnection conn, final String message) {
|
||||
try {
|
||||
JsonRpcRequest req = new JsonObject(message).mapTo(JsonRpcRequest.class);
|
||||
if ("eth_getWork".equals(req.getMethod())) {
|
||||
sendNewWork(conn, req.getId());
|
||||
} else if ("eth_submitWork".equals(req.getMethod())) {
|
||||
handleMiningSubmit(conn, req);
|
||||
}
|
||||
} catch (IllegalArgumentException | IOException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
conn.close(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMiningSubmit(final StratumConnection conn, final JsonRpcRequest req)
|
||||
throws IOException {
|
||||
LOG.debug("Miner submitted solution {}", req);
|
||||
boolean result = false;
|
||||
JsonRpcParameter parameters = new JsonRpcParameter();
|
||||
final EthHashSolution solution =
|
||||
new EthHashSolution(
|
||||
BytesValue.fromHexString(parameters.required(req.getParams(), 0, String.class))
|
||||
.getLong(0),
|
||||
parameters.required(req.getParams(), 2, Hash.class),
|
||||
BytesValue.fromHexString(parameters.required(req.getParams(), 1, String.class))
|
||||
.getArrayUnsafe());
|
||||
if (Arrays.equals(currentInput.getPrePowHash(), solution.getPowHash())) {
|
||||
result = submitCallback.apply(solution);
|
||||
}
|
||||
|
||||
String response = mapper.writeValueAsString(new JsonRpcSuccessResponse(req.getId(), result));
|
||||
conn.send(response + "\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentWorkTask(final EthHashSolverInputs input) {
|
||||
this.currentInput = input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubmitCallback(final Function<EthHashSolution, Boolean> submitSolutionCallback) {
|
||||
this.submitCallback = submitSolutionCallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.stratum;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
|
||||
import org.hyperledger.besu.ethereum.core.Hash;
|
||||
import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
import org.hyperledger.besu.util.bytes.BytesValues;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Implementation of the stratum+tcp protocol.
|
||||
*
|
||||
* <p>This protocol allows miners to submit EthHash solutions over a persistent TCP connection.
|
||||
*/
|
||||
public class Stratum1Protocol implements StratumProtocol {
|
||||
private static final Logger LOG = getLogger();
|
||||
private static final JsonMapper mapper = new JsonMapper();
|
||||
private static final String STRATUM_1 = "EthereumStratum/1.0.0";
|
||||
|
||||
private static String createSubscriptionID() {
|
||||
byte[] subscriptionBytes = new byte[16];
|
||||
new Random().nextBytes(subscriptionBytes);
|
||||
return BytesValue.wrap(subscriptionBytes).toUnprefixedString();
|
||||
}
|
||||
|
||||
private final String extranonce;
|
||||
private EthHashSolverInputs currentInput;
|
||||
private Function<EthHashSolution, Boolean> submitCallback;
|
||||
private final Supplier<String> jobIdSupplier;
|
||||
private final Supplier<String> subscriptionIdCreator;
|
||||
private final List<StratumConnection> activeConnections = new ArrayList<>();
|
||||
|
||||
public Stratum1Protocol(final String extranonce) {
|
||||
this(
|
||||
extranonce,
|
||||
() -> {
|
||||
BytesValue timeValue = BytesValues.toMinimalBytes(Instant.now().toEpochMilli());
|
||||
return timeValue.slice(timeValue.size() - 4, 4).toUnprefixedString();
|
||||
},
|
||||
Stratum1Protocol::createSubscriptionID);
|
||||
}
|
||||
|
||||
Stratum1Protocol(
|
||||
final String extranonce,
|
||||
final Supplier<String> jobIdSupplier,
|
||||
final Supplier<String> subscriptionIdCreator) {
|
||||
this.extranonce = extranonce;
|
||||
this.jobIdSupplier = jobIdSupplier;
|
||||
this.subscriptionIdCreator = subscriptionIdCreator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canHandle(final String initialMessage, final StratumConnection conn) {
|
||||
JsonRpcRequest req;
|
||||
try {
|
||||
req = new JsonObject(initialMessage).mapTo(JsonRpcRequest.class);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
if (!"mining.subscribe".equals(req.getMethod())) {
|
||||
LOG.debug("Invalid first message method: {}", req.getMethod());
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
String notify =
|
||||
mapper.writeValueAsString(
|
||||
new JsonRpcSuccessResponse(
|
||||
req.getId(),
|
||||
new Object[] {
|
||||
new String[] {
|
||||
"mining.notify",
|
||||
subscriptionIdCreator.get(), // subscription ID, never reused.
|
||||
STRATUM_1
|
||||
},
|
||||
extranonce
|
||||
}));
|
||||
conn.send(notify + "\n");
|
||||
} catch (JsonProcessingException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
conn.close(null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void registerConnection(final StratumConnection conn) {
|
||||
activeConnections.add(conn);
|
||||
if (currentInput != null) {
|
||||
sendNewWork(conn);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendNewWork(final StratumConnection conn) {
|
||||
byte[] dagSeed = DirectAcyclicGraphSeed.dagSeed(currentInput.getBlockNumber());
|
||||
Object[] params =
|
||||
new Object[] {
|
||||
jobIdSupplier.get(),
|
||||
BytesValue.wrap(currentInput.getPrePowHash()).getHexString(),
|
||||
BytesValue.wrap(dagSeed).getHexString(),
|
||||
currentInput.getTarget().toHexString(),
|
||||
true
|
||||
};
|
||||
JsonRpcRequest req = new JsonRpcRequest("2.0", "mining.notify", params);
|
||||
try {
|
||||
conn.send(mapper.writeValueAsString(req) + "\n");
|
||||
} catch (JsonProcessingException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(final StratumConnection conn) {
|
||||
activeConnections.remove(conn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StratumConnection conn, final String message) {
|
||||
try {
|
||||
JsonRpcRequest req = new JsonObject(message).mapTo(JsonRpcRequest.class);
|
||||
if ("mining.authorize".equals(req.getMethod())) {
|
||||
handleMiningAuthorize(conn, req);
|
||||
} else if ("mining.submit".equals(req.getMethod())) {
|
||||
handleMiningSubmit(conn, req);
|
||||
}
|
||||
} catch (IllegalArgumentException | IOException e) {
|
||||
LOG.debug(e.getMessage(), e);
|
||||
conn.close(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMiningSubmit(final StratumConnection conn, final JsonRpcRequest message)
|
||||
throws IOException {
|
||||
LOG.debug("Miner submitted solution {}", message);
|
||||
boolean result = false;
|
||||
JsonRpcParameter parameters = new JsonRpcParameter();
|
||||
final EthHashSolution solution =
|
||||
new EthHashSolution(
|
||||
BytesValue.fromHexString(parameters.required(message.getParams(), 2, String.class))
|
||||
.getLong(0),
|
||||
Hash.fromHexString(parameters.required(message.getParams(), 4, String.class)),
|
||||
BytesValue.fromHexString(parameters.required(message.getParams(), 3, String.class))
|
||||
.getArrayUnsafe());
|
||||
if (Arrays.equals(currentInput.getPrePowHash(), solution.getPowHash())) {
|
||||
result = submitCallback.apply(solution);
|
||||
}
|
||||
|
||||
String response =
|
||||
mapper.writeValueAsString(new JsonRpcSuccessResponse(message.getId(), result));
|
||||
conn.send(response + "\n");
|
||||
}
|
||||
|
||||
private void handleMiningAuthorize(final StratumConnection conn, final JsonRpcRequest message)
|
||||
throws IOException {
|
||||
// discard message contents as we don't care for username/password.
|
||||
// send confirmation
|
||||
String confirm = mapper.writeValueAsString(new JsonRpcSuccessResponse(message.getId(), true));
|
||||
conn.send(confirm + "\n");
|
||||
// ready for work.
|
||||
registerConnection(conn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentWorkTask(final EthHashSolverInputs input) {
|
||||
this.currentInput = input;
|
||||
LOG.debug("Sending new work to miners: {}", input);
|
||||
for (StratumConnection conn : activeConnections) {
|
||||
sendNewWork(conn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubmitCallback(final Function<EthHashSolution, Boolean> submitSolutionCallback) {
|
||||
this.submitCallback = submitSolutionCallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.stratum;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Persistent TCP connection using a variant of the Stratum protocol, connecting the client to
|
||||
* miners.
|
||||
*/
|
||||
final class StratumConnection {
|
||||
private static final Logger LOG = getLogger();
|
||||
|
||||
private String incompleteMessage = "";
|
||||
|
||||
private final StratumProtocol[] protocols;
|
||||
private final Runnable closeHandle;
|
||||
private final Consumer<String> sender;
|
||||
|
||||
private StratumProtocol protocol;
|
||||
|
||||
StratumConnection(
|
||||
final StratumProtocol[] protocols,
|
||||
final Runnable closeHandle,
|
||||
final Consumer<String> sender) {
|
||||
this.protocols = protocols;
|
||||
this.closeHandle = closeHandle;
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
void handleBuffer(final Buffer buffer) {
|
||||
LOG.trace("Buffer received {}", buffer);
|
||||
Splitter splitter = Splitter.on('\n');
|
||||
boolean firstMessage = false;
|
||||
String messagesString;
|
||||
try {
|
||||
messagesString = buffer.toString(StandardCharsets.UTF_8);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.debug("Invalid message with non UTF-8 characters: " + e.getMessage(), e);
|
||||
closeHandle.run();
|
||||
return;
|
||||
}
|
||||
Iterator<String> messages = splitter.split(messagesString).iterator();
|
||||
while (messages.hasNext()) {
|
||||
String message = messages.next();
|
||||
if (!firstMessage) {
|
||||
message = incompleteMessage + message;
|
||||
firstMessage = true;
|
||||
}
|
||||
if (!messages.hasNext()) {
|
||||
incompleteMessage = message;
|
||||
} else {
|
||||
LOG.trace("Dispatching message {}", message);
|
||||
handleMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void close(final Void aVoid) {
|
||||
if (protocol != null) {
|
||||
protocol.onClose(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessage(final String message) {
|
||||
if (protocol == null) {
|
||||
for (StratumProtocol protocol : protocols) {
|
||||
if (protocol.canHandle(message, this)) {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
}
|
||||
if (protocol == null) {
|
||||
LOG.debug("Invalid first message: {}", message);
|
||||
closeHandle.run();
|
||||
}
|
||||
} else {
|
||||
protocol.handle(this, message);
|
||||
}
|
||||
}
|
||||
|
||||
public void send(final String message) {
|
||||
LOG.debug("Sending message {}", message);
|
||||
sender.accept(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.stratum;
|
||||
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Stratum protocol handler.
|
||||
*
|
||||
* <p>Manages the lifecycle of a TCP connection according to a particular variant of the Stratum
|
||||
* protocol.
|
||||
*/
|
||||
public interface StratumProtocol {
|
||||
|
||||
/**
|
||||
* Checks if the protocol can handle a TCP connection, based on the initial message.
|
||||
*
|
||||
* @param initialMessage the initial message sent over the TCP connection.
|
||||
* @param conn the connection itself
|
||||
* @return true if the protocol can handle this connection
|
||||
*/
|
||||
boolean canHandle(String initialMessage, StratumConnection conn);
|
||||
|
||||
/**
|
||||
* Callback when a stratum connection is closed.
|
||||
*
|
||||
* @param conn the connection that just closed
|
||||
*/
|
||||
void onClose(StratumConnection conn);
|
||||
|
||||
/**
|
||||
* Handle a message over an established Stratum connection
|
||||
*
|
||||
* @param conn the Stratum connection
|
||||
* @param message the message to handle
|
||||
*/
|
||||
void handle(StratumConnection conn, String message);
|
||||
|
||||
/**
|
||||
* Sets the current proof-of-work job.
|
||||
*
|
||||
* @param input the new proof-of-work job to send to miners
|
||||
*/
|
||||
void setCurrentWorkTask(EthHashSolverInputs input);
|
||||
|
||||
void setSubmitCallback(Function<EthHashSolution, Boolean> submitSolutionCallback);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.stratum;
|
||||
|
||||
import static org.apache.logging.log4j.LogManager.getLogger;
|
||||
|
||||
import org.hyperledger.besu.ethereum.chain.EthHashObserver;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolution;
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.NetServer;
|
||||
import io.vertx.core.net.NetServerOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* TCP server allowing miners to connect to the client over persistent TCP connections, using the
|
||||
* various Stratum protocols.
|
||||
*/
|
||||
public class StratumServer implements EthHashObserver {
|
||||
|
||||
private static final Logger logger = getLogger();
|
||||
|
||||
private final Vertx vertx;
|
||||
private final int port;
|
||||
private final String networkInterface;
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
private final StratumProtocol[] protocols;
|
||||
private NetServer server;
|
||||
|
||||
public StratumServer(
|
||||
final Vertx vertx, final int port, final String networkInterface, final String extraNonce) {
|
||||
this.vertx = vertx;
|
||||
this.port = port;
|
||||
this.networkInterface = networkInterface;
|
||||
protocols =
|
||||
new StratumProtocol[] {new Stratum1Protocol(extraNonce), new Stratum1EthProxyProtocol()};
|
||||
}
|
||||
|
||||
public CompletableFuture<?> start() {
|
||||
if (started.compareAndSet(false, true)) {
|
||||
logger.info("Starting stratum server on {}:{}", networkInterface, port);
|
||||
server =
|
||||
vertx.createNetServer(
|
||||
new NetServerOptions().setPort(port).setHost(networkInterface).setTcpKeepAlive(true));
|
||||
CompletableFuture<?> result = new CompletableFuture<>();
|
||||
server.connectHandler(this::handle);
|
||||
server.listen(
|
||||
res -> {
|
||||
if (res.failed()) {
|
||||
result.completeExceptionally(
|
||||
new StratumServerException(
|
||||
String.format(
|
||||
"Failed to bind Stratum Server listener to %s:%s: %s",
|
||||
networkInterface, port, res.cause().getMessage())));
|
||||
} else {
|
||||
result.complete(null);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
private void handle(final NetSocket socket) {
|
||||
StratumConnection conn =
|
||||
new StratumConnection(
|
||||
protocols, socket::close, bytes -> socket.write(Buffer.buffer(bytes)));
|
||||
socket.handler(conn::handleBuffer);
|
||||
socket.closeHandler(conn::close);
|
||||
}
|
||||
|
||||
public CompletableFuture<?> stop() {
|
||||
if (started.compareAndSet(true, false)) {
|
||||
CompletableFuture<?> result = new CompletableFuture<>();
|
||||
server.close(
|
||||
res -> {
|
||||
if (res.failed()) {
|
||||
result.completeExceptionally(
|
||||
new StratumServerException(
|
||||
String.format(
|
||||
"Failed to bind Stratum Server listener to %s:%s: %s",
|
||||
networkInterface, port, res.cause().getMessage())));
|
||||
} else {
|
||||
result.complete(null);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
logger.debug("Stopping StratumServer that was not running");
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void newJob(final EthHashSolverInputs ethHashSolverInputs) {
|
||||
if (!started.get()) {
|
||||
logger.debug("Discarding {} as stratum server is not started", ethHashSolverInputs);
|
||||
return;
|
||||
}
|
||||
for (StratumProtocol protocol : protocols) {
|
||||
protocol.setCurrentWorkTask(ethHashSolverInputs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubmitWorkCallback(
|
||||
final Function<EthHashSolution, Boolean> submitSolutionCallback) {
|
||||
for (StratumProtocol protocol : protocols) {
|
||||
protocol.setSubmitCallback(submitSolutionCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.stratum;
|
||||
|
||||
/** Class of exception occurring while launching the Stratum server. */
|
||||
public class StratumServerException extends RuntimeException {
|
||||
|
||||
public StratumServerException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright ConsenSys AG.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.stratum;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.hyperledger.besu.ethereum.mainnet.EthHashSolverInputs;
|
||||
import org.hyperledger.besu.util.bytes.BytesValue;
|
||||
import org.hyperledger.besu.util.uint.UInt256;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import org.junit.Test;
|
||||
|
||||
public class StratumConnectionTest {
|
||||
|
||||
@Test
|
||||
public void testNoSuitableProtocol() {
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
StratumConnection conn =
|
||||
new StratumConnection(new StratumProtocol[] {}, () -> called.set(true), bytes -> {});
|
||||
conn.handleBuffer(Buffer.buffer("{}\n"));
|
||||
assertThat(called.get()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStratum1WithoutMatches() {
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
StratumConnection conn =
|
||||
new StratumConnection(
|
||||
new StratumProtocol[] {new Stratum1Protocol("")}, () -> called.set(true), bytes -> {});
|
||||
conn.handleBuffer(Buffer.buffer("{}\n"));
|
||||
assertThat(called.get()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStratum1Matches() {
|
||||
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
|
||||
AtomicReference<String> message = new AtomicReference<>();
|
||||
|
||||
StratumConnection conn =
|
||||
new StratumConnection(
|
||||
new StratumProtocol[] {new Stratum1Protocol("", () -> "abcd", () -> "abcd")},
|
||||
() -> called.set(true),
|
||||
message::set);
|
||||
conn.handleBuffer(
|
||||
Buffer.buffer(
|
||||
"{"
|
||||
+ " \"id\": 23,"
|
||||
+ " \"method\": \"mining.subscribe\", "
|
||||
+ " \"params\": [ "
|
||||
+ " \"MinerName/1.0.0\", \"EthereumStratum/1.0.0\" "
|
||||
+ " ]"
|
||||
+ "}\n"));
|
||||
assertThat(called.get()).isFalse();
|
||||
|
||||
assertThat(message.get())
|
||||
.isEqualTo(
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":23,\"result\":[[\"mining.notify\",\"abcd\",\"EthereumStratum/1.0.0\"],\"\"]}\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStratum1SendWork() {
|
||||
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
|
||||
AtomicReference<String> message = new AtomicReference<>();
|
||||
|
||||
Stratum1Protocol protocol = new Stratum1Protocol("", () -> "abcd", () -> "abcd");
|
||||
|
||||
StratumConnection conn =
|
||||
new StratumConnection(
|
||||
new StratumProtocol[] {protocol}, () -> called.set(true), message::set);
|
||||
conn.handleBuffer(
|
||||
Buffer.buffer(
|
||||
"{"
|
||||
+ " \"id\": 23,"
|
||||
+ " \"method\": \"mining.subscribe\", "
|
||||
+ " \"params\": [ "
|
||||
+ " \"MinerName/1.0.0\", \"EthereumStratum/1.0.0\" "
|
||||
+ " ]"
|
||||
+ "}\n"));
|
||||
conn.handleBuffer(
|
||||
Buffer.buffer(
|
||||
"{"
|
||||
+ " \"id\": null,"
|
||||
+ " \"method\": \"mining.authorize\", "
|
||||
+ " \"params\": [ "
|
||||
+ " \"someusername\", \"password\" "
|
||||
+ " ]"
|
||||
+ "}\n"));
|
||||
assertThat(called.get()).isFalse();
|
||||
// now send work without waiting.
|
||||
protocol.setCurrentWorkTask(
|
||||
new EthHashSolverInputs(
|
||||
UInt256.of(3), BytesValue.fromHexString("deadbeef").getArrayUnsafe(), 42));
|
||||
|
||||
assertThat(message.get())
|
||||
.isEqualTo(
|
||||
"{\"jsonrpc\":\"2.0\",\"method\":\"mining.notify\",\"params\":[\"abcd\",\"0xdeadbeef\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x0000000000000000000000000000000000000000000000000000000000000003\",true],\"id\":null}\n");
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ include 'ethereum:referencetests'
|
||||
include 'ethereum:retesteth'
|
||||
include 'ethereum:rlp'
|
||||
include 'ethereum:trie'
|
||||
include 'ethereum:stratum'
|
||||
include 'metrics:core'
|
||||
include 'metrics:rocksdb'
|
||||
include 'nat'
|
||||
|
||||
Reference in New Issue
Block a user