package jelectrum; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; import java.util.Collection; import java.util.List; import java.util.LinkedList; import java.util.ArrayList; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Address; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.Block; import org.bitcoinj.core.AddressFormatException; import org.json.JSONObject; import org.json.JSONArray; import com.google.protobuf.ByteString; import org.junit.Assert; /** * Why is the logic of preparing results for clients * mixed between here and StratumConnection? This needs to be refactored. */ public class ElectrumNotifier { Map<String, Subscriber> block_subscribers; Map<String, Subscriber> blocknum_subscribers; Map<ByteString, Map<String, Subscriber> > scripthash_subscribers; LRUCache<ByteString, String> scripthash_sums; Jelectrum jelly; private TXUtil tx_util; volatile StoredBlock chain_head; Object chain_head_lock= new Object(); public ElectrumNotifier(Jelectrum jelly) { this.jelly = jelly; tx_util = jelly.getDB().getTXUtil(); block_subscribers = new HashMap<String, Subscriber>(512, 0.5f); blocknum_subscribers = new HashMap<String, Subscriber>(512, 0.5f); scripthash_subscribers = new HashMap<ByteString, Map<String, Subscriber> >(512, 0.5f); scripthash_sums = new LRUCache<ByteString, String>(10000); } public void start() throws org.bitcoinj.store.BlockStoreException { chain_head = jelly.getBlockStore().getChainHead(); new PruneThread().start(); } public int getHeadHeight() { return chain_head.getHeight(); } public void registerBlockchainHeaders(StratumConnection conn, Object request_id, boolean send_initial) { Subscriber sub = new Subscriber(conn, request_id); synchronized(block_subscribers) { String conn_id = conn.getId(); block_subscribers.put(conn_id, sub); } if (send_initial) { StoredBlock blk = chain_head; try { JSONObject reply = sub.startReply(); JSONObject block_data = new JSONObject(); populateBlockData(blk, block_data); reply.put("result", block_data); reply.put("jsonrpc", "2.0"); sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } } public void registerBlockCount(StratumConnection conn, Object request_id, boolean send_initial) { Subscriber sub = new Subscriber(conn, request_id); synchronized(blocknum_subscribers) { String conn_id = conn.getId(); blocknum_subscribers.put(conn_id, sub); } if (send_initial) { StoredBlock blk = chain_head; try { JSONObject reply = sub.startReply(); reply.put("result", blk.getHeight()); reply.put("jsonrpc", "2.0"); sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } } public void notifyNewBlock(Block b) { if (chain_head == null) return; StoredBlock blk = null; blk = jelly.getBlockStore().get(b.getHash()); synchronized(chain_head_lock) { if (blk.getHeight() >= chain_head.getHeight()) { chain_head = blk; } } synchronized(block_subscribers) { for(Subscriber sub : block_subscribers.values()) { blockNotify(sub, chain_head); } } synchronized(blocknum_subscribers) { for(Subscriber sub : blocknum_subscribers.values()) { blockNumNotify(sub, chain_head); } } } public void notifyNewTransaction(Collection<ByteString> scripthashes, int height) { Assert.assertNotNull(scripthashes); synchronized(scripthash_sums) { for(ByteString s : scripthashes) { scripthash_sums.remove(s); } } //Inside a sync do a deep copy of just the entries that we need Map<ByteString, Map<String, Subscriber> > scripthash_subscribers_copy = new HashMap<ByteString, Map<String, Subscriber>>(); synchronized(scripthash_subscribers) { for(ByteString s : scripthashes) { Map<String, Subscriber> m = scripthash_subscribers.get(s); if (m != null) { TreeMap<String, Subscriber> copy = new TreeMap<String, Subscriber>(); copy.putAll(m); scripthash_subscribers_copy.put(s, m); } } } //Now with our clean copy we can do the notifications without holding any locks try { for(ByteString s : scripthashes) { Map<String, Subscriber> m = scripthash_subscribers_copy.get(s); if ((m != null) && (m.size() > 0)) { String sum = getScriptHashChecksum(s); JSONObject reply = new JSONObject(); JSONArray info = new JSONArray(); info.put(Util.getHexString(s)); info.put(sum); reply.put("jsonrpc", "2.0"); reply.put("params", info); reply.put("id", JSONObject.NULL); reply.put("method", "blockchain.scripthash.subscribe"); for(Subscriber sub : m.values()) { sub.sendReply(reply); } } } } catch(org.json.JSONException e) { throw new RuntimeException(e); } catch(jelectrum.db.DBTooManyResultsException e) { //LOL } } private void blockNotify(Subscriber sub, StoredBlock blk) { try { JSONObject reply = new JSONObject(); JSONObject block_data = new JSONObject(); populateBlockData(blk, block_data); JSONArray crap = new JSONArray(); crap.put(block_data); reply.put("params", crap); reply.put("jsonrpc", "2.0"); reply.put("id", JSONObject.NULL); reply.put("method", "blockchain.headers.subscribe"); sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } private void blockNumNotify(Subscriber sub, StoredBlock blk) { try { JSONObject reply = new JSONObject(); JSONArray crap = new JSONArray(); crap.put(blk.getHeight()); reply.put("params", crap); reply.put("id", JSONObject.NULL); reply.put("jsonrpc", "2.0"); reply.put("method", "blockchain.numblocks.subscribe"); sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } public void populateBlockData(StoredBlock blk, JSONObject block_data) throws org.json.JSONException { Block header = blk.getHeader(); block_data.put("nonce", header.getNonce()); block_data.put("prev_block_hash", header.getPrevBlockHash().toString()); block_data.put("timestamp", header.getTimeSeconds()); block_data.put("merkle_root", header.getMerkleRoot().toString()); block_data.put("block_height", blk.getHeight()); block_data.put("version",header.getVersion()); block_data.put("bits", header.getDifficultyTarget()); block_data.put("height", blk.getHeight()); block_data.put("hex", Util.getHeaderHex(header)); //block_data.put("utxo_root", jelly.getUtxoTrieMgr().getRootHash(header.getHash())); } public void registerBlockchainAddress(StratumConnection conn, Object request_id, boolean send_initial, ByteString scripthash) { Subscriber sub = new Subscriber(conn, request_id); synchronized(scripthash_subscribers) { if (scripthash_subscribers.get(scripthash) == null) { scripthash_subscribers.put(scripthash, new TreeMap<String, Subscriber>()); } scripthash_subscribers.get(scripthash).put(conn.getId(), sub); } if (send_initial) { try { JSONObject reply = sub.startReply(); String sum = getScriptHashChecksum(scripthash); if (sum==null) { reply.put("result", JSONObject.NULL); } else { reply.put("result", sum); } sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } } public void sendAddressHistory(StratumConnection conn, Object request_id, ByteString scripthash, boolean include_confirmed, boolean include_mempool) { Subscriber sub = new Subscriber(conn, request_id); try { JSONObject reply = sub.startReply(); reply.put("result", getScriptHashHistory(scripthash,include_confirmed,include_mempool)); sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } public void sendUnspent(StratumConnection conn, Object request_id, ByteString target) throws AddressFormatException { try { Subscriber sub = new Subscriber(conn, request_id); JSONObject reply = sub.startReply(); Collection<TransactionOutPoint> outs = jelly.getUtxoSource().getUnspentForScriptHash(target); JSONArray arr =new JSONArray(); for(TransactionOutPoint out : outs) { JSONObject o = new JSONObject(); o.put("tx_hash", out.getHash().toString()); o.put("tx_pos", out.getIndex()); SortedTransaction s_tx = new SortedTransaction(out.getHash(), false); Transaction tx = s_tx.tx; long value = tx.getOutput((int)out.getIndex()).getValue().longValue(); o.put("value",value); o.put("height", s_tx.getEffectiveHeight()); arr.put(o); } reply.put("result", arr); sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } public void sendAddressBalance(StratumConnection conn, Object request_id, ByteString target) throws AddressFormatException { Subscriber sub = new Subscriber(conn, request_id); try { JSONObject reply = sub.startReply(); List<SortedTransaction> lst = getTransactionsForScriptHash(target, true, true); TreeMap<String, Long> confirmed_outs = new TreeMap<>(); TreeMap<String, Long> unconfirmed_outs = new TreeMap<>(); // Add all outputs for(SortedTransaction stx : lst) { Transaction tx = stx.tx; int idx=0; for(TransactionOutput tx_out : tx.getOutputs()) { ByteString a = tx_util.getScriptHashForOutput(tx_out); if (target.equals(a)) { String k = tx.getHash().toString() + ":" + idx; if (stx.height > 0) { confirmed_outs.put(k, tx_out.getValue().longValue()); } else { unconfirmed_outs.put(k, tx_out.getValue().longValue()); } } idx++; } } // Remove all inputs for(SortedTransaction stx : lst) { Transaction tx = stx.tx; boolean confirmed = (stx.height > 0); for(TransactionInput tx_in : tx.getInputs()) { if (!tx_in.isCoinBase()) { TransactionOutPoint tx_op = tx_in.getOutpoint(); String k = tx_op.getHash().toString() + ":" + tx_op.getIndex(); if (confirmed) { confirmed_outs.remove(k); unconfirmed_outs.remove(k); } else { unconfirmed_outs.remove(k); if (confirmed_outs.containsKey(k)) { unconfirmed_outs.put(k, -confirmed_outs.get(k)); } } } } } long balance_confirmed=0; long balance_unconfirmed=0; for(long x : confirmed_outs.values()) balance_confirmed+=x; for(long x : unconfirmed_outs.values()) balance_unconfirmed+=x; JSONObject arr = new JSONObject(); //JSONObject b_c = new JSONObject(); //JSONObject b_u = new JSONObject(); arr.put("confirmed", balance_confirmed); arr.put("unconfirmed", balance_unconfirmed); arr.put("transactions", lst.size()); //arr.put(b_c); //arr.put(b_u); reply.put("result", arr); sub.sendReply(reply); } catch(org.json.JSONException e) { throw new RuntimeException(e); } } public Object getScriptHashHistory(ByteString address, boolean include_confirmed, boolean include_mempool) { try { List<SortedTransaction> lst = getTransactionsForScriptHash(address,include_confirmed,include_mempool); //if (lst.size() > 0) { JSONArray arr =new JSONArray(); for(SortedTransaction ts : lst) { JSONObject o = new JSONObject(); o.put("tx_hash", ts.tx.getHash().toString()); if (ts.confirmed) { o.put("height", ts.height); } else { int height = 0; if (jelly.getMemPooler().areSomeInputsPending(ts.tx)) height = -1; o.put("height", height); } if (ts.fee >= 0) { o.put("fee",ts.fee); } arr.put(o); } return arr; } /*else { return JSONObject.NULL; }*/ } catch(org.json.JSONException e) { throw new RuntimeException(e); } } public String getScriptHashChecksum(ByteString address) { synchronized(scripthash_sums) { if (scripthash_sums.containsKey(address)) { return scripthash_sums.get(address); } } String hash = null; List<SortedTransaction> lst = getTransactionsForScriptHash(address, true, true); if (lst.size() > 0) { StringBuilder sb = new StringBuilder(); for(SortedTransaction ts : lst) { sb.append(ts.tx.getHash()); sb.append(':'); if (ts.confirmed) { sb.append(ts.height); } else { int height = 0; if (jelly.getMemPooler().areSomeInputsPending(ts.tx)) height=-1; sb.append(height); } sb.append(':'); } hash = Util.SHA256(sb.toString()); } synchronized(scripthash_sums) { scripthash_sums.put(address,hash); } return hash; } private void printSubscriptionSummary() { int conn_count = jelly.getStratumServer().getConnectionCount(); int block_subscriber_count = 0; int blocknum_subscriber_count = 0; int address_subscriptions = 0; synchronized(block_subscribers) { block_subscriber_count = block_subscribers.size(); } synchronized(blocknum_subscribers) { blocknum_subscriber_count = blocknum_subscribers.size(); } synchronized(scripthash_subscribers) { for(Map.Entry<ByteString, Map<String, Subscriber>> me : scripthash_subscribers.entrySet()) { address_subscriptions += me.getValue().size(); } } jelly.getEventLog().log("USERS Connections: " + conn_count + " Block subs: " + block_subscriber_count + " Block num subs: " + blocknum_subscriber_count + " Address subs: " + address_subscriptions); } public class PruneThread extends Thread { public PruneThread() { setName("ElectrumNotifier/PruneThread"); setDaemon(true); } public void run() { while(true) { try{Thread.sleep(60000);}catch(Exception e){} synchronized(block_subscribers) { pruneMap(block_subscribers); } synchronized(blocknum_subscribers) { pruneMap(blocknum_subscribers); } synchronized(scripthash_subscribers) { for(Map.Entry<ByteString, Map<String, Subscriber>> me : scripthash_subscribers.entrySet()) { pruneMap(me.getValue()); } } printSubscriptionSummary(); } } private void pruneMap(Map<String, Subscriber> input_map) { TreeSet<String> to_delete =new TreeSet<String>(); for(Subscriber sub : input_map.values()) { if (!sub.isOpen()) { to_delete.add(sub.getId()); } } for(String id : to_delete) { input_map.remove(id); } } } public List<SortedTransaction> getTransactionsForScriptHash(ByteString scripthash, boolean include_confirmed, boolean include_mempool) { TreeSet<SortedTransaction> set = new TreeSet<SortedTransaction>(); if (include_confirmed) { Set<Sha256Hash> tx_list = jelly.getDB().getScriptHashToTxSet(scripthash); if (tx_list != null) { for(Sha256Hash tx_hash : tx_list) { SortedTransaction stx = new SortedTransaction(tx_hash, false); if (stx.isValid()) { set.add(stx); } } } } if (include_mempool) { Set<Sha256Hash> tx_mem_list = jelly.getMemPooler().getTxForScriptHash(scripthash); for(Sha256Hash tx_hash : tx_mem_list) { SortedTransaction stx = new SortedTransaction(tx_hash, true); if (stx.isValid()) { set.add(stx); } } } ArrayList<SortedTransaction> out = new ArrayList<SortedTransaction>(); for(SortedTransaction s : set) { out.add(s); } return out; } public class Subscriber { private StratumConnection conn; private Object request_id; public Subscriber(StratumConnection conn, Object request_id) { this.conn = conn; this.request_id = request_id; } public JSONObject startReply() throws org.json.JSONException { JSONObject reply = new JSONObject(); reply.put("id", request_id); reply.put("jsonrpc", "2.0"); return reply; } public void sendReply(JSONObject o) { conn.sendMessage(o); } public boolean isOpen() { return conn.isOpen(); } public String getId() { return conn.getId(); } } public class SortedTransaction implements Comparable<SortedTransaction> { SerializedTransaction s_tx; Transaction tx; boolean confirmed; boolean mempool; int height; long fee=-1; public SortedTransaction(Sha256Hash tx_hash, boolean mempool) { this.mempool = mempool; this.s_tx = jelly.getDB().getTransaction(tx_hash); if (s_tx==null) return; this.tx = s_tx.getTx(jelly.getNetworkParameters()); height = tx_util.getTXBlockHeight(tx, jelly.getBlockChainCache(), jelly.getBitcoinRPC()); if (height >= 0) confirmed=true; if (tx!=null) { if (tx.getFee() != null) { this.fee = tx.getFee().getValue(); } } } public int getEffectiveHeight() { if (confirmed) return height; return Integer.MAX_VALUE; } public int compareTo(SortedTransaction o) { if (getEffectiveHeight() < o.getEffectiveHeight()) return -1; if (getEffectiveHeight() > o.getEffectiveHeight()) return 1; return tx.getHash().toString().compareTo(o.tx.getHash().toString()); } public boolean isValid() { if (s_tx ==null) return false; if (confirmed) return true; if (mempool) return true; // These days, it must be from a block or from the mempool // so almost certainly valid /*if (s_tx.getSavedTime() + 86400L * 1000L < System.currentTimeMillis()) return false; // For unconfirmed transactions, make sure all the inputs // are known for(TransactionInput tx_in : s_tx.getTx(jelly.getNetworkParameters()).getInputs()) { TransactionOutPoint op = tx_in.getOutpoint(); Sha256Hash tx_in_h = op.getHash(); SerializedTransaction s_in_tx = jelly.getDB().getTransaction(tx_in_h); if (s_in_tx == null) return false; }*/ return false; } } }