[stratum] add proxy subscription + cleanup + Netty integration (#5129)

* Allow lenient hex param in eth_submitHashRate

* Migrate Stratum tests to Junit5

* Introduce MockitoExtension in Stratum tests

* Convert start/stop Stratum method to Vert.x future

* Use Stratum only with PowCoordinator

* Call JSON-RPC implementations from Stratum proxy

* Subscribe to newWork on proxy login

* Replace HTTP processing with Netty pipelines

---------

Signed-off-by: Diego López León <dieguitoll@gmail.com>
pull/5350/head
Diego López León 2 years ago committed by GitHub
parent 534a369574
commit f68b97b45e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      besu/src/main/java/org/hyperledger/besu/Runner.java
  2. 10
      besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
  3. 52
      besu/src/test/java/org/hyperledger/besu/RunnerBuilderTest.java
  4. 27
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetWork.java
  5. 2
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthSubmitHashRate.java
  6. 53
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetWorkTest.java
  7. 6
      ethereum/stratum/build.gradle
  8. 125
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/GetWorkProtocol.java
  9. 133
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1EthProxyProtocol.java
  10. 89
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/Stratum1Protocol.java
  11. 101
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumConnection.java
  12. 33
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumProtocol.java
  13. 231
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServer.java
  14. 23
      ethereum/stratum/src/main/java/org/hyperledger/besu/ethereum/stratum/StratumServerException.java
  15. 109
      ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/GetWorkProtocolTest.java
  16. 35
      ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/Stratum1EthProxyProtocolTest.java
  17. 124
      ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/StratumConnectionTest.java
  18. 357
      ethereum/stratum/src/test/java/org/hyperledger/besu/ethereum/stratum/StratumServerTest.java
  19. 5
      ethereum/stratum/src/test/resources/rpc-request.json

@ -149,7 +149,10 @@ public class Runner implements AutoCloseable {
service ->
waitForServiceToStart(
"ipcJsonRpc", service.start().toCompletionStage().toCompletableFuture()));
stratumServer.ifPresent(server -> waitForServiceToStart("stratum", server.start()));
stratumServer.ifPresent(
server ->
waitForServiceToStart(
"stratum", server.start().toCompletionStage().toCompletableFuture()));
autoTransactionLogBloomCachingService.ifPresent(AutoTransactionLogBloomCachingService::start);
ethStatsService.ifPresent(EthStatsService::start);
}

@ -24,7 +24,6 @@ import static org.hyperledger.besu.ethereum.core.PrivacyParameters.FLEXIBLE_PRIV
import org.hyperledger.besu.cli.config.EthNetworkConfig;
import org.hyperledger.besu.cli.config.NetworkName;
import org.hyperledger.besu.cli.options.stable.EthstatsOptions;
import org.hyperledger.besu.consensus.merge.blockcreation.TransitionCoordinator;
import org.hyperledger.besu.controller.BesuController;
import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.ethereum.ProtocolContext;
@ -67,6 +66,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.syncing.
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.api.query.PrivacyQueries;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.MiningParameters;
@ -742,10 +742,10 @@ public class RunnerBuilder {
Optional<StratumServer> stratumServer = Optional.empty();
if (miningParameters.isStratumMiningEnabled()) {
var powMiningCoordinator = miningCoordinator;
if (miningCoordinator instanceof TransitionCoordinator) {
LOG.debug("fetching powMiningCoordinator from TransitionCoordinator");
powMiningCoordinator = ((TransitionCoordinator) miningCoordinator).getPreMergeObject();
if (!(miningCoordinator instanceof PoWMiningCoordinator powMiningCoordinator)) {
throw new IllegalArgumentException(
"Stratum server requires an PoWMiningCoordinator not "
+ ((miningCoordinator == null) ? "null" : miningCoordinator.getClass().getName()));
}
stratumServer =
Optional.of(

@ -19,9 +19,6 @@ import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider
import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.BLOCKCHAIN;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.cli.config.EthNetworkConfig;
@ -32,7 +29,6 @@ import org.hyperledger.besu.consensus.common.bft.network.PeerConnectionTracker;
import org.hyperledger.besu.consensus.common.bft.protocol.BftProtocolManager;
import org.hyperledger.besu.consensus.ibft.protocol.IbftSubProtocol;
import org.hyperledger.besu.consensus.merge.blockcreation.MergeMiningCoordinator;
import org.hyperledger.besu.consensus.merge.blockcreation.TransitionCoordinator;
import org.hyperledger.besu.controller.BesuController;
import org.hyperledger.besu.crypto.SECP256K1;
import org.hyperledger.besu.cryptoservices.KeyPairSecurityModule;
@ -44,7 +40,6 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.chain.DefaultBlockchain;
import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
import org.hyperledger.besu.ethereum.core.Block;
@ -398,51 +393,4 @@ public final class RunnerBuilderTest {
assertThat(runner.getJsonRpcPort()).isPresent();
assertThat(runner.getEngineJsonRpcPort()).isEmpty();
}
@Test
public void assertTransitionStratumConfiguration() {
final JsonRpcConfiguration jrpc = JsonRpcConfiguration.createDefault();
jrpc.setEnabled(true);
final JsonRpcConfiguration engine = JsonRpcConfiguration.createEngineDefault();
engine.setEnabled(true);
final EthNetworkConfig mockMainnet = mock(EthNetworkConfig.class);
when(mockMainnet.getNetworkId()).thenReturn(BigInteger.ONE);
MergeConfigOptions.setMergeEnabled(true);
final MiningParameters mockMiningParams = mock(MiningParameters.class);
when(besuController.getMiningParameters()).thenReturn(mockMiningParams);
when(mockMiningParams.isStratumMiningEnabled()).thenReturn(true);
final TransitionCoordinator mockTransitionCoordinator =
spy(
new TransitionCoordinator(
mock(PoWMiningCoordinator.class), mock(MergeMiningCoordinator.class)));
when(besuController.getMiningCoordinator()).thenReturn(mockTransitionCoordinator);
new RunnerBuilder()
.discovery(true)
.p2pListenInterface("0.0.0.0")
.p2pListenPort(30303)
.p2pAdvertisedHost("127.0.0.1")
.p2pEnabled(true)
.natMethod(NatMethod.NONE)
.besuController(besuController)
.ethNetworkConfig(mockMainnet)
.metricsSystem(mock(ObservableMetricsSystem.class))
.permissioningService(mock(PermissioningServiceImpl.class))
.jsonRpcConfiguration(jrpc)
.engineJsonRpcConfiguration(engine)
.graphQLConfiguration(mock(GraphQLConfiguration.class))
.webSocketConfiguration(mock(WebSocketConfiguration.class))
.jsonRpcIpcConfiguration(mock(JsonRpcIpcConfiguration.class))
.metricsConfiguration(mock(MetricsConfiguration.class))
.vertx(Vertx.vertx())
.dataDir(dataDir.getRoot().toPath())
.storageProvider(mock(KeyValueStorageProvider.class))
.rpcEndpointService(new RpcEndpointServiceImpl())
.besuPluginContext(mock(BesuPluginContextImpl.class))
.networkingConfiguration(NetworkingConfiguration.create())
.build();
verify(mockTransitionCoordinator, times(1)).getPreMergeObject();
verify(mockTransitionCoordinator, times(1)).addEthHashObserver(any());
}
}

@ -27,6 +27,8 @@ import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import com.google.common.io.BaseEncoding;
@ -56,21 +58,24 @@ public class EthGetWork implements JsonRpcMethod {
@Override
public JsonRpcResponse response(final JsonRpcRequestContext requestContext) {
final Optional<PoWSolverInputs> solver = miner.getWorkDefinition();
final Object requestId = requestContext.getRequest().getId();
if (solver.isPresent()) {
final PoWSolverInputs rawResult = solver.get();
final byte[] dagSeed =
DirectAcyclicGraphSeed.dagSeed(rawResult.getBlockNumber(), epochCalculator);
final String[] result = {
rawResult.getPrePowHash().toHexString(),
"0x" + BaseEncoding.base16().lowerCase().encode(dagSeed),
rawResult.getTarget().toHexString(),
Quantity.create(rawResult.getBlockNumber())
};
return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), result);
final List<String> response = new ArrayList<>(rawResponse(rawResult));
response.add(Quantity.create(rawResult.getBlockNumber()));
return new JsonRpcSuccessResponse(requestId, response);
} else {
LOG.trace("Mining is not operational, eth_getWork request cannot be processed");
return new JsonRpcErrorResponse(
requestContext.getRequest().getId(), JsonRpcError.NO_MINING_WORK_FOUND);
return new JsonRpcErrorResponse(requestId, JsonRpcError.NO_MINING_WORK_FOUND);
}
}
public List<String> rawResponse(final PoWSolverInputs rawResult) {
final byte[] dagSeed =
DirectAcyclicGraphSeed.dagSeed(rawResult.getBlockNumber(), epochCalculator);
return List.of(
rawResult.getPrePowHash().toHexString(),
"0x" + BaseEncoding.base16().lowerCase().encode(dagSeed),
rawResult.getTarget().toHexString());
}
}

@ -42,6 +42,6 @@ public class EthSubmitHashRate implements JsonRpcMethod {
return new JsonRpcSuccessResponse(
requestContext.getRequest().getId(),
miningCoordinator.submitHashRate(
id, Bytes.fromHexString(hashRate).toBigInteger().longValue()));
id, Bytes.fromHexStringLenient(hashRate).toBigInteger().longValue()));
}
}

