package jelectrum; import java.net.Socket; import java.io.InputStream; import java.io.OutputStream; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.TimeUnit; import java.util.concurrent.LinkedBlockingQueue; import java.util.Scanner; import java.util.Random; import org.json.JSONObject; import org.json.JSONArray; import org.apache.commons.codec.binary.Hex; import java.nio.ByteBuffer; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Block; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Transaction; import org.apache.commons.codec.binary.Hex; public class StratumConnection { //ghostbird, dirtnerd, beancurd public static final String JELECTRUM_VERSION="beancurd"; public static final String PROTO_VERSION="1.0"; public static final boolean use_thread_per_request=false; private Jelectrum jelectrum; private StratumServer server; private Socket sock; private String connection_id; private AtomicLong last_network_action; private volatile boolean open; private Config config; private TXUtil tx_util; private long connection_start_time; private String version_info; private String first_address; private AtomicInteger subscription_count = new AtomicInteger(0); private RateLimit session_rate_limit; private LinkedBlockingQueue<JSONObject> out_queue = new LinkedBlockingQueue<JSONObject>(); private long get_client_id=-1; private String client_version; public static final long PRINT_INFO_DELAY=15000L; private String banner="Jelectrum"; private boolean detail_logs = false; public StratumConnection(Jelectrum jelectrum, StratumServer server, Socket sock, String connection_id) throws IOException { this.jelectrum = jelectrum; this.tx_util = new TXUtil(jelectrum.getDB(), jelectrum.getNetworkParameters()); this.server = server; this.config = server.getConfig(); this.sock = sock; this.connection_id = connection_id; detail_logs = jelectrum.getConfig().getBoolean("connection_detail_logs"); open=true; last_network_action=new AtomicLong(System.nanoTime()); if (detail_logs) jelectrum.getEventLog().log("New connection from: " + sock + " " + connection_id); connection_start_time = System.currentTimeMillis(); if (jelectrum.getConfig().get("banner_file") != null) { String banner_file_path = jelectrum.getConfig().get("banner_file"); DataInputStream d_in = new DataInputStream(new FileInputStream(banner_file_path)); int len = (int)new File(banner_file_path).length(); byte b[] = new byte[len]; d_in.readFully(b); banner = new String(b); d_in.close(); } if (jelectrum.getConfig().isSet("session_rate_limit")) { session_rate_limit = new RateLimit(jelectrum.getConfig().getDouble("session_rate_limit"), 2.0); } new OutThread().start(); new InThread().start(); } public void close() { open=false; try { sock.close(); } catch(Throwable t){} } public boolean isOpen() { return open; } public long getLastNetworkAction() { return last_network_action.get(); } protected void updateLastNetworkAction() { last_network_action.set(System.nanoTime()); } public String getId() { return connection_id; } private void logRequest(String method, int input_size, int output_size) { if (detail_logs) { jelectrum.getEventLog().log(connection_id + " - " + method + " in: " + input_size + " out: " + output_size); } } public void sendMessage(JSONObject msg) { try { out_queue.put(msg); } catch(java.lang.InterruptedException e) { throw new RuntimeException(e); } } public class OutThread extends Thread { public OutThread() { setName("OutThread"); setDaemon(true); } public void run() { boolean info_printed=false; try { PrintStream out = new PrintStream(sock.getOutputStream()); while(open) { //Using poll rather than take so this thread will //exit if the connection is closed. Otherwise, //it would wait forever on this queue JSONObject msg = out_queue.poll(30, TimeUnit.SECONDS); if (msg != null) { String msg_str = msg.toString(0); if (session_rate_limit != null) { session_rate_limit.waitForRate(msg_str.length()); } server.applyGlobalRateLimit(msg_str.length()); out.println(msg_str); out.flush(); //System.out.println("Out: " + msg.toString()); updateLastNetworkAction(); } if (!info_printed) if (open) if (first_address != null) if (System.currentTimeMillis() > PRINT_INFO_DELAY + connection_start_time) { if (detail_logs) jelectrum.getEventLog().log(connection_id + " - " + version_info + " " + subscription_count.get()); info_printed=true; } } } catch(Throwable e) { System.out.println(connection_id + ": " + e); } finally { close(); } } } public class InThread extends Thread { public InThread() { setName("InThread"); setDaemon(true); } public void run() { String line = null; try { Scanner scan = new Scanner(sock.getInputStream()); while(open) { line = scan.nextLine(); updateLastNetworkAction(); int input_size = line.length(); line = line.trim(); if (line.length() > 0) { JSONObject msg = new JSONObject(line); if (use_thread_per_request) { new InWorkerThread(msg, input_size).start(); } else { processInMessage(msg, input_size); } } } } catch(java.util.NoSuchElementException e) { if (detail_logs) jelectrum.getEventLog().log("Connection closed " + sock + " " + connection_id); } catch(Throwable e) { jelectrum.getEventLog().log("Unexpected error ("+connection_id+"): " + e + " line: " + line); } finally { close(); } } } public class InWorkerThread extends Thread { private JSONObject msg; private int input_size; public InWorkerThread(JSONObject msg, int input_size) { this.msg = msg; this.input_size = input_size; setName("InWorkerThread"); setDaemon(true); } public void run() { try { processInMessage(msg, input_size); } catch(Throwable t) { if (detail_logs) { jelectrum.getEventLog().log(connection_id + ": error in processing: " + t); } close(); } } } private void processInMessage(JSONObject msg, int input_size) throws Exception { long idx = msg.optLong("id",-1); Object id = msg.opt("id"); try { if (!msg.has("method")) { jelectrum.getEventLog().alarm(connection_id + " - Unknown message: " + msg.toString()); return; } String method = msg.getString("method"); if (method.equals("server.version")) { JSONObject reply = new JSONObject(); reply.put("id", id); reply.put("result",PROTO_VERSION); reply.put("jelectrum",JELECTRUM_VERSION); version_info = msg.get("params").toString(); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("server.banner")) { JSONObject reply = new JSONObject(); reply.put("id", id); reply.put("result",banner); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("blockchain.headers.subscribe")) { //Should send this on each new block: //{"id": 1, "result": {"nonce": 3114737334, "prev_block_hash": "000000000000000089e1f388af7cda336b6241b3f0b0ca36def7a8f22e44d39b", "timestamp": 1387995813, "merkle_root": "0debf5bd535624a955d229337a9bf3da5f370cc5a1f5fbee7261b0bdd0bd0f10", "block_height": 276921, "version": 2, "bits": 419668748}} logRequest(method, input_size, 0); jelectrum.getElectrumNotifier().registerBlockchainHeaders(this, id, true); } else if (method.equals("blockchain.numblocks.subscribe")) { //Should send this on each new block: //{"id": 1, "result": {"nonce": 3114737334, "prev_block_hash": "000000000000000089e1f388af7cda336b6241b3f0b0ca36def7a8f22e44d39b", "timestamp": 1387995813, "merkle_root": "0debf5bd535624a955d229337a9bf3da5f370cc5a1f5fbee7261b0bdd0bd0f10", "block_height": 276921, "version": 2, "bits": 419668748}} logRequest(method, input_size, 0); jelectrum.getElectrumNotifier().registerBlockCount(this, id, true); } else if (method.equals("blockchain.address.get_history")) { //{"id": 29, "result": [{"tx_hash": "fc054ede2383904323d9b54991693b9150bb1a0a7cd3c344afb883d3ffc093f4", "height": 274759}, {"tx_hash": "9dc9363fe032e08630057edb61488fc8fa9910d8b21f02eb1b12ef2928c88550", "height": 274709}]} JSONArray params = msg.getJSONArray("params"); String address = params.getString(0); logRequest(method, input_size, 0); jelectrum.getElectrumNotifier().sendAddressHistory(this, id, address); } else if (method.equals("blockchain.address.get_proof")) { JSONArray params = msg.getJSONArray("params"); String address = params.getString(0); logRequest(method, input_size, 0); jelectrum.getElectrumNotifier().sendAddressHistory(this, id, address); } else if (method.equals("blockchain.address.get_balance")) { JSONArray params = msg.getJSONArray("params"); String address = params.getString(0); jelectrum.getElectrumNotifier().sendAddressBalance(this, id, address); } else if (method.equals("blockchain.address.listunspent")) { JSONArray params = msg.getJSONArray("params"); String address = params.getString(0); logRequest(method, input_size, 0); jelectrum.getElectrumNotifier().sendUnspent(this, id, address); } else if (method.equals("blockchain.address.subscribe")) { //the result is //sha256(fc054ede2383904323d9b54991693b9150bb1a0a7cd3c344afb883d3ffc093f4:274759:7bb11e62ceb5c9e918d9de541ec8d5d215353c6bbf2fcb32b300ec641f3a0b3f:274708:) //tx:height:tx:height: //or null if no transactions JSONArray params = msg.getJSONArray("params"); String address = params.getString(0); subscription_count.getAndIncrement(); if (first_address==null) { first_address=address; } logRequest(method, input_size, 0); jelectrum.getElectrumNotifier().registerBlockchainAddress(this, id, true, address); } else if (method.equals("server.peers.subscribe")) { JSONObject reply = new JSONObject(); JSONArray lst = jelectrum.getPeerManager().getPeers(); reply.put("id", id); reply.put("result", lst); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("server.features")) { JSONObject reply = new JSONObject(); JSONObject data = jelectrum.getPeerManager().getServerFeatures(); reply.put("id", id); reply.put("result", data); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("server.add_peer")) { JSONObject reply = new JSONObject(); reply.put("id", id); JSONArray params = msg.getJSONArray("params"); jelectrum.getPeerManager().addPeers(params); reply.put("result", "OK"); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("server.donation_address")) { JSONObject reply = new JSONObject(); reply.put("id", id); String addr = ""; if (config.isSet("donation_address")) { addr = config.get("donation_address"); } reply.put("result", addr); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("blockchain.transaction.get")) { JSONObject reply = new JSONObject(); reply.put("id", id); JSONArray params = msg.getJSONArray("params"); Sha256Hash hash = null; try { hash =new Sha256Hash( params.getString(0)); } catch(Throwable t) { throw new Exception("Bad transaction hash: " + params.getString(0)); } Transaction tx = tx_util.getTransaction(hash); if (tx==null) { reply.put("error","unknown transaction"); } else { byte buff[]= tx.bitcoinSerialize(); reply.put("result", Util.getHexString(buff)); } logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("blockchain.block.get_header")) { JSONObject reply = new JSONObject(); reply.put("id", id); JSONArray arr = msg.getJSONArray("params"); int height = arr.getInt(0); Sha256Hash block_hash = jelectrum.getBlockChainCache().getBlockHashAtHeight(height); StoredBlock blk = jelectrum.getDB().getBlockStoreMap().get(block_hash); JSONObject result = new JSONObject(); jelectrum.getElectrumNotifier().populateBlockData(blk, result); reply.put("result", result); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("blockchain.transaction.broadcast")) { JSONArray arr = msg.getJSONArray("params"); String hex = arr.getString(0); byte[] tx_data = new Hex().decode(hex.getBytes()); Transaction tx = new Transaction(jelectrum.getNetworkParameters(), tx_data); jelectrum.getPeerGroup().broadcastTransaction(tx); if (jelectrum.getBitcoinRPC()!=null) { jelectrum.getBitcoinRPC().submitTransaction(hex); } JSONObject reply = new JSONObject(); reply.put("id", id); reply.put("result", tx.getHash().toString()); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); jelectrum.getImporter().saveTransaction(tx); } else if (method.equals("blockchain.transaction.get_merkle")) { JSONObject reply = new JSONObject(); reply.put("id", id); JSONArray arr = msg.getJSONArray("params"); Sha256Hash tx_hash = new Sha256Hash(arr.getString(0)); int height = arr.getInt(1); Sha256Hash block_hash = jelectrum.getBlockChainCache().getBlockHashAtHeight(height); Block blk = jelectrum.getDB().getBlockMap().get(block_hash).getBlock(jelectrum.getNetworkParameters()); JSONObject result = Util.getMerkleTreeForTransaction(blk.getTransactions(), tx_hash); result.put("block_height", height); reply.put("result", result); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("blockchain.block.get_chunk")) { JSONObject reply = new JSONObject(); reply.put("id", id); JSONArray arr = msg.getJSONArray("params"); int index = arr.getInt(0); reply.put("result", jelectrum.getHeaderChunkAgent().getChunk(index)); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("blockchain.estimatefee")) { JSONObject reply = new JSONObject(); reply.put("id", id); JSONArray arr = msg.getJSONArray("params"); int block = arr.getInt(0); double fee = -1; if (jelectrum.getBitcoinRPC() != null) { fee = jelectrum.getBitcoinRPC().getFeeEstimate(block); } else { fee = Util.getFeeEstimateMap().get(block); } reply.put("result", fee); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else if (method.equals("blockchain.relayfee")) { JSONObject reply = new JSONObject(); reply.put("id", id); double fee = 0.0; if (jelectrum.getBitcoinRPC() != null) { fee = jelectrum.getBitcoinRPC().getRelayFee(); } reply.put("result", fee); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } else { jelectrum.getEventLog().alarm(connection_id + " - Unknown electrum method: " + method); System.out.println(msg); JSONObject reply = new JSONObject(); reply.put("id", id); reply.put("error","unknown method - " + method); logRequest(method, input_size, reply.toString().length()); sendMessage(reply); } } catch(Throwable t) { JSONObject reply = new JSONObject(); reply.put("id", id); reply.put("error","Exception: " + t); sendMessage(reply); jelectrum.getEventLog().log(connection_id + " - error: " + t); jelectrum.getEventLog().log(t); if (detail_logs) { t.printStackTrace(); } close(); } } }