/*
* DiabloMiner - OpenCL miner for BitCoin
* Copyright (C) 2010, 2011, 2012 Patrick McFarland <diablod3@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.diablominer.DiabloMiner.NetworkState;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.util.Formatter;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import org.apache.commons.codec.binary.Base64;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.NullNode;
import org.codehaus.jackson.node.ObjectNode;
import com.diablominer.DiabloMiner.DiabloMiner;
import com.diablominer.DiabloMiner.DeviceState.DeviceState.ExecutionState;
public class JSONRPCNetworkState extends NetworkState {
URL longPollUrl;
String userPass;
boolean rollNTime = false;
boolean noDelay = false;
String rejectReason = null;
final GetWorkAsync getWorkAsync = this.new GetWorkAsync();
final SendWorkAsync sendWorkAsync = this.new SendWorkAsync();
LongPollAsync longPollAsync = null;
LinkedBlockingDeque<WorkState> incomingQueue = new LinkedBlockingDeque<WorkState>();
final ObjectMapper mapper = new ObjectMapper();
public JSONRPCNetworkState(DiabloMiner diabloMiner, URL queryUrl, String user, String pass, byte hostChain) {
super(diabloMiner, queryUrl, user, pass, hostChain);
this.userPass = "Basic " + Base64.encodeBase64String((user + ":" + pass).getBytes()).trim().replace("\r\n", "");
Thread thread = new Thread(getWorkAsync, "DiabloMiner JSONRPC GetWorkAsync for " + queryUrl.getHost());
thread.start();
diabloMiner.addThread(thread);
thread = new Thread(sendWorkAsync, "DiabloMiner JSONRPC SendWorkAsync for " + queryUrl.getHost());
thread.start();
diabloMiner.addThread(thread);
}
JsonNode doJSONRPCCall(boolean longPoll, ObjectNode message) throws IOException {
HttpURLConnection connection = null;
try {
URL url;
if(longPoll)
url = longPollUrl;
else
url = queryUrl;
Proxy proxy = diabloMiner.getProxy();
if(proxy == null)
connection = (HttpURLConnection) url.openConnection();
else
connection = (HttpURLConnection) url.openConnection(proxy);
if(longPoll) {
connection.setConnectTimeout(10 * 60 * 1000);
connection.setReadTimeout(10 * 60 * 1000);
} else {
connection.setConnectTimeout(15 * 1000);
connection.setReadTimeout(15 * 1000);
}
connection.setRequestProperty("Authorization", userPass);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("Accept-Encoding", "gzip,deflate");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Cache-Control", "no-cache");
connection.setRequestProperty("User-Agent", "DiabloMiner");
connection.setRequestProperty("X-Mining-Extensions", "longpoll rollntime switchto");
connection.setDoOutput(true);
OutputStream requestStream = connection.getOutputStream();
Writer request = new OutputStreamWriter(requestStream);
request.write(message.toString());
request.close();
requestStream.close();
ObjectNode responseMessage = null;
InputStream responseStream = null;
try {
String xLongPolling = connection.getHeaderField("X-Long-Polling");
if(xLongPolling != null && !"".equals(xLongPolling) && longPollAsync == null) {
if(xLongPolling.startsWith("http"))
longPollUrl = new URL(xLongPolling);
else if(xLongPolling.startsWith("/"))
longPollUrl = new URL(queryUrl.getProtocol(), queryUrl.getHost(), queryUrl.getPort(), xLongPolling);
else
longPollUrl = new URL(queryUrl.getProtocol(), queryUrl.getHost(), queryUrl.getPort(), (url.getFile() + "/" + xLongPolling).replace("//", "/"));
longPollAsync = new LongPollAsync();
Thread thread = new Thread(longPollAsync, "DiabloMiner JSONRPC LongPollAsync for " + url.getHost());
thread.start();
diabloMiner.addThread(thread);
workLifetime = 60000;
diabloMiner.debug(queryUrl.getHost() + ": Enabling long poll support");
}
String xRollNTime = connection.getHeaderField("X-Roll-NTime");
if(xRollNTime != null && !"".equals(xRollNTime)) {
if(!"n".equalsIgnoreCase(xRollNTime) && rollNTime == false) {
rollNTime = true;
if(xRollNTime.startsWith("expire=")) {
try {
workLifetime = Integer.parseInt(xRollNTime.substring(7)) * 1000;
} catch(NumberFormatException ex) { }
} else {
workLifetime = 60000;
}
diabloMiner.debug(queryUrl.getHost() + ": Enabling roll ntime support, expire after " + (workLifetime / 1000) + " seconds");
} else if("n".equalsIgnoreCase(xRollNTime) && rollNTime == true) {
rollNTime = false;
if(longPoll)
workLifetime = 60000;
else
workLifetime = diabloMiner.getWorkLifetime();
diabloMiner.debug(queryUrl.getHost() + ": Disabling roll ntime support");
}
}
String xSwitchTo = connection.getHeaderField("X-Switch-To");
if(xSwitchTo != null && !"".equals(xSwitchTo)) {
String oldHost = queryUrl.getHost();
JsonNode newHost = mapper.readTree(xSwitchTo);
queryUrl = new URL(queryUrl.getProtocol(), newHost.get("host").asText(), newHost.get("port").getIntValue(), queryUrl.getPath());
if(longPollUrl != null)
longPollUrl = new URL(longPollUrl.getProtocol(), newHost.get("host").asText(), newHost.get("port").getIntValue(), longPollUrl.getPath());
diabloMiner.info(oldHost + ": Switched to " + queryUrl.getHost());
}
String xRejectReason = connection.getHeaderField("X-Reject-Reason");
if(xRejectReason != null && !"".equals(xRejectReason)) {
rejectReason = xRejectReason;
}
String xIsP2Pool = connection.getHeaderField("X-Is-P2Pool");
if(xIsP2Pool != null && !"".equals(xIsP2Pool)) {
if(!noDelay)
diabloMiner.info("P2Pool no delay mode enabled");
noDelay = true;
}
if(connection.getContentEncoding() != null) {
if(connection.getContentEncoding().equalsIgnoreCase("gzip"))
responseStream = new GZIPInputStream(connection.getInputStream());
else if(connection.getContentEncoding().equalsIgnoreCase("deflate"))
responseStream = new InflaterInputStream(connection.getInputStream());
} else {
responseStream = connection.getInputStream();
}
if(responseStream == null)
throw new IOException("Drop to error handler");
Object output = mapper.readTree(responseStream);
if(NullNode.class.equals(output.getClass())) {
throw new IOException("Bitcoin returned unparsable JSON");
} else {
try {
responseMessage = (ObjectNode) output;
} catch(ClassCastException e) {
throw new IOException("Bitcoin returned unparsable JSON");
}
}
responseStream.close();
} catch(JsonProcessingException e) {
throw new IOException("Bitcoin returned unparsable JSON");
} catch(IOException e) {
InputStream errorStream = null;
IOException e2 = null;
if(connection.getErrorStream() == null)
throw new IOException("Bitcoin disconnected during response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
if(connection.getContentEncoding() != null) {
if(connection.getContentEncoding().equalsIgnoreCase("gzip"))
errorStream = new GZIPInputStream(connection.getErrorStream());
else if(connection.getContentEncoding().equalsIgnoreCase("deflate"))
errorStream = new InflaterInputStream(connection.getErrorStream());
} else {
errorStream = connection.getErrorStream();
}
if(errorStream == null)
throw new IOException("Bitcoin disconnected during response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
byte[] errorbuf = new byte[8192];
if(errorStream.read(errorbuf) < 1)
throw new IOException("Bitcoin returned an error, but with no message");
String error = new String(errorbuf).trim();
if(error.startsWith("{")) {
try {
Object output = mapper.readTree(error);
if(NullNode.class.equals(output.getClass()))
throw new IOException("Bitcoin returned an error message: " + error);
else
try {
responseMessage = (ObjectNode) output;
} catch(ClassCastException f) {
throw new IOException("Bitcoin returned unparsable JSON");
}
if(responseMessage.get("error") != null) {
if(responseMessage.get("error").get("message") != null && responseMessage.get("error").get("message").asText() != null) {
error = responseMessage.get("error").get("message").asText().trim();
e2 = new IOException("Bitcoin returned error message: " + error);
} else if(responseMessage.get("error").asText() != null) {
error = responseMessage.get("error").asText().trim();
if(!"null".equals(error) && !"".equals(error))
e2 = new IOException("Bitcoin returned an error message: " + error);
}
}
} catch(JsonProcessingException f) {
e2 = new IOException("Bitcoin returned unparsable JSON");
}
} else {
e2 = new IOException("Bitcoin returned an error message: " + error);
}
errorStream.close();
if(responseStream != null)
responseStream.close();
if(e2 == null)
e2 = new IOException("Bitcoin returned an error, but with no message");
throw e2;
}
if(responseMessage.get("error") != null) {
if(responseMessage.get("error").get("message") != null && responseMessage.get("error").get("message").asText() != null) {
String error = responseMessage.get("error").get("message").asText().trim();
throw new IOException("Bitcoin returned error message: " + error);
} else if(responseMessage.get("error").asText() != null) {
String error = responseMessage.get("error").asText().trim();
if(!"null".equals(error) && !"".equals(error))
throw new IOException("Bitcoin returned error message: " + error);
}
}
JsonNode result;
try {
result = responseMessage.get("result");
} catch(Exception e) {
throw new IOException("Bitcoin returned unparsable JSON");
}
if(result == null)
throw new IOException("Bitcoin did not return a result or an error");
return result;
} catch(IOException e) {
if(connection != null)
connection.disconnect();
throw e;
}
}
WorkState doGetWorkMessage(boolean longPoll) throws IOException {
ObjectNode getWorkMessage = mapper.createObjectNode();
getWorkMessage.put("method", "getwork");
getWorkMessage.putArray("params");
getWorkMessage.put("id", 1);
JsonNode responseMessage = doJSONRPCCall(longPoll, getWorkMessage);
String datas;
String midstates;
String targets;
try {
datas = responseMessage.get("data").asText();
midstates = responseMessage.get("midstate").asText();
targets = responseMessage.get("target").asText();
} catch(Exception e) {
throw new IOException("Bitcoin returned unparsable JSON");
}
WorkState workState = new WorkState(this);
String parse;
for(int i = 0; i < 32; i++) {
parse = datas.substring(i * 8, (i * 8) + 8);
workState.setData(i, Integer.reverseBytes((int) Long.parseLong(parse, 16)));
}
for(int i = 0; i < 8; i++) {
parse = midstates.substring(i * 8, (i * 8) + 8);
workState.setMidstate(i, Integer.reverseBytes((int) Long.parseLong(parse, 16)));
}
for(int i = 0; i < 8; i++) {
parse = targets.substring(i * 8, (i * 8) + 8);
workState.setTarget(i, (Long.reverseBytes(Long.parseLong(parse, 16) << 16)) >>> 16);
}
return workState;
}
boolean doSendWorkMessage(WorkState workState) throws IOException {
StringBuilder dataOutput = new StringBuilder(8 * 32 + 1);
Formatter dataFormatter = new Formatter(dataOutput);
int[] data = workState.getData();
dataFormatter.format(
"%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x" +
"%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x",
Integer.reverseBytes(data[0]), Integer.reverseBytes(data[1]),
Integer.reverseBytes(data[2]), Integer.reverseBytes(data[3]),
Integer.reverseBytes(data[4]), Integer.reverseBytes(data[5]),
Integer.reverseBytes(data[6]), Integer.reverseBytes(data[7]),
Integer.reverseBytes(data[8]), Integer.reverseBytes(data[9]),
Integer.reverseBytes(data[10]), Integer.reverseBytes(data[11]),
Integer.reverseBytes(data[12]), Integer.reverseBytes(data[13]),
Integer.reverseBytes(data[14]), Integer.reverseBytes(data[15]),
Integer.reverseBytes(data[16]), Integer.reverseBytes(data[17]),
Integer.reverseBytes(data[18]), Integer.reverseBytes(data[19]),
Integer.reverseBytes(data[20]), Integer.reverseBytes(data[21]),
Integer.reverseBytes(data[22]), Integer.reverseBytes(data[23]),
Integer.reverseBytes(data[24]), Integer.reverseBytes(data[25]),
Integer.reverseBytes(data[26]), Integer.reverseBytes(data[27]),
Integer.reverseBytes(data[28]), Integer.reverseBytes(data[29]),
Integer.reverseBytes(data[30]), Integer.reverseBytes(data[31]));
ObjectNode sendWorkMessage = mapper.createObjectNode();
sendWorkMessage.put("method", "getwork");
ArrayNode params = sendWorkMessage.putArray("params");
params.add(dataOutput.toString());
sendWorkMessage.put("id", 1);
JsonNode responseMessage = doJSONRPCCall(false, sendWorkMessage);
boolean accepted;
dataFormatter.close();
try {
accepted = responseMessage.getBooleanValue();
} catch(Exception e) {
throw new IOException("Bitcoin returned unparsable JSON");
}
return accepted;
}
class GetWorkAsync implements Runnable {
public void run() {
while(diabloMiner.getRunning()) {
ExecutionState executionState = null;
try {
executionState = getQueue.take();
} catch(InterruptedException e) {
continue;
}
if(executionState != null) {
WorkState workState = incomingQueue.poll();
if(workState == null) {
try {
workState = doGetWorkMessage(false);
} catch (IOException e) {
diabloMiner.error("Cannot connect to " + queryUrl.getHost() + ": " + e.getLocalizedMessage());
networkStateNext.addGetQueue(executionState);
try {
if(!noDelay)
Thread.sleep(250);
} catch(InterruptedException f) { }
continue;
}
}
workState.setExecutionState(executionState);
executionState.addIncomingQueue(workState);
}
}
}
}
class SendWorkAsync implements Runnable {
public void run() {
while(diabloMiner.getRunning()) {
WorkState workState = null;
try {
workState = sendQueue.take();
} catch(InterruptedException e) {
continue;
}
if(workState != null) {
boolean accepted;
try {
accepted = doSendWorkMessage(workState);
} catch (IOException e) {
diabloMiner.error("Cannot connect to " + queryUrl.getHost() + ": " + e.getLocalizedMessage());
sendQueue.addFirst(workState);
try {
if(!noDelay)
Thread.sleep(250);
} catch(InterruptedException f) { }
continue;
}
if(accepted) {
diabloMiner.info(queryUrl.getHost() + " accepted block " + diabloMiner.incrementBlocks() + " from " + workState.getExecutionState().getExecutionName());
} else {
diabloMiner.info(queryUrl.getHost() + " rejected block " + diabloMiner.incrementRejects() + " from " + workState.getExecutionState().getExecutionName());
diabloMiner.debug("Rejected block " + (float) ((DiabloMiner.now() - workState.timestamp) / 1000.0) + " seconds old, roll ntime set to " + workState.getNetworkState().getRollNTime() + ", rolled " + workState.getRolledNTime() + " times");
}
if(rejectReason != null) {
diabloMiner.info("Reject reason: " + rejectReason);
rejectReason = null;
}
}
}
}
}
class LongPollAsync implements Runnable {
public void run() {
while(diabloMiner.getRunning()) {
try {
WorkState workState = doGetWorkMessage(true);
incomingQueue.add(workState);
refreshTimestamp.set(workState.getTimestamp());
diabloMiner.debug(queryUrl.getHost() + ": Long poll returned");
} catch(IOException e) {
diabloMiner.error("Cannot connect to " + queryUrl.getHost() + ": " + e.getLocalizedMessage());
}
try {
if(!noDelay)
Thread.sleep(250);
} catch(InterruptedException e) { }
}
}
}
}