@ -28,6 +28,7 @@ import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.util.List;
import java.util.Optional;
import com.google.common.io.BaseEncoding;
@ -66,12 +67,12 @@ public class EthGetWorkTest {
final JsonRpcRequestContext request = requestWithParams();
final PoWSolverInputs values =
new PoWSolverInputs(UInt256.fromHexString(hexValue), Bytes.fromHexString(hexValue), 0);
final String[] expectedValue = {
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x0"
};
final List<String> expectedValue =
List.of(
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x0");
final JsonRpcResponse expectedResponse =
new JsonRpcSuccessResponse(request.getRequest().getId(), expectedValue);
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(values));
@ -86,17 +87,17 @@ public class EthGetWorkTest {
final PoWSolverInputs values =
new PoWSolverInputs(UInt256.fromHexString(hexValue), Bytes.fromHexString(hexValue), 30000);
final String[] expectedValue = {
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x"
+ BaseEncoding.base16()
.lowerCase()
.encode(
DirectAcyclicGraphSeed.dagSeed(
30000, new EpochCalculator.DefaultEpochCalculator())),
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x7530"
};
final List<String> expectedValue =
List.of(
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x"
+ BaseEncoding.base16()
.lowerCase()
.encode(
DirectAcyclicGraphSeed.dagSeed(
30000, new EpochCalculator.DefaultEpochCalculator())),
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x7530");
final JsonRpcResponse expectedResponse =
new JsonRpcSuccessResponse(request.getRequest().getId(), expectedValue);
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(values));
@ -114,15 +115,15 @@ public class EthGetWorkTest {
final PoWSolverInputs values =
new PoWSolverInputs(UInt256.fromHexString(hexValue), Bytes.fromHexString(hexValue), 60000);
final String[] expectedValue = {
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x"
+ BaseEncoding.base16()
.lowerCase()
.encode(DirectAcyclicGraphSeed.dagSeed(60000, epochCalculator)),
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0xea60"
};
final List<String> expectedValue =
List.of(
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0x"
+ BaseEncoding.base16()
.lowerCase()
.encode(DirectAcyclicGraphSeed.dagSeed(60000, epochCalculator)),
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"0xea60");
final JsonRpcResponse expectedResponse =
new JsonRpcSuccessResponse(request.getRequest().getId(), expectedValue);
when(miningCoordinator.getWorkDefinition()).thenReturn(Optional.of(values));

@ -48,10 +48,10 @@ dependencies {
testImplementation project(':testutil')
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
testImplementation 'junit:junit'
testImplementation 'io.vertx:vertx-junit5'
testImplementation 'io.vertx:vertx-web-client'
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.mockito:mockito-core'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
testImplementation 'org.mockito:mockito-junit-jupiter'
}

@ -17,18 +17,20 @@ package org.hyperledger.besu.ethereum.stratum;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolution;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.util.function.Consumer;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,50 +39,43 @@ import org.slf4j.LoggerFactory;
public class GetWorkProtocol implements StratumProtocol {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkProtocol.class);
private static final ObjectMapper mapper = new ObjectMapper();
private static final String CRLF = "\r\n";
private final EpochCalculator epochCalculator;
private volatile PoWSolverInputs currentInput;
private Function<PoWSolution, Boolean> submitCallback;
private String[] getWorkResult;
public GetWorkProtocol(final MiningCoordinator miningCoordinator) {
if (miningCoordinator instanceof PoWMiningCoordinator) {
this.epochCalculator = ((PoWMiningCoordinator) miningCoordinator).getEpochCalculator();
} else {
this.epochCalculator = new EpochCalculator.DefaultEpochCalculator();
}
public GetWorkProtocol(final EpochCalculator epochCalculator) {
this.epochCalculator = epochCalculator;
}
private JsonNode readMessage(final String message) {
int bodyIndex = message.indexOf(CRLF + CRLF);
if (bodyIndex == -1) {
return null;
}
if (!message.startsWith("POST / HTTP")) {
return null;
}
String body = message.substring(bodyIndex);
@Override
public boolean maybeHandle(
final Buffer initialMessage, final StratumConnection conn, final Consumer<String> sender) {
JsonObject message;
try {
return mapper.readTree(body);
} catch (JsonProcessingException e) {
return null;
message = initialMessage.toJsonObject();
} catch (DecodeException e) {
return false;
}
}
@Override
public boolean maybeHandle(final String initialMessage, final StratumConnection conn) {
JsonNode message = readMessage(initialMessage);
if (message == null) {
return false;
}
JsonNode methodNode = message.get("method");
if (methodNode != null) {
String method = methodNode.textValue();
String method = message.getString("method");
if (method != null) {
if ("eth_getWork".equals(method) || "eth_submitWork".equals(method)) {
JsonNode idNode = message.get("id");
boolean canHandle = idNode != null && idNode.isInt();
handle(conn, initialMessage);
boolean canHandle;
try {
Integer idNode = message.getInteger("id");
canHandle = idNode != null;
} catch (ClassCastException e) {
canHandle = false;
}
try {
handle(conn, initialMessage, sender);
} catch (Exception e) {
LOG.warn("Error handling message", e);
}
return canHandle;
}
}
@ -91,66 +86,58 @@ public class GetWorkProtocol implements StratumProtocol {
public void onClose(final StratumConnection conn) {}
@Override
public void handle(final StratumConnection conn, final String message) {
JsonNode jsonrpcMessage = readMessage(message);
public void handle(
final StratumConnection conn, final Buffer message, final Consumer<String> sender) {
JsonObject jsonrpcMessage = message.toJsonObject();
if (jsonrpcMessage == null) {
LOG.warn("Invalid message {}", message);
conn.close();
return;
}
JsonNode methodNode = jsonrpcMessage.get("method");
JsonNode idNode = jsonrpcMessage.get("id");
if (methodNode == null || idNode == null) {
LOG.warn("Invalid message {}", message);
conn.close();
return;
String method = jsonrpcMessage.getString("method");
Integer id;
try {
id = jsonrpcMessage.getInteger("id");
} catch (ClassCastException e) {
throw new IllegalArgumentException(e);
}
if (method == null || id == null) {
throw new IllegalArgumentException("Invalid JSON-RPC message");
}
String method = methodNode.textValue();
if ("eth_getWork".equals(method)) {
JsonRpcSuccessResponse response =
new JsonRpcSuccessResponse(idNode.intValue(), getWorkResult);
JsonRpcSuccessResponse response = new JsonRpcSuccessResponse(id, getWorkResult);
try {
String responseMessage = mapper.writeValueAsString(response);
conn.send(
"HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nKeep-Alive: timeout=5, max=1000\r\nContent-Length: "
+ responseMessage.length()
+ CRLF
+ CRLF
+ responseMessage);
sender.accept(responseMessage);
} catch (JsonProcessingException e) {
LOG.warn("Error sending work", e);
conn.close();
}
} else if ("eth_submitWork".equals(method)) {
JsonNode paramsNode = jsonrpcMessage.get("params");
JsonArray paramsNode;
try {
paramsNode = jsonrpcMessage.getJsonArray("params");
} catch (ClassCastException e) {
throw new IllegalArgumentException("Invalid eth_submitWork params");
}
if (paramsNode == null || paramsNode.size() != 3) {
LOG.warn("Invalid eth_submitWork params {}", message);
conn.close();
return;
throw new IllegalArgumentException("Invalid eth_submitWork params");
}
final PoWSolution solution =
new PoWSolution(
Bytes.fromHexString(paramsNode.get(0).textValue()).getLong(0),
Hash.fromHexString(paramsNode.get(2).textValue()),
Bytes.fromHexString(paramsNode.getString(0)).getLong(0),
Hash.fromHexString(paramsNode.getString(2)),
null,
Bytes.fromHexString(paramsNode.get(1).textValue()));
Bytes.fromHexString(paramsNode.getString(1)));
final boolean result = submitCallback.apply(solution);
try {
String resultMessage =
mapper.writeValueAsString(new JsonRpcSuccessResponse(idNode.intValue(), result));
conn.send(
"HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nKeep-Alive: timeout=5, max=1000\r\nContent-Length: "
+ resultMessage.length()
+ CRLF
+ CRLF
+ resultMessage);
String resultMessage = mapper.writeValueAsString(new JsonRpcSuccessResponse(id, result));
sender.accept(resultMessage);
} catch (JsonProcessingException e) {
LOG.warn("Error accepting solution work", e);
conn.close();
throw new IllegalStateException("Error accepting solution work", e);
}
} else {
LOG.warn("Unknown method {}", method);
conn.close();
throw new UnsupportedOperationException("Unsupported method " + method);
}
}

@ -14,27 +14,27 @@
*/
package org.hyperledger.besu.ethereum.stratum;
import org.hyperledger.besu.consensus.merge.blockcreation.TransitionCoordinator;
import org.hyperledger.besu.datatypes.Hash;
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.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthGetWork;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSubmitHashRate;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthSubmitWork;
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.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.mainnet.DirectAcyclicGraphSeed;
import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolution;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;
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.buffer.Buffer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -46,30 +46,20 @@ import org.slf4j.LoggerFactory;
public class Stratum1EthProxyProtocol implements StratumProtocol {
private static final Logger LOG = LoggerFactory.getLogger(Stratum1EthProxyProtocol.class);
private static final JsonMapper mapper = new JsonMapper();
private final MiningCoordinator miningCoordinator;
private PoWSolverInputs currentInput;
private Function<PoWSolution, Boolean> submitCallback;
private final EpochCalculator epochCalculator;
public Stratum1EthProxyProtocol(final MiningCoordinator miningCoordinator) {
MiningCoordinator maybePowMiner = miningCoordinator;
if (maybePowMiner instanceof TransitionCoordinator) {
maybePowMiner = ((TransitionCoordinator) maybePowMiner).getPreMergeObject();
}
if (!(maybePowMiner instanceof PoWMiningCoordinator)) {
throw new IllegalArgumentException(
"Stratum1 Proxies require an PoWMiningCoordinator not "
+ ((maybePowMiner == null) ? "null" : maybePowMiner.getClass().getName()));
}
this.miningCoordinator = maybePowMiner;
this.epochCalculator = ((PoWMiningCoordinator) maybePowMiner).getEpochCalculator();
private final EthGetWork ethGetWork;
private final EthSubmitWork ethSubmitWork;
private final EthSubmitHashRate ethSubmitHashRate;
private final Collection<StratumConnection> activeConnections = new ArrayList<>();
public Stratum1EthProxyProtocol(final PoWMiningCoordinator miningCoordinator) {
ethGetWork = new EthGetWork(miningCoordinator);
ethSubmitWork = new EthSubmitWork(miningCoordinator);
ethSubmitHashRate = new EthSubmitHashRate(miningCoordinator);
}
@Override
public boolean maybeHandle(final String initialMessage, final StratumConnection conn) {
public boolean maybeHandle(
final Buffer initialMessage, final StratumConnection conn, final Consumer<String> sender) {
JsonRpcRequest req;
try {
req = new JsonObject(initialMessage).mapTo(JsonRpcRequest.class);
@ -84,7 +74,8 @@ public class Stratum1EthProxyProtocol implements StratumProtocol {
try {
String response = mapper.writeValueAsString(new JsonRpcSuccessResponse(req.getId(), true));
conn.send(response + "\n");
sender.accept(response);
activeConnections.add(conn);
} catch (JsonProcessingException e) {
LOG.debug(e.getMessage(), e);
conn.close();
@ -93,66 +84,48 @@ public class Stratum1EthProxyProtocol implements StratumProtocol {
return true;
}
private void sendNewWork(final StratumConnection conn, final Object id) {
byte[] dagSeed = DirectAcyclicGraphSeed.dagSeed(currentInput.getBlockNumber(), epochCalculator);
final String[] result = {
currentInput.getPrePowHash().toHexString(),
"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) {}
public void onClose(final StratumConnection conn) {
activeConnections.remove(conn);
}
@Override
public void handle(final StratumConnection conn, final String message) {
try {
final JsonRpcRequest req = new JsonObject(message).mapTo(JsonRpcRequest.class);
if (RpcMethod.ETH_GET_WORK.getMethodName().equals(req.getMethod())) {
sendNewWork(conn, req.getId());
} else if (RpcMethod.ETH_SUBMIT_WORK.getMethodName().equals(req.getMethod())) {
handleMiningSubmit(conn, req);
} else if (RpcMethod.ETH_SUBMIT_HASHRATE.getMethodName().equals(req.getMethod())) {
handleHashrateSubmit(mapper, miningCoordinator, conn, req);
public void handle(
final StratumConnection conn, final Buffer message, final Consumer<String> sender) {
final JsonRpcRequest req = new JsonObject(message).mapTo(JsonRpcRequest.class);
final JsonRpcRequestContext reqContext = new JsonRpcRequestContext(req);
final JsonRpcResponse rpcResponse;
switch (req.getMethod()) {
case "eth_getWork" -> rpcResponse = ethGetWork.response(reqContext);
case "eth_submitWork" -> rpcResponse = ethSubmitWork.response(reqContext);
case "eth_submitHashrate" -> rpcResponse = ethSubmitHashRate.response(reqContext);
default -> {
LOG.debug("Invalid method: {}", req.getMethod());
throw new UnsupportedOperationException("Invalid method: " + req.getMethod());
}
} catch (IllegalArgumentException | IOException e) {
LOG.debug(e.getMessage(), e);
conn.close();
}
}
private void handleMiningSubmit(final StratumConnection conn, final JsonRpcRequest req)
throws IOException {
LOG.debug("Miner submitted solution {}", req);
boolean result = false;
final PoWSolution solution =
new PoWSolution(
Bytes.fromHexString(req.getRequiredParameter(0, String.class)).getLong(0),
req.getRequiredParameter(2, Hash.class),
null,
Bytes.fromHexString(req.getRequiredParameter(1, String.class)));
if (currentInput.getPrePowHash().equals(solution.getPowHash())) {
result = submitCallback.apply(solution);
try {
sender.accept(mapper.writeValueAsString(rpcResponse));
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
String response = mapper.writeValueAsString(new JsonRpcSuccessResponse(req.getId(), result));
conn.send(response + "\n");
}
@Override
public void setCurrentWorkTask(final PoWSolverInputs input) {
this.currentInput = input;
activeConnections.forEach(
conn -> {
try {
conn.notificationSender()
.accept(
mapper.writeValueAsString(
new JsonRpcSuccessResponse(0, ethGetWork.rawResponse(input))));
} catch (JsonProcessingException e) {
LOG.error("Failed to announce new work", e);
}
});
}
@Override
public void setSubmitCallback(final Function<PoWSolution, Boolean> submitSolutionCallback) {
this.submitCallback = submitSolutionCallback;
}
public void setSubmitCallback(final Function<PoWSolution, Boolean> submitSolutionCallback) {}
}

@ -14,7 +14,6 @@
*/
package org.hyperledger.besu.ethereum.stratum;
import org.hyperledger.besu.consensus.merge.blockcreation.TransitionCoordinator;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
@ -26,16 +25,17 @@ import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolution;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;
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.buffer.Buffer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import org.apache.tuweni.bytes.Bytes;
@ -67,7 +67,7 @@ public class Stratum1Protocol implements StratumProtocol {
private final List<StratumConnection> activeConnections = new ArrayList<>();
private final EpochCalculator epochCalculator;
public Stratum1Protocol(final String extranonce, final MiningCoordinator miningCoordinator) {
public Stratum1Protocol(final String extranonce, final PoWMiningCoordinator miningCoordinator) {
this(
extranonce,
miningCoordinator,
@ -80,28 +80,19 @@ public class Stratum1Protocol implements StratumProtocol {
Stratum1Protocol(
final String extranonce,
final MiningCoordinator miningCoordinator,
final PoWMiningCoordinator miningCoordinator,
final Supplier<String> jobIdSupplier,
final Supplier<String> subscriptionIdCreator) {
MiningCoordinator maybePowMiner = miningCoordinator;
if (maybePowMiner instanceof TransitionCoordinator) {
maybePowMiner = ((TransitionCoordinator) maybePowMiner).getPreMergeObject();
}
if (!(maybePowMiner instanceof PoWMiningCoordinator)) {
throw new IllegalArgumentException(
"Stratum1 requires an PoWMiningCoordinator not "
+ ((maybePowMiner == null) ? "null" : maybePowMiner.getClass().getName()));
}
this.extranonce = extranonce;
this.miningCoordinator = maybePowMiner;
this.miningCoordinator = miningCoordinator;
this.jobIdSupplier = jobIdSupplier;
this.subscriptionIdCreator = subscriptionIdCreator;
this.epochCalculator = ((PoWMiningCoordinator) maybePowMiner).getEpochCalculator();
this.epochCalculator = miningCoordinator.getEpochCalculator();
}
@Override
public boolean maybeHandle(final String initialMessage, final StratumConnection conn) {
public boolean maybeHandle(
final Buffer initialMessage, final StratumConnection conn, final Consumer<String> sender) {
final JsonRpcRequest requestBody;
try {
requestBody = new JsonObject(initialMessage).mapTo(JsonRpcRequest.class);
@ -126,7 +117,7 @@ public class Stratum1Protocol implements StratumProtocol {
},
extranonce
}));
conn.send(notify + "\n");
sender.accept(notify);
} catch (JsonProcessingException e) {
LOG.debug(e.getMessage(), e);
conn.close();
@ -134,14 +125,14 @@ public class Stratum1Protocol implements StratumProtocol {
return true;
}
private void registerConnection(final StratumConnection conn) {
private void registerConnection(final StratumConnection conn, final Consumer<String> sender) {
activeConnections.add(conn);
if (currentInput != null) {
sendNewWork(conn);
sendNewWork(sender);
}
}
private void sendNewWork(final StratumConnection conn) {
private void sendNewWork(final Consumer<String> sender) {
byte[] dagSeed = DirectAcyclicGraphSeed.dagSeed(currentInput.getBlockNumber(), epochCalculator);
Object[] params =
new Object[] {
@ -153,9 +144,9 @@ public class Stratum1Protocol implements StratumProtocol {
};
JsonRpcRequest req = new JsonRpcRequest("2.0", "mining.notify", params);
try {
conn.send(mapper.writeValueAsString(req) + "\n");
sender.accept(mapper.writeValueAsString(req));
} catch (JsonProcessingException e) {
LOG.debug(e.getMessage(), e);
throw new IllegalStateException(e);
}
}
@ -165,24 +156,19 @@ public class Stratum1Protocol implements StratumProtocol {
}
@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);
} else if (RpcMethod.ETH_SUBMIT_HASHRATE.getMethodName().equals(req.getMethod())) {
handleHashrateSubmit(mapper, miningCoordinator, conn, req);
}
} catch (IllegalArgumentException | IOException e) {
LOG.debug(e.getMessage(), e);
conn.close();
public void handle(
final StratumConnection conn, final Buffer message, final Consumer<String> sender) {
JsonRpcRequest req = new JsonObject(message).mapTo(JsonRpcRequest.class);
if ("mining.authorize".equals(req.getMethod())) {
handleMiningAuthorize(conn, req, sender);
} else if ("mining.submit".equals(req.getMethod())) {
handleMiningSubmit(req, sender);
} else if (RpcMethod.ETH_SUBMIT_HASHRATE.getMethodName().equals(req.getMethod())) {
handleHashrateSubmit(mapper, miningCoordinator, conn, req, sender);
}
}
private void handleMiningSubmit(final StratumConnection conn, final JsonRpcRequest message)
throws IOException {
private void handleMiningSubmit(final JsonRpcRequest message, final Consumer<String> sender) {
LOG.debug("Miner submitted solution {}", message);
boolean result = false;
final PoWSolution solution =
@ -195,19 +181,28 @@ public class Stratum1Protocol implements StratumProtocol {
result = submitCallback.apply(solution);
}
String response =
mapper.writeValueAsString(new JsonRpcSuccessResponse(message.getId(), result));
conn.send(response + "\n");
String response;
try {
response = mapper.writeValueAsString(new JsonRpcSuccessResponse(message.getId(), result));
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
sender.accept(response);
}
private void handleMiningAuthorize(final StratumConnection conn, final JsonRpcRequest message)
throws IOException {
private void handleMiningAuthorize(
final StratumConnection conn, final JsonRpcRequest message, final Consumer<String> sender) {
// 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");
String confirm;
try {
confirm = mapper.writeValueAsString(new JsonRpcSuccessResponse(message.getId(), true));
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
sender.accept(confirm);
// ready for work.
registerConnection(conn);
registerConnection(conn, sender);
}
@Override
@ -215,7 +210,7 @@ public class Stratum1Protocol implements StratumProtocol {
this.currentInput = input;
LOG.debug("Sending new work to miners: {}", input);
for (StratumConnection conn : activeConnections) {
sendNewWork(conn);
sendNewWork(conn.notificationSender());
}
}

@ -14,13 +14,8 @@
*/
package org.hyperledger.besu.ethereum.stratum;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Splitter;
import io.vertx.core.buffer.Buffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,76 +27,29 @@ import org.slf4j.LoggerFactory;
final class StratumConnection {
private static final Logger LOG = LoggerFactory.getLogger(StratumConnection.class);
private String incompleteMessage = "";
private boolean httpDetected = false;
private final StratumProtocol[] protocols;
private final Runnable closeHandle;
private final Consumer<String> sender;
private final Consumer<String> notificationSender;
private StratumProtocol protocol;
private static final Pattern contentLengthPattern =
Pattern.compile("\r\nContent-Length: (\\d+)\r\n");
StratumConnection(
final StratumProtocol[] protocols,
final Runnable closeHandle,
final Consumer<String> sender) {
StratumConnection(final StratumProtocol[] protocols, final Consumer<String> notificationSender) {
this.protocols = protocols;
this.closeHandle = closeHandle;
this.sender = sender;
this.notificationSender = notificationSender;
}
void handleBuffer(final Buffer buffer) {
LOG.trace("Buffer received {}", buffer);
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;
}
if (httpDetected) {
httpDetected = false;
messagesString = incompleteMessage + messagesString;
}
Matcher match = contentLengthPattern.matcher(messagesString);
if (match.find()) {
try {
int contentLength = Integer.parseInt(match.group(1));
String body = messagesString.substring(messagesString.indexOf("\r\n\r\n") + 4);
if (body.length() < contentLength) {
incompleteMessage = messagesString;
httpDetected = true;
return;
void handleBuffer(final Buffer message, final Consumer<String> sender) {
LOG.trace(">> {}", message);
if (protocol == null) {
for (StratumProtocol protocol : protocols) {
if (protocol.maybeHandle(message, this, sender)) {
LOG.trace("Using protocol: {}", protocol.getClass().getSimpleName());
this.protocol = protocol;
}
} catch (NumberFormatException e) {
close();
return;
}
LOG.trace("Dispatching HTTP message {}", messagesString);
handleMessage(messagesString);
} else {
boolean firstMessage = false;
Splitter splitter = Splitter.on('\n');
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);
}
if (protocol == null) {
throw new IllegalArgumentException("Invalid first message");
}
} else {
protocol.handle(this, message, sender);
}
}
@ -111,24 +59,7 @@ final class StratumConnection {
}
}
private void handleMessage(final String message) {
if (protocol == null) {
for (StratumProtocol protocol : protocols) {
if (protocol.maybeHandle(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);
public Consumer<String> notificationSender() {
return notificationSender;
}
}

@ -20,10 +20,12 @@ import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolution;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.io.IOException;
import java.util.function.Consumer;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.vertx.core.buffer.Buffer;
import org.apache.tuweni.bytes.Bytes;
/**
@ -40,9 +42,10 @@ public interface StratumProtocol {
*
* @param initialMessage the initial message sent over the TCP connection.
* @param conn the connection itself
* @param sender the callback to use to send messages back to the client
* @return true if the protocol can handle this connection
*/
boolean maybeHandle(String initialMessage, StratumConnection conn);
boolean maybeHandle(Buffer initialMessage, StratumConnection conn, Consumer<String> sender);
/**
* Callback when a stratum connection is closed.
@ -56,8 +59,9 @@ public interface StratumProtocol {
*
* @param conn the Stratum connection
* @param message the message to handle
* @param sender the callback to use to send messages back to the client
*/
void handle(StratumConnection conn, String message);
void handle(StratumConnection conn, Buffer message, Consumer<String> sender);
/**
* Sets the current proof-of-work job.
@ -72,16 +76,21 @@ public interface StratumProtocol {
final JsonMapper mapper,
final MiningCoordinator miningCoordinator,
final StratumConnection conn,
final JsonRpcRequest message)
throws IOException {
final JsonRpcRequest message,
final Consumer<String> sender) {
final String hashRate = message.getRequiredParameter(0, String.class);
final String id = message.getRequiredParameter(1, String.class);
String response =
mapper.writeValueAsString(
new JsonRpcSuccessResponse(
message.getId(),
miningCoordinator.submitHashRate(
id, Bytes.fromHexString(hashRate).toBigInteger().longValue())));
conn.send(response + "\n");
String response;
try {
response =
mapper.writeValueAsString(
new JsonRpcSuccessResponse(
message.getId(),
miningCoordinator.submitHashRate(
id, Bytes.fromHexString(hashRate).toBigInteger().longValue())));
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
sender.accept(response);
}
}

@ -14,7 +14,7 @@
*/
package org.hyperledger.besu.ethereum.stratum;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.chain.PoWObserver;
import org.hyperledger.besu.ethereum.mainnet.EthHash;
import org.hyperledger.besu.ethereum.mainnet.PoWSolution;
@ -24,18 +24,39 @@ import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import com.google.common.util.concurrent.AtomicDouble;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.DecoderResultProvider;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import io.vertx.core.Future;
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 io.vertx.core.net.impl.NetSocketInternal;
import org.apache.tuweni.units.bigints.UInt256;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -47,6 +68,7 @@ import org.slf4j.LoggerFactory;
public class StratumServer implements PoWObserver {
private static final Logger logger = LoggerFactory.getLogger(StratumServer.class);
private static final String VERTX_HANDLER_NAME = "handler";
private final Vertx vertx;
private final int port;
@ -61,7 +83,7 @@ public class StratumServer implements PoWObserver {
public StratumServer(
final Vertx vertx,
final MiningCoordinator miningCoordinator,
final PoWMiningCoordinator miningCoordinator,
final int port,
final String networkInterface,
final String extraNonce,
@ -71,7 +93,7 @@ public class StratumServer implements PoWObserver {
this.networkInterface = networkInterface;
protocols =
new StratumProtocol[] {
new GetWorkProtocol(miningCoordinator),
new GetWorkProtocol(miningCoordinator.getEpochCalculator()),
new Stratum1Protocol(extraNonce, miningCoordinator),
new Stratum1EthProxyProtocol(miningCoordinator)
};
@ -90,65 +112,196 @@ public class StratumServer implements PoWObserver {
BesuMetricCategory.STRATUM, "disconnections", "Number of disconnections over time");
}
public CompletableFuture<?> start() {
public Future<NetServer> 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 server
.listen()
.onSuccess(
v ->
logger.info("Stratum server started on {}:{}", networkInterface, v.actualPort()));
}
return CompletableFuture.completedFuture(null);
return Future.succeededFuture(server);
}
private void handle(final NetSocket socket) {
connectionsCount.inc();
numberOfMiners.incrementAndGet();
NetSocketInternal internalSocket = (NetSocketInternal) socket;
StratumConnection conn =
new StratumConnection(
protocols, socket::close, bytes -> socket.write(Buffer.buffer(bytes)));
socket.handler(conn::handleBuffer);
protocols, response -> sendLineBasedResponse(internalSocket, response));
ChannelPipeline pipeline = internalSocket.channelHandlerContext().pipeline();
pipeline.addBefore(
VERTX_HANDLER_NAME,
"stratumDecoder",
new HttpRequestDecoder() {
@Override
protected void decode(
final ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out)
throws Exception {
ByteBuf inputCopy = in.copy();
int indexBeforeDecode = inputCopy.readerIndex();
super.decode(ctx, inputCopy, out);
if (out.isEmpty()) { // to process last chunk
super.decode(ctx, inputCopy, out);
}
DecoderResultProvider httpDecodingResult =
(DecoderResultProvider) out.get(out.size() - 1);
ChannelPipeline pipeline = ctx.pipeline();
if (httpDecodingResult.decoderResult().isFailure()) {
logger.trace("Received non-HTTP request, switching to line-based protocol");
out.remove(httpDecodingResult);
pipeline.addBefore(
VERTX_HANDLER_NAME, "frameDecoder", new LineBasedFrameDecoder(240));
pipeline.addBefore(
VERTX_HANDLER_NAME,
"lineTrimmer",
new MessageToMessageDecoder<ByteBuf>() {
@Override
protected void decode(
final ChannelHandlerContext ctx,
final ByteBuf message,
final List<Object> out) {
if (message.readableBytes() > 0
&& message.getByte(message.readableBytes() - 1) == '\n') {
if (message.readableBytes() > 1
&& message.getByte(message.readableBytes() - 2) == '\r') {
out.add(message.readRetainedSlice(message.readableBytes() - 2));
} else {
out.add(message.readRetainedSlice(message.readableBytes() - 1));
}
} else {
out.add(message.retain());
}
}
});
pipeline.addBefore(
VERTX_HANDLER_NAME, "stringEncoder", new StringEncoder(CharsetUtil.UTF_8));
pipeline.remove(this);
} else {
String httpEncoderHandlerName = "httpEncoder";
if (pipeline.get(httpEncoderHandlerName) == null) {
logger.trace("Received HTTP request, switching to HTTP protocol");
pipeline.addBefore(
VERTX_HANDLER_NAME, httpEncoderHandlerName, new HttpResponseEncoder());
}
if (httpDecodingResult instanceof HttpRequest request
&& !request.method().equals(HttpMethod.POST)) {
logger.debug("Received non-POST request");
in.skipBytes(in.readableBytes()); // skip body decode
} else {
in.skipBytes(inputCopy.readerIndex() - indexBeforeDecode);
}
}
}
});
Buffer requestBody = Buffer.buffer();
internalSocket.messageHandler(
obj -> {
try {
if (obj instanceof HttpRequest request && !request.method().equals(HttpMethod.POST)) {
sendHttpResponse(
internalSocket, HttpResponseStatus.METHOD_NOT_ALLOWED, Unpooled.EMPTY_BUFFER);
} else if (obj instanceof HttpContent httpContent) {
requestBody.appendBuffer(Buffer.buffer(httpContent.content()));
if (obj instanceof LastHttpContent) {
try {
conn.handleBuffer(
requestBody,
response ->
sendHttpResponse(
internalSocket,
HttpResponseStatus.OK,
Unpooled.wrappedBuffer(response.getBytes(CharsetUtil.UTF_8))));
} catch (IllegalArgumentException e) {
logger.warn("Invalid message {}", requestBody);
sendHttpResponse(
internalSocket, HttpResponseStatus.BAD_REQUEST, Unpooled.EMPTY_BUFFER);
} catch (Exception e) {
logger.warn("Unexpected error", e);
sendHttpResponse(
internalSocket,
HttpResponseStatus.INTERNAL_SERVER_ERROR,
Unpooled.EMPTY_BUFFER);
} finally {
conn.close();
}
}
} else if (obj instanceof ByteBuf value && value.readableBytes() > 0) {
try {
conn.handleBuffer(
Buffer.buffer(value),
response -> sendLineBasedResponse(internalSocket, response));
} catch (Exception e) {
logger.error("Error handling request", e);
internalSocket.close().onFailure(ex -> logger.error("Error closing socket", ex));
}
}
} catch (Exception e) {
pipeline.fireExceptionCaught(e);
}
});
internalSocket.exceptionHandler(
ex -> {
logger.error("Unknown error", ex);
internalSocket
.close()
.onFailure(
closingException -> logger.error("Error closing socket", closingException));
});
socket.closeHandler(
(aVoid) -> {
logger.debug("Socket closed");
conn.close();
numberOfMiners.decrementAndGet();
disconnectionsCount.inc();
});
}
public CompletableFuture<?> stop() {
private static void sendHttpResponse(
final NetSocketInternal internalSocket,
final HttpResponseStatus responseStatus,
final ByteBuf content) {
ByteBuf contentResponse = content;
if (logger.isTraceEnabled()) {
contentResponse = content.copy();
}
internalSocket
.writeMessage(
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseStatus, contentResponse))
.onFailure(ex -> logger.error("Failed to write response", ex))
.onSuccess(
writeResult ->
internalSocket
.close()
.onSuccess(
v ->
logger.trace(
"<< {}",
content.isReadable()
? content.toString(StandardCharsets.UTF_8)
: "no content"))
.onFailure(ex -> logger.error("Failed to close socket", ex)));
}
private static Future<Void> sendLineBasedResponse(
final NetSocketInternal internalSocket, final String response) {
return internalSocket
.writeMessage(response + '\n') // response is delimited by a newline
.onSuccess(v -> logger.trace("<< {}", response))
.onFailure(ex -> logger.error("Failed to send response: {}", response, ex));
}
public Future<Void> 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;
return server.close();
}
logger.debug("Stopping StratumServer that was not running");
return CompletableFuture.completedFuture(null);
return Future.succeededFuture();
}
@Override

@ -1,23 +0,0 @@
/*
* 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);
}
}

@ -17,96 +17,64 @@ package org.hyperledger.besu.ethereum.stratum;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.blockcreation.NoopMiningCoordinator;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolution;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.util.concurrent.atomic.AtomicReference;
import io.vertx.core.buffer.Buffer;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.junit.Test;
import org.junit.jupiter.api.Test;
public class GetWorkProtocolTest {
@Test
public void testCanHandleGetWorkMessage() {
String message =
"POST / HTTP/1.1\r\nHost: localhost:8000\r\nContent-Type:application/json\r\n\r\n {\"method\":\"eth_getWork\",\"id\":1}";
MiningCoordinator coordinator = mock(PoWMiningCoordinator.class);
GetWorkProtocol protocol = new GetWorkProtocol(coordinator);
String message = "{\"method\":\"eth_getWork\",\"id\":1}";
EpochCalculator epochCalculator = mock(EpochCalculator.class);
GetWorkProtocol protocol = new GetWorkProtocol(epochCalculator);
assertThat(
protocol.maybeHandle(
message, new StratumConnection(new StratumProtocol[0], () -> {}, (msg) -> {})))
Buffer.buffer(message),
new StratumConnection(new StratumProtocol[0], null),
(msg) -> {}))
.isTrue();
}
@Test
public void testCanHandleSubmitWorkMessage() {
String message =
"POST / HTTP/1.1\r\nHost: localhost:8000\r\nContent-Type:application/json\r\n\r\n {\"method\":\"eth_submitWork\",\"id\":1}";
MiningCoordinator coordinator = mock(PoWMiningCoordinator.class);
GetWorkProtocol protocol = new GetWorkProtocol(coordinator);
String message = "{\"method\":\"eth_submitWork\",\"id\":1}";
EpochCalculator epochCalculator = mock(EpochCalculator.class);
GetWorkProtocol protocol = new GetWorkProtocol(epochCalculator);
assertThat(
protocol.maybeHandle(
message, new StratumConnection(new StratumProtocol[0], () -> {}, (msg) -> {})))
Buffer.buffer(message),
new StratumConnection(new StratumProtocol[0], null),
(msg) -> {}))
.isTrue();
}
@Test
public void testCanHandleNotPost() {
String message =
"DELETE / HTTP/1.1\r\nHost: localhost:8000\r\nContent-Type:application/json\r\n\r\n {\"method\":\"eth_getWork\",\"id\":1}";
MiningCoordinator coordinator = mock(PoWMiningCoordinator.class);
GetWorkProtocol protocol = new GetWorkProtocol(coordinator);
assertThat(
protocol.maybeHandle(
message, new StratumConnection(new StratumProtocol[0], () -> {}, (msg) -> {})))
.isFalse();
}
@Test
public void testCanHandleBadGet() {
String message = "GET / HTTP/1.1\r\nHost: localhost:8000\r\nContent-Type:text/plain\r\n\r\n";
MiningCoordinator coordinator = mock(PoWMiningCoordinator.class);
GetWorkProtocol protocol = new GetWorkProtocol(coordinator);
assertThat(
protocol.maybeHandle(
message, new StratumConnection(new StratumProtocol[0], () -> {}, (msg) -> {})))
.isFalse();
}
@Test
public void testCanHandleNotHTTP() {
String message = "{\"method\":\"eth_getWork\",\"id\":1}";
MiningCoordinator coordinator = mock(PoWMiningCoordinator.class);
GetWorkProtocol protocol = new GetWorkProtocol(coordinator);
public void testCanHandleBadRequest() {
String message = "bad-request";
EpochCalculator epochCalculator = mock(EpochCalculator.class);
GetWorkProtocol protocol = new GetWorkProtocol(epochCalculator);
assertThat(
protocol.maybeHandle(
message, new StratumConnection(new StratumProtocol[0], () -> {}, (msg) -> {})))
Buffer.buffer(message),
new StratumConnection(new StratumProtocol[0], null),
(msg) -> {}))
.isFalse();
}
@Test
public void testCanGetSolutions() {
MiningCoordinator coordinator =
new NoopMiningCoordinator(
new MiningParameters.Builder()
.coinbase(Address.wrap(Bytes.random(20)))
.minTransactionGasPrice(Wei.of(1))
.extraData(Bytes.fromHexString("0xc0ffee"))
.miningEnabled(true)
.build());
AtomicReference<String> messageRef = new AtomicReference<>();
StratumConnection connection =
new StratumConnection(new StratumProtocol[0], () -> {}, messageRef::set);
AtomicReference<Object> messageRef = new AtomicReference<>();
StratumConnection connection = new StratumConnection(new StratumProtocol[0], null);
GetWorkProtocol protocol = new GetWorkProtocol(coordinator);
GetWorkProtocol protocol = new GetWorkProtocol(new EpochCalculator.DefaultEpochCalculator());
AtomicReference<PoWSolution> solutionFound = new AtomicReference<>();
protocol.setSubmitCallback(
(sol) -> {
@ -115,30 +83,21 @@ public class GetWorkProtocolTest {
});
protocol.setCurrentWorkTask(
new PoWSolverInputs(UInt256.ZERO, Bytes.fromHexString("0xdeadbeef"), 123L));
String requestWork =
"POST / HTTP/1.1\r\nHost: localhost:8000\r\nContent-Type:application/json\r\n\r\n {\"method\":\"eth_getWork\",\"id\":1}";
protocol.handle(connection, requestWork);
String requestWork = "{\"method\":\"eth_getWork\",\"id\":1}";
protocol.handle(connection, Buffer.buffer(requestWork), messageRef::set);
assertThat(messageRef.get())
.isEqualTo(
"HTTP/1.1 200 OK\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Keep-Alive: timeout=5, max=1000\r\n"
+ "Content-Length: 193\r\n"
+ "\r\n"
+ "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[\"0xdeadbeef\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x7b\"]}");
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[\"0xdeadbeef\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x7b\"]}");
String submitWork =
"POST / HTTP/1.1\r\nHost: localhost:8000\r\nContent-Type:application/json\r\n\r\n {\"method\":\"eth_submitWork\",\"id\":1,\"params\":[\"0xdeadbeefdeadbeef\", \"0x0000000000000000000000000000000000000000000000000000000000000000\", \"0x0000000000000000000000000000000000000000000000000000000000000000\"]}";
"{\"method\":\"eth_submitWork\",\"id\":1,\"params\":[\"0xdeadbeefdeadbeef\", \"0x0000000000000000000000000000000000000000000000000000000000000000\", \"0x0000000000000000000000000000000000000000000000000000000000000000\"]}";
assertThat(
protocol.maybeHandle(
submitWork, new StratumConnection(new StratumProtocol[0], () -> {}, (msg) -> {})))
Buffer.buffer(submitWork),
new StratumConnection(new StratumProtocol[0], null),
messageRef::set))
.isTrue();
protocol.handle(connection, submitWork);
assertThat(messageRef.get())
.isEqualTo(
"HTTP/1.1 200 OK\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Keep-Alive: timeout=5, max=1000\r\n"
+ "Content-Length: 38\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":true}");
protocol.handle(connection, Buffer.buffer(submitWork), messageRef::set);
assertThat(messageRef.get()).isEqualTo("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":true}");
}
}

@ -15,47 +15,46 @@
package org.hyperledger.besu.ethereum.stratum;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import io.vertx.core.buffer.Buffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class Stratum1EthProxyProtocolTest {
private Stratum1EthProxyProtocol protocol;
private StratumConnection conn;
private List<String> receivedMessages;
private List<Object> receivedMessages;
@Before
@BeforeEach
public void setUp() {
MiningCoordinator coordinator = mock(PoWMiningCoordinator.class);
protocol = new Stratum1EthProxyProtocol(coordinator);
protocol = new Stratum1EthProxyProtocol(null);
receivedMessages = new ArrayList<>();
conn = new StratumConnection(new StratumProtocol[0], null, receivedMessages::add);
conn = new StratumConnection(new StratumProtocol[0], null);
}
@Test
public void testCanHandleEmptyString() {
assertThat(protocol.maybeHandle("", conn)).isFalse();
assertThat(protocol.maybeHandle(Buffer.buffer(), conn, receivedMessages::add)).isFalse();
}
@Test
public void testCanHandleMalformedJSON() {
assertThat(protocol.maybeHandle("{[\"foo\",", conn)).isFalse();
assertThat(protocol.maybeHandle(Buffer.buffer("{[\"foo\","), conn, receivedMessages::add))
.isFalse();
}
@Test
public void testCanHandleWrongMethod() {
assertThat(
protocol.maybeHandle(
"{\"id\":0,\"method\":\"eth_byebye\",\"params\":[\"0xdeadbeefdeadbeef.worker\"]}",
conn))
Buffer.buffer(
"{\"id\":0,\"method\":\"eth_byebye\",\"params\":[\"0xdeadbeefdeadbeef.worker\"]}"),
conn,
receivedMessages::add))
.isFalse();
}
@ -63,8 +62,10 @@ public class Stratum1EthProxyProtocolTest {
public void testCanHandleWellFormedRequest() {
assertThat(
protocol.maybeHandle(
"{\"id\":0,\"method\":\"eth_submitLogin\",\"params\":[\"0xdeadbeefdeadbeef.worker\"]}",
conn))
Buffer.buffer(
"{\"id\":0,\"method\":\"eth_submitLogin\",\"params\":[\"0xdeadbeefdeadbeef.worker\"]}"),
conn,
receivedMessages::add))
.isTrue();
}
}

@ -15,70 +15,65 @@
package org.hyperledger.besu.ethereum.stratum;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import io.vertx.core.buffer.Buffer;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.units.bigints.UInt256;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class StratumConnectionTest {
@Mock PoWMiningCoordinator miningCoordinator;
private EpochCalculator epochCalculator;
@Before
@BeforeEach
public void setup() {
miningCoordinator = Mockito.mock(PoWMiningCoordinator.class);
when(miningCoordinator.getEpochCalculator())
.thenReturn(new EpochCalculator.DefaultEpochCalculator());
this.epochCalculator = new EpochCalculator.DefaultEpochCalculator();
}
@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();
StratumConnection conn = new StratumConnection(new StratumProtocol[] {}, notification -> {});
assertThatThrownBy(() -> conn.handleBuffer(Buffer.buffer("{}\n"), bytes -> {}))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void testStratum1WithoutMatches() {
AtomicBoolean called = new AtomicBoolean(false);
when(miningCoordinator.getEpochCalculator()).thenReturn(epochCalculator);
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {new Stratum1Protocol("", miningCoordinator)},
() -> called.set(true),
bytes -> {});
conn.handleBuffer(Buffer.buffer("{}\n"));
assertThat(called.get()).isTrue();
notification -> {});
assertThatThrownBy(() -> conn.handleBuffer(Buffer.buffer("{}\n"), bytes -> {}))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void testStratum1Matches() {
AtomicBoolean called = new AtomicBoolean(false);
AtomicReference<String> message = new AtomicReference<>();
when(miningCoordinator.getEpochCalculator()).thenReturn(epochCalculator);
AtomicReference<Object> message = new AtomicReference<>();
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {
new Stratum1Protocol("", miningCoordinator, () -> "abcd", () -> "abcd")
},
() -> called.set(true),
message::set);
notification -> {});
conn.handleBuffer(
Buffer.buffer(
"{"
@ -87,27 +82,23 @@ public class StratumConnectionTest {
+ " \"params\": [ "
+ " \"MinerName/1.0.0\", \"EthereumStratum/1.0.0\" "
+ " ]"
+ "}\n"));
assertThat(called.get()).isFalse();
+ "}\n"),
message::set);
assertThat(message.get())
.isEqualTo(
"{\"jsonrpc\":\"2.0\",\"id\":23,\"result\":[[\"mining.notify\",\"abcd\",\"EthereumStratum/1.0.0\"],\"\"]}\n");
"{\"jsonrpc\":\"2.0\",\"id\":23,\"result\":[[\"mining.notify\",\"abcd\",\"EthereumStratum/1.0.0\"],\"\"]}");
}
@Test
public void testStratum1SendWork() {
AtomicBoolean called = new AtomicBoolean(false);
AtomicReference<String> message = new AtomicReference<>();
when(miningCoordinator.getEpochCalculator()).thenReturn(epochCalculator);
AtomicReference<Object> message = new AtomicReference<>();
Stratum1Protocol protocol =
new Stratum1Protocol("", miningCoordinator, () -> "abcd", () -> "abcd");
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {protocol}, () -> called.set(true), message::set);
StratumConnection conn = new StratumConnection(new StratumProtocol[] {protocol}, message::set);
conn.handleBuffer(
Buffer.buffer(
"{"
@ -116,7 +107,8 @@ public class StratumConnectionTest {
+ " \"params\": [ "
+ " \"MinerName/1.0.0\", \"EthereumStratum/1.0.0\" "
+ " ]"
+ "}\n"));
+ "}\n"),
message::set);
conn.handleBuffer(
Buffer.buffer(
"{"
@ -125,23 +117,21 @@ public class StratumConnectionTest {
+ " \"params\": [ "
+ " \"someusername\", \"password\" "
+ " ]"
+ "}\n"));
assertThat(called.get()).isFalse();
+ "}\n"),
message::set);
// now send work without waiting.
protocol.setCurrentWorkTask(
new PoWSolverInputs(UInt256.valueOf(3), Bytes.fromHexString("deadbeef"), 42));
assertThat(message.get())
.isEqualTo(
"{\"jsonrpc\":\"2.0\",\"method\":\"mining.notify\",\"params\":[\"abcd\",\"0xdeadbeef\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x0000000000000000000000000000000000000000000000000000000000000003\",true],\"id\":null}\n");
"{\"jsonrpc\":\"2.0\",\"method\":\"mining.notify\",\"params\":[\"abcd\",\"0xdeadbeef\",\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x0000000000000000000000000000000000000000000000000000000000000003\",true],\"id\":null}");
}
@Test
public void testStratum1SubmitHashrate() {
AtomicBoolean called = new AtomicBoolean(false);
AtomicReference<String> message = new AtomicReference<>();
when(miningCoordinator.getEpochCalculator()).thenReturn(epochCalculator);
AtomicReference<Object> message = new AtomicReference<>();
Stratum1Protocol protocol =
new Stratum1Protocol("", miningCoordinator, () -> "abcd", () -> "abcd");
@ -149,8 +139,7 @@ public class StratumConnectionTest {
Mockito.when(miningCoordinator.submitHashRate("0x02", 3L)).thenReturn(true);
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {protocol}, () -> called.set(true), message::set);
new StratumConnection(new StratumProtocol[] {protocol}, notification -> {});
conn.handleBuffer(
Buffer.buffer(
"{"
@ -159,7 +148,8 @@ public class StratumConnectionTest {
+ " \"params\": [ "
+ " \"MinerName/1.0.0\", \"EthereumStratum/1.0.0\" "
+ " ]"
+ "}\n"));
+ "}\n"),
message::set);
conn.handleBuffer(
Buffer.buffer(
"{"
@ -168,46 +158,8 @@ public class StratumConnectionTest {
+ " \"params\": [ "
+ " \"0x03\",\"0x02\" "
+ " ]"
+ "}\n"));
assertThat(called.get()).isFalse();
assertThat(message.get()).isEqualTo("{\"jsonrpc\":\"2.0\",\"id\":23,\"result\":true}\n");
}
@Test
public void testHttpMessage() {
AtomicBoolean called = new AtomicBoolean(false);
AtomicReference<String> received = new AtomicReference<>();
GetWorkProtocol protocol = new GetWorkProtocol(miningCoordinator);
protocol.setCurrentWorkTask(new PoWSolverInputs(UInt256.ZERO, Bytes32.random(), 123L));
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {protocol}, () -> called.set(true), received::set);
String message =
"POST / HTTP/1.1\r\nHost: 127.0.0.1:8008\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nContent-Length: 31\r\n\r\n{\"id\":1,\"method\":\"eth_getWork\"}";
conn.handleBuffer(Buffer.buffer(message));
assertThat(called.get()).isFalse();
assertThat(received.get()).contains("\"jsonrpc\":\"2.0\",\"id\":1,\"result\"");
}
@Test
public void testHttpMessageChunks() {
AtomicBoolean called = new AtomicBoolean(false);
AtomicReference<String> received = new AtomicReference<>();
GetWorkProtocol protocol = new GetWorkProtocol(miningCoordinator);
StratumConnection conn =
new StratumConnection(
new StratumProtocol[] {protocol}, () -> called.set(true), received::set);
String message =
"POST / HTTP/1.1\r\nHost: 127.0.0.1:8008\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nContent-Length: 31\r\n\r\n{\"id";
String secondMessage = "\":1,\"method\":\"eth_getWork\"}";
conn.handleBuffer(Buffer.buffer(message));
conn.handleBuffer(Buffer.buffer(secondMessage));
assertThat(called.get()).isFalse();
assertThat(received.get()).contains("\"jsonrpc\":\"2.0\",\"id\":1,\"result\"");
+ "}\n"),
message::set);
assertThat(message.get()).isEqualTo("{\"jsonrpc\":\"2.0\",\"id\":23,\"result\":true}");
}
}

@ -15,22 +15,47 @@
package org.hyperledger.besu.ethereum.stratum;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
import org.hyperledger.besu.ethereum.mainnet.EpochCalculator;
import org.hyperledger.besu.ethereum.mainnet.PoWSolverInputs;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.StubMetricsSystem;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import java.util.concurrent.CompletableFuture;
import java.nio.charset.StandardCharsets;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.OpenOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetServer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.junit.Test;
import org.mockito.Mockito;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith({MockitoExtension.class, VertxExtension.class})
public class StratumServerTest {
public static final Buffer DEFAULT_TEST_MESSAGE =
new JsonObject()
.put("version", "1.0")
.put("id", 1)
.put("method", RpcMethod.ETH_GET_WORK.getMethodName())
.toBuffer();
@Mock private PoWMiningCoordinator mockPoW;
@Test
public void runJobSetDifficultyShouldSucceed() {
StubMetricsSystem metrics = new StubMetricsSystem();
@ -47,16 +72,332 @@ public class StratumServerTest {
.isEqualTo(UInt256.MAX_VALUE.toUnsignedBigInteger().doubleValue());
}
@Test
public void handleHttpChunkedPost(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server ->
vertx
.fileSystem()
.open("rpc-request.json", new OpenOptions())
.onComplete(
testContext.succeeding(
file -> {
WebClient http =
WebClient.create(
vertx,
new WebClientOptions()
.setDefaultHost(host)
.setDefaultPort(server.actualPort()));
http.post("/")
.sendStream(
file,
testContext.succeeding(
resp -> {
testContext.verify(
() -> {
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.bodyAsString())
.contains(
"\"jsonrpc\":\"2.0\",\"id\":1,\"result\"");
stratum
.stop()
.onComplete(
testContext.succeedingThenComplete());
});
}));
}))));
}
@Test
public void rejectNotHttpPost(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
WebClient http =
WebClient.create(
vertx,
new WebClientOptions()
.setDefaultHost(host)
.setDefaultPort(server.actualPort()));
http.put("/")
.sendBuffer(DEFAULT_TEST_MESSAGE)
.onComplete(
testContext.succeeding(
resp -> {
testContext.verify(
() -> {
assertThat(resp.statusCode()).isEqualTo(405);
stratum
.stop()
.onComplete(testContext.succeedingThenComplete());
});
}));
}));
}
@Test
public void handlePlainText(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
NetClient client = vertx.createNetClient();
client
.connect(server.actualPort(), host)
.onComplete(
testContext.succeeding(
socket ->
socket
.handler(
resp ->
testContext.verify(
() -> {
assertThat(resp.length()).isNotZero();
assertThat(resp.toString())
.contains(
"\"jsonrpc\":\"2.0\",\"id\":1,\"result\"");
stratum
.stop()
.onComplete(
testContext.succeedingThenComplete());
}))
.write(
DEFAULT_TEST_MESSAGE.appendString(System.lineSeparator()))
.onComplete(
writeResult ->
testContext.verify(
() ->
assertThat(writeResult.failed())
.isFalse()))));
}));
}
@Test
public void handlePlainTextMultiple(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
NetClient client = vertx.createNetClient();
client
.connect(server.actualPort(), host)
.onComplete(
testContext.succeeding(
socket -> {
socket.handler(
resp -> {
testContext.verify(
() -> {
assertThat(resp.length()).isNotZero();
String[] responses =
resp.toString(StandardCharsets.UTF_8).split("\n");
assertThat(responses).hasSize(2);
assertThat(responses)
.containsOnly(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}");
stratum
.stop()
.onComplete(testContext.succeedingThenComplete());
});
});
socket
.write(
DEFAULT_TEST_MESSAGE
.copy()
.appendString(System.lineSeparator())
.appendBuffer(DEFAULT_TEST_MESSAGE)
.appendString(System.lineSeparator()))
.onComplete(
writeResult ->
testContext.verify(
() -> assertThat(writeResult.failed()).isFalse()));
}));
}));
}
@Test
public void handleHttpPost(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
WebClient http =
WebClient.create(
vertx,
new WebClientOptions()
.setDefaultHost(host)
.setDefaultPort(server.actualPort()));
http.post("/")
.putHeader("Connection", "keep-alive")
.putHeader("Accept-Encoding", "gzip, deflate")
.sendBuffer(DEFAULT_TEST_MESSAGE)
.onComplete(
testContext.succeeding(
resp -> {
testContext.verify(
() -> {
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.bodyAsString())
.contains("\"jsonrpc\":\"2.0\",\"id\":1,\"result\"");
stratum
.stop()
.onComplete(testContext.succeedingThenComplete());
});
}));
}));
}
@Test
public void handleHttpBadJson(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
WebClient http =
WebClient.create(
vertx,
new WebClientOptions()
.setDefaultHost(host)
.setDefaultPort(server.actualPort()));
http.post("/")
.sendBuffer(Buffer.buffer("{\"jsonrpc\":\"2.0\",..."))
.onComplete(
testContext.succeeding(
resp -> {
testContext.verify(
() -> {
assertThat(resp.statusCode()).isEqualTo(400);
stratum
.stop()
.onComplete(testContext.succeedingThenComplete());
});
}));
}));
}
@Test
public void handleHttpBadJsonRpc(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
WebClient http =
WebClient.create(
vertx,
new WebClientOptions()
.setDefaultHost(host)
.setDefaultPort(server.actualPort()));
http.post("/")
.sendBuffer(
Buffer.buffer(
"{\"jsonrpc\":\"2.0\",\"id\":one,\"method\":\"eth_getWork\"}"))
.onComplete(
testContext.succeeding(
resp -> {
testContext.verify(
() -> {
assertThat(resp.statusCode()).isEqualTo(400);
stratum
.stop()
.onComplete(testContext.succeedingThenComplete());
});
}));
}));
}
@Test
public void handleHttpUnknownMethod(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
WebClient http =
WebClient.create(
vertx,
new WebClientOptions()
.setDefaultHost(host)
.setDefaultPort(server.actualPort()));
http.post("/")
.sendBuffer(
Buffer.buffer(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"unknown_method\"}"))
.onComplete(
testContext.succeeding(
resp -> {
testContext.verify(
() -> {
assertThat(resp.statusCode()).isEqualTo(400);
stratum
.stop()
.onComplete(testContext.succeedingThenComplete());
});
}));
}));
}
@Test
public void handlePlainTextBadMessage(final Vertx vertx, final VertxTestContext testContext) {
String host = "localhost";
StratumServer stratum = new StratumServer(vertx, mockPoW, 0, host, "", new NoOpMetricsSystem());
stratum
.start()
.onComplete(
testContext.succeeding(
server -> {
NetClient client = vertx.createNetClient();
client
.connect(server.actualPort(), host)
.onComplete(
testContext.succeeding(
socket ->
socket
.closeHandler(v -> testContext.completeNow())
.write(
Buffer.buffer(
"{\"jsonrpc\":\"2.0\",..." + System.lineSeparator()))
.onComplete(
writeResult ->
testContext.verify(
() ->
assertThat(writeResult.failed())
.isFalse()))));
}));
}
private StratumServer stratumServerWithMocks(final ObservableMetricsSystem metrics) {
PoWMiningCoordinator mockPoW =
Mockito.when(Mockito.mock(PoWMiningCoordinator.class).getEpochCalculator())
.thenReturn(new EpochCalculator.DefaultEpochCalculator())
.getMock();
when(mockPoW.getEpochCalculator()).thenReturn(new EpochCalculator.DefaultEpochCalculator());
StratumServer ss =
new StratumServer(null, mockPoW, 0, "lo", "", metrics) {
@Override
public CompletableFuture<?> start() {
public Future<NetServer> start() {
this.started.set(true);
return null;
}

@ -0,0 +1,5 @@
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getWork"
}
Loading…
Cancel
Save