mirror of https://github.com/hyperledger/besu
Stratum Server Support (#140)
Add support for external GPU mining via the stratum protocol. Three new CLI Options support this: `--miner-stratum-enabled`, `--miner-stratum-host`, and `--miner-stratum-port`. To use stratum first use the `--miner-enabled` option and add the `--miner-stratum-enabled` option. This disables local CPU mining and opens up a stratum server, configurable via `--miner-stratum-host` (default is `0.0.0.0`) and `--miner-stratum-port` (default is 8008). This server supports `stratum+tcp` mining and the JSON-RPC services (if enabled) will support the `eth_getWork` and `eth_submitWork` calls as well (supporting `getwork` or `http` schemes). This is known to work with ethminer. Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>pull/182/head
parent
dd46332530
commit
55e24759b7
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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"); |
||||
} |
||||
} |
Loading…
Reference in new issue