package jelectrum;
import java.util.Collection;
import java.util.LinkedList;
import java.security.MessageDigest;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Set;
import java.util.List;
import java.util.StringTokenizer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.nio.ByteBuffer;
import java.util.Scanner;
import java.net.URL;
import org.bitcoinj.core.Block;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.ScriptException;
import org.bitcoinj.core.Address;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptChunk;
import java.io.FileInputStream;
import java.util.Scanner;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.DataInputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.codec.binary.Hex;
import java.text.DecimalFormat;
import java.util.Random;
import org.junit.Assert;
import com.google.protobuf.ByteString;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import static org.bitcoinj.script.ScriptOpCodes.*;
/**
* Blocks have to be loaded in order
*
* @todo - Add shutdown hook to avoid exit during flush
*
*/
public class UtxoTrieMgr
{
public static final int UTXO_WORKER_THREADS = 12;
// A key is 20 bytes of public key, 32 bytes of transaction id and 4 bytes of output index
public static final int ADDR_SPACE = 56;
public static boolean DEBUG=false;
private NetworkParameters params;
private Jelectrum jelly;
private TXUtil tx_util;
private StatData get_block_stat=new StatData();
private StatData add_block_stat=new StatData();
private StatData get_hash_stat=new StatData();
// Maps a partial key prefix to a tree node
// The tree node has children, which are other prefixes or full keys
protected TreeMap<String, UtxoTrieNode> node_map = new TreeMap<String, UtxoTrieNode>();
protected Map<String, UtxoTrieNode> db_map;
protected Sha256Hash last_flush_block_hash;
protected Sha256Hash last_added_block_hash;
protected Object block_notify= new Object();
protected Object block_done_notify = new Object();
//Not to be trusted, only used for logging
protected int block_height;
protected volatile boolean caught_up=false;
protected static PrintStream debug_out;
private boolean enabled=true;
private LRUCache<Sha256Hash, Sha256Hash> root_hash_cache = new LRUCache<>(256);
private final Semaphore cache_sem = new Semaphore(0);
private ThreadPoolExecutor worker_exec;
public UtxoTrieMgr(Jelectrum jelly)
throws java.io.FileNotFoundException
{
this.jelly = jelly;
this.params = jelly.getNetworkParameters();
tx_util = new TXUtil(jelly.getDB(), params);
if (jelly.getConfig().getBoolean("utxo_disabled"))
{
enabled=false;
return;
}
worker_exec = new ThreadPoolExecutor(
UTXO_WORKER_THREADS,
UTXO_WORKER_THREADS,
2, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(),
new DaemonThreadFactory());
db_map = jelly.getDB().getUtxoTrieMap();
if (jelly.getConfig().isSet("utxo_reset") && jelly.getConfig().getBoolean("utxo_reset"))
{
jelly.getEventLog().alarm("UTXO reset");
resetEverything();
}
if (DEBUG)
{
debug_out = new PrintStream(new FileOutputStream("utxo-debug.log"));
}
}
public void setTxUtil(TXUtil tx_util)
{
this.tx_util = tx_util;
}
public boolean isUpToDate()
{
return caught_up;
}
private boolean started=false;
public void start()
{
if (!enabled) return;
if (started) return;
started=true;
new UtxoMgrThread().start();
new UtxoCheckThread().start();
}
public void resetEverything()
{
node_map.clear();
putSaveSet("", new UtxoTrieNode(""));
last_flush_block_hash = params.getGenesisBlock().getHash();
last_added_block_hash = params.getGenesisBlock().getHash();
flush();
}
/**
* Just public for testing, don't call
*/
public void flush()
{
DecimalFormat df = new DecimalFormat("0.000");
//get_block_stat.print("get_block", df);
//add_block_stat.print("add_block", df);
//get_hash_stat.print("get_hash", df);
jelly.getEventLog().alarm("UTXO Flushing: " + node_map.size() + " height: " + block_height);
saveState(new UtxoStatus(last_added_block_hash, last_flush_block_hash));
db_map.putAll(node_map.descendingMap());
node_map.clear();
saveState(new UtxoStatus(last_added_block_hash));
last_flush_block_hash = last_added_block_hash;
jelly.getEventLog().alarm("UTXO Flush complete");
}
public void saveState(UtxoStatus status)
{
jelly.getDB().getSpecialObjectMap().put("utxo_trie_mgr_state", status);
}
public synchronized void printTree(PrintStream out)
{
out.println("UTXO TREE:");
getByKey("").printTree(out, 1, this);
}
public synchronized UtxoStatus getUtxoState()
{
if (!enabled) return null;
try
{
Object o = jelly.getDB().getSpecialObjectMap().get("utxo_trie_mgr_state");
if (o != null)
{
return (UtxoStatus)o;
}
}
catch(Throwable t)
{
t.printStackTrace();
}
jelly.getEventLog().alarm("Problem loading UTXO status, starting fresh");
resetEverything();
return getUtxoState();
}
public synchronized void addBlock(Block b)
{
long t1 = System.nanoTime();
LinkedList<String> keys_to_add = new LinkedList<String>();
LinkedList<String> keys_to_remove = new LinkedList<String>();
for(Transaction tx : b.getTransactions())
{
long t2 = System.nanoTime();
addTransactionKeys(tx, keys_to_add, keys_to_remove);
TimeRecord.record(t2, "utxo_get_tx_keys");
}
LinkedList<String> all_keys = new LinkedList<String>();
all_keys.addAll(keys_to_add);
all_keys.addAll(keys_to_remove);
final UtxoTrieMgr mgr = this;
long t1_cache = System.nanoTime();
for(final String key : all_keys)
{
worker_exec.execute(
new Runnable()
{
public void run()
{
try
{
long t2 = System.nanoTime();
getByKey("").cacheNodes(key, mgr);
TimeRecord.record(t2, "utxo_cache_nodes");
}
catch(Throwable t)
{
t.printStackTrace();
}
finally
{
cache_sem.release(1);
}
}
}
);
}
try
{
cache_sem.acquire(all_keys.size());
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
TimeRecord.record(t1_cache, "utxo_cache_walltime");
for(String key : keys_to_add)
{
long t2 = System.nanoTime();
getByKey("").addHash(key, this);
TimeRecord.record(t2, "utxo_add_hash");
}
for(String key : keys_to_remove)
{
long t2 = System.nanoTime();
getByKey("").removeHash(key, this);
TimeRecord.record(t2, "utxo_remove_hash");
}
long t2 = System.nanoTime();
last_added_block_hash = b.getHash();
TimeRecord.record(t2, "utxo_gethash");
TimeRecord.record(t1, "utxo_add_block");
}
// They see me rollin, they hating
public synchronized void rollbackBlock(Block b)
{
LinkedList<Transaction> back_list = new LinkedList<Transaction>();
for(Transaction tx : b.getTransactions())
{
back_list.addFirst(tx);
}
for(Transaction tx : back_list)
{
rollTransaction(tx);
}
}
public synchronized void dumpDB(OutputStream out)
throws java.io.IOException
{
UtxoStatus status = getUtxoState();
if (!status.isConsistent())
{
throw new RuntimeException("UTXO status inconsistent - unable to dump db");
}
System.out.println("Dumping DB at block: " + status.getBlockHash());
//write header
out.write(status.getBlockHash().getBytes());
getByKey("").dumpDB(out, this);
//write end marker
byte[] sz = new byte[4];
out.write(sz);
out.flush();
}
public synchronized void loadDB(InputStream in)
throws java.io.IOException
{
node_map.clear();
last_added_block_hash = null;
last_flush_block_hash = null;
byte[] hash_data = new byte[32];
DataInputStream d_in = new DataInputStream(in);
d_in.readFully(hash_data);
Sha256Hash read_hash = new Sha256Hash(hash_data);
System.out.println("Reading block: " + read_hash);
int node_count=0;
TreeMap<String, UtxoTrieNode> save_map = new TreeMap<>();
StatData size_info = new StatData();
while(true)
{
int size = d_in.readInt();
if (size == 0) break;
byte[] data = new byte[size];
d_in.readFully(data);
UtxoTrieNode node = new UtxoTrieNode(ByteString.copyFrom(data));
size_info.addDataPoint(size);
save_map.put(node.getPrefix(), node);
node_count++;
if (node_count % 1000 == 0)
{
db_map.putAll(save_map);
save_map.clear();
System.out.print('.');
}
}
db_map.putAll(save_map);
save_map.clear();
System.out.print('.');
System.out.println();
System.out.println("Saved " + node_count + " nodes");
saveState(new UtxoStatus(read_hash));
System.out.println("UTXO root hash: " + getRootHash(null));
size_info.print("sizes", new DecimalFormat("0.0"));
}
public void putSaveSet(String prefix, UtxoTrieNode node)
{
synchronized(node_map)
{
node_map.put(prefix, node);
}
}
public UtxoTrieNode getByKey(String prefix)
{
UtxoTrieNode n = null;
synchronized(node_map)
{
n = node_map.get(prefix);
if (n != null) return n;
}
n = db_map.get(prefix);
if (n != null) return n;
jelly.getEventLog().alarm("UTXO node missing: ." + prefix + ".");
return n;
}
private void checkUtxoHash(int height, Sha256Hash block, Sha256Hash utxo_hash)
{
UtxoCheckEntry check_entry = new UtxoCheckEntry(height, block, utxo_hash);
check_queue.offer(check_entry);
}
private void addTransactionKeys(Transaction tx, LinkedList<String> keys_to_add, LinkedList<String> keys_to_remove)
{
int idx=0;
for(TransactionOutput tx_out : tx.getOutputs())
{
long t1 = System.nanoTime();
String key = getKeyForOutput(tx_out, idx);
TimeRecord.record(t1, "utxo_get_key_for_output");
if (key != null)
{
keys_to_add.add(key);
}
/*if (DEBUG) debug_out.println("Adding key: " + key);
if (key != null)
{
long t2 = System.nanoTime();
getByKey("").addHash(key, this);
TimeRecord.record(t2, "utxo_addhash");
}*/
idx++;
}
for(TransactionInput tx_in : tx.getInputs())
{
if (!tx_in.isCoinBase())
{
long t1 = System.nanoTime();
String key = getKeyForInput(tx_in);
TimeRecord.record(t1, "utxo_get_key_for_input");
if (key != null)
{
keys_to_remove.add(key);
/*long t2 = System.nanoTime();
getByKey("").removeHash(key, this);
TimeRecord.record(t2, "utxo_removehash");*/
}
}
}
}
private void rollTransaction(Transaction tx)
{
int idx=0;
for(TransactionOutput tx_out : tx.getOutputs())
{
String key = getKeyForOutput(tx_out, idx);
if (DEBUG) System.out.println("Adding key: " + key);
if (key != null)
{
getByKey("").removeHash(key, this);
}
idx++;
}
for(TransactionInput tx_in : tx.getInputs())
{
if (!tx_in.isCoinBase())
{
String key = getKeyForInput(tx_in);
if (key != null)
{
getByKey("").addHash(key, this);
}
}
}
}
public synchronized Collection<TransactionOutPoint> getUnspentForAddress(Address a)
{
String prefix = getPrefixForAddress(a);
Collection<String> keys = getByKey("").getKeysByPrefix(prefix, this);
LinkedList<TransactionOutPoint> outs = new LinkedList<TransactionOutPoint>();
try
{
for(String key : keys)
{
byte[] key_data=Hex.decodeHex(key.toCharArray());
ByteBuffer bb = ByteBuffer.wrap(key_data);
bb.order(java.nio.ByteOrder.LITTLE_ENDIAN);
bb.position(20);
byte[] tx_data = new byte[32];
bb.get(tx_data);
int idx=bb.getInt();
Sha256Hash tx_id = new Sha256Hash(tx_data);
TransactionOutPoint o = new TransactionOutPoint(jelly.getNetworkParameters(), idx, tx_id);
outs.add(o);
}
}
catch(org.apache.commons.codec.DecoderException e)
{
throw new RuntimeException(e);
}
return outs;
}
public synchronized Sha256Hash getRootHash(Sha256Hash interest_block)
{
if (!enabled)
{
byte[] b = new byte[32];
return new Sha256Hash(b);
}
if (interest_block != null)
{
synchronized(root_hash_cache)
{
if (root_hash_cache.containsKey(interest_block))
{
return root_hash_cache.get(interest_block);
}
}
}
if ((interest_block == null) || (last_added_block_hash == null) || (interest_block.equals(last_added_block_hash)))
{
Sha256Hash root = getByKey("").getHash("", this);
if (DEBUG) debug_out.println("Root is now: " + root);
return root;
}
byte[] b = new byte[32];
return new Sha256Hash(b);
}
public static int commonLength(String a, String b)
{
int max = Math.min(a.length(), b.length());
int same = 0;
for(int i=0; i<max; i++)
{
if (a.charAt(i) == b.charAt(i)) same++;
else break;
}
if (same % 2 == 1) same--;
return same;
}
public static Sha256Hash hashThings(String skip, Collection<Sha256Hash> hash_list)
{
try
{
if (DEBUG) debug_out.print("hash(");
MessageDigest md = MessageDigest.getInstance("SHA-256");
if ((skip != null) && (skip.length() > 0))
{
byte[] skipb = Hex.decodeHex(skip.toCharArray());
md.update(skipb);
if (DEBUG)
{
debug_out.print("skip:");
debug_out.print(skip);
debug_out.print(' ');
}
}
for(Sha256Hash h : hash_list)
{
md.update(h.getBytes());
if (DEBUG)
{
debug_out.print(h);
debug_out.print(" ");
}
}
if (DEBUG) debug_out.print(") - ");
byte[] pass = md.digest();
md = MessageDigest.getInstance("SHA-256");
md.update(pass);
Sha256Hash out = new Sha256Hash(md.digest());
if (DEBUG) debug_out.println(out);
return out;
}
catch(java.security.NoSuchAlgorithmException e)
{
throw new RuntimeException(e);
}
catch(org.apache.commons.codec.DecoderException e)
{
throw new RuntimeException(e);
}
}
public void notifyBlock(boolean wait_for_it, Sha256Hash wait_for_block)
{
synchronized(block_notify)
{
block_notify.notifyAll();
}
if (wait_for_it)
{
long end_wait = System.currentTimeMillis() + 15000;
while(end_wait > System.currentTimeMillis())
{
long wait_tm = end_wait - System.currentTimeMillis();
if (wait_tm > 0)
{
try
{
synchronized(root_hash_cache)
{
if (root_hash_cache.containsKey(wait_for_block)) return;
}
synchronized(block_done_notify)
{
block_done_notify.wait(wait_tm);
}
}
catch(Throwable t){}
}
}
}
}
public static Sha256Hash getHashFromKey(String key)
{
String hash = key.substring(40, 40+64);
return new Sha256Hash(hash);
}
public static String getKey(byte[] publicKey, Sha256Hash tx_id, int idx)
{
String addr_part = Hex.encodeHexString(publicKey);
ByteBuffer bb = ByteBuffer.allocate(4);
bb.order(java.nio.ByteOrder.LITTLE_ENDIAN);
bb.putInt(idx);
String idx_str = Hex.encodeHexString(bb.array());
String key = addr_part + tx_id + idx_str;
return key;
}
public String getKeyForInput(TransactionInput in)
{
if (in.isCoinBase()) return null;
try
{
byte[] public_key=null;
Address a = in.getFromAddress();
public_key = a.getHash160();
return getKey(public_key, in.getOutpoint().getHash(), (int)in.getOutpoint().getIndex());
}
catch(ScriptException e)
{
//Lets try this the other way
try
{
TransactionOutPoint out_p = in.getOutpoint();
Transaction src_tx = tx_util.getTransaction(out_p.getHash());
TransactionOutput out = src_tx.getOutput((int)out_p.getIndex());
return getKeyForOutput(out, (int)out_p.getIndex());
}
catch(ScriptException e2)
{
return null;
}
}
}
public static String getPrefixForAddress(Address a)
{
byte[] public_key=a.getHash160();
return Hex.encodeHexString(public_key);
}
public static byte[] getPublicKeyForTxOut(TransactionOutput out, NetworkParameters params)
{
byte[] public_key=null;
Script script = null;
try
{
script = out.getScriptPubKey();
if (script.isSentToRawPubKey())
{
byte[] key = out.getScriptPubKey().getPubKey();
byte[] address_bytes = org.bitcoinj.core.Utils.sha256hash160(key);
public_key = address_bytes;
}
else
{
Address a = script.getToAddress(params);
public_key = a.getHash160();
}
}
catch(ScriptException e)
{
public_key = figureOutStrange(script, out, params);
}
return public_key;
}
public static byte[] figureOutStrange(Script script, TransactionOutput out, NetworkParameters params)
{
if (script == null) return null;
//org.bitcoinj.core.Utils.sha256hash160
List<ScriptChunk> chunks = script.getChunks();
/*System.out.println("STRANGE: " + out.getParentTransaction().getHash() + " - has strange chunks " + chunks.size());
for(int i =0; i<chunks.size(); i++)
{
System.out.print("Chunk " + i + " ");
System.out.print(Hex.encodeHex(chunks.get(i).data));
System.out.println(" " + getOpCodeName(chunks.get(i).data[0]));
}*/
//Remember, java bytes are signed because hate
//System.out.println(out);
//System.out.println(script);
if (chunks.size() == 6)
{
if (chunks.get(0).equalsOpCode(OP_DUP))
if (chunks.get(1).equalsOpCode(OP_HASH160))
if (chunks.get(3).equalsOpCode(OP_EQUALVERIFY))
if (chunks.get(4).equalsOpCode(OP_CHECKSIG))
if (chunks.get(5).equalsOpCode(OP_NOP))
if (chunks.get(2).isPushData())
if (chunks.get(2).data.length == 20)
{
return chunks.get(2).data;
}
}
/*if ((chunks.size() == 6) && (chunks.get(2).data.length == 20))
{
for(int i=0; i<6; i++)
{
if (chunks.get(i) == null) return null;
if (chunks.get(i).data == null) return null;
if (i != 2)
{
if (chunks.get(i).data.length != 1) return null;
}
}
if (chunks.get(0).data[0] == OP_DUP)
if ((int)(chunks.get(1).data[0] & 0xFF) == OP_HASH160)
if ((int)(chunks.get(3).data[0] & 0xFF) == OP_EQUALVERIFY)
if ((int)(chunks.get(4).data[0] & 0xFF) == OP_CHECKSIG)
if (chunks.get(5).data[0] == OP_NOP)
{
return chunks.get(2).data;
}
}*/
return null;
}
public String getKeyForOutput(TransactionOutput out, int idx)
{
byte[] public_key = getPublicKeyForTxOut(out, params);
if (public_key == null) return null;
return getKey(public_key, out.getParentTransaction().getHash(), idx);
}
public class UtxoMgrThread extends Thread
{
private BlockChainCache block_chain_cache;
public UtxoMgrThread()
{
setName("UtxoMgrThread");
setDaemon(true);
block_chain_cache = jelly.getBlockChainCache();
}
public void run()
{
recover();
while(true)
{
while(catchup()){}
waitForBlocks();
}
}
private void recover()
{
UtxoStatus status = getUtxoState();
if (!status.isConsistent())
{
Sha256Hash start = status.getPrevBlockHash();
Sha256Hash end = status.getBlockHash();
last_flush_block_hash = start;
jelly.getEventLog().alarm("UTXO inconsistent, attempting recovery from " + start + " to " + end);
LinkedList<Sha256Hash> recover_block_list = new LinkedList<Sha256Hash>();
Sha256Hash ptr = end;
while(!ptr.equals(start))
{
recover_block_list.addFirst(ptr);
//System.out.println("Getting prev of : " + ptr);
ptr = jelly.getDB().getBlockStoreMap().get(ptr).getHeader().getPrevBlockHash();
}
recover_block_list.addFirst(start);
jelly.getEventLog().alarm("UTXO attempting recovery of " + recover_block_list.size() + " blocks");
for(Sha256Hash blk_hash : recover_block_list)
{
Block b = jelly.getDB().getBlockMap().get(blk_hash).getBlock(jelly.getNetworkParameters());
addBlock(b);
}
flush();
}
else
{
last_flush_block_hash = status.getBlockHash();
last_added_block_hash = status.getBlockHash();
}
}
int added_since_flush = 0;
long last_flush = 0;
private boolean catchup()
{
while(!block_chain_cache.isBlockInMainChain(last_added_block_hash))
{
rollback();
}
int curr_height = jelly.getDB().getBlockStoreMap().get(last_added_block_hash).getHeight();
int head_height = jelly.getDB().getBlockStoreMap().get(
jelly.getBlockChainCache().getHead()).getHeight();
boolean near_caught_up=caught_up;
for(int i=curr_height+1; i<=head_height; i++)
{
while(jelly.getSpaceLimited())
{
try
{
Thread.sleep(5000);
}
catch(Throwable t){}
}
Sha256Hash block_hash = jelly.getBlockChainCache().getBlockHashAtHeight(i);
long t1=System.currentTimeMillis();
SerializedBlock sb = jelly.getDB().getBlockMap().get(block_hash);
if (sb == null)
{
try{Thread.sleep(250); return true;}catch(Throwable t){}
}
caught_up=false;
Block b = sb.getBlock(params);
long t2=System.currentTimeMillis();
get_block_stat.addDataPoint(t2-t1);
if (b.getPrevBlockHash().equals(last_added_block_hash))
{
t1=System.currentTimeMillis();
addBlock(b);
t2=System.currentTimeMillis();
add_block_stat.addDataPoint(t2-t1);
Sha256Hash root_hash = getRootHash(null);
synchronized(root_hash_cache)
{
root_hash_cache.put(block_hash, root_hash);
}
block_height=i;
checkUtxoHash(i, block_hash, root_hash);
if ((near_caught_up) && (jelly.isUpToDate()))
{
jelly.getEventLog().alarm("UTXO added block " + i + " - " + root_hash);
}
else
{
jelly.getEventLog().log("UTXO added block " + i + " - " + root_hash);
}
added_since_flush++;
}
else
{
return true;
}
int flush_mod = 1000;
//After the blocks get bigger, flush more often
if (block_height > 220000) flush_mod = 100;
if (block_height > 320000) flush_mod = 10;
if (i % flush_mod == 0)
{
flush();
added_since_flush=0;
last_flush = System.currentTimeMillis();
}
}
synchronized(block_done_notify)
{
block_done_notify.notifyAll();
}
if ((added_since_flush > 0) && (last_flush +15000L < System.currentTimeMillis()))
{
flush();
added_since_flush=0;
last_flush = System.currentTimeMillis();
}
return false;
}
private void rollback()
{
jelly.getEventLog().alarm("UTXO rolling back " + last_added_block_hash);
Sha256Hash prev = jelly.getDB().getBlockStoreMap().get(last_added_block_hash).getHeader().getPrevBlockHash();
last_flush_block_hash = prev;
Block b = jelly.getDB().getBlockMap().get(last_added_block_hash).getBlock(jelly.getNetworkParameters());
rollbackBlock(b);
//Setting hashes such that it looks like we are doing prev -> last_added_block_hash
//That way on recovery we will re-add the block and then roll back again
flush();
last_added_block_hash = prev;
}
private void waitForBlocks()
{
caught_up=true;
synchronized(block_done_notify)
{
block_done_notify.notifyAll();
}
synchronized(block_notify)
{
try
{
block_notify.wait(10000);
}
catch(InterruptedException e){}
}
}
}
private LinkedBlockingQueue<UtxoCheckEntry> check_queue = new LinkedBlockingQueue<UtxoCheckEntry>(4);
public class UtxoCheckEntry
{
int height;
Sha256Hash block;
Sha256Hash utxo_root;
public UtxoCheckEntry(int height, Sha256Hash block, Sha256Hash utxo_root)
{
this.height = height;
this.block = block;
this.utxo_root = utxo_root;
}
}
public class UtxoCheckThread extends Thread
{
String client_name;
public UtxoCheckThread()
{
setName("UtxoCheckThread");
setDaemon(true);
client_name = "j_" + StratumConnection.JELECTRUM_VERSION + "_";
if (jelly.getConfig().isSet("irc_advertise_host"))
{
client_name += jelly.getConfig().get("irc_advertise_host");
}
else
{
client_name += new java.util.Random().nextInt();
}
//client_name = "check_file";
}
public void run()
{
while(true)
{
try
{
UtxoCheckEntry e = check_queue.take();
String url = "https://jelectrum-1022.appspot.com/utxo?"
+ "block=" + e.block.toString()
+ "&utxo=" + e.utxo_root.toString()
+ "&client="+ client_name;
URL u = new URL(url);
Scanner scan =new Scanner(u.openStream());
String line = scan.nextLine();
scan.close();
StringTokenizer stok = new StringTokenizer(line, ",");
String root_str = stok.nextToken();
if (!root_str.equals("undetermined"))
{
Sha256Hash concur_root = new Sha256Hash(root_str);
int matching = Integer.parseInt(stok.nextToken());
int total = Integer.parseInt(stok.nextToken());
if (!concur_root.equals(e.utxo_root))
{
jelly.getEventLog().alarm("UTXO check mismatch at " + e.height + " - me:" + e.utxo_root + " others:" + concur_root + " agreement " + + matching + " of " + total);
}
else
{
jelly.getEventLog().log("UTXO check at " + e.height + " - " + concur_root + " - matching " + matching + " of " + total);
}
}
else
{
jelly.getEventLog().log("UTXO check at " + e.height + " - " + root_str);
}
}
catch(Throwable t)
{
jelly.getEventLog().alarm("UtxoCheckThread: " + t);
t.printStackTrace();
}
}
}
}
public static void main(String args[]) throws Exception
{
String config_path = args[0];
Jelectrum jelly = new Jelectrum(new Config(config_path));
int block_number = Integer.parseInt(args[1]);
Sha256Hash block_hash = jelly.getBlockChainCache().getBlockHashAtHeight(block_number);
System.out.println("Block hash: " + block_hash);
Block b = jelly.getDB().getBlockMap().get(block_hash).getBlock(jelly.getNetworkParameters());
System.out.println("Inspecting " + block_number + " - " + block_hash);
int tx_count =0;
int out_count =0;
for(Transaction tx : b.getTransactions())
{
int idx=0;
for(TransactionOutput tx_out : tx.getOutputs())
{
byte[] pub_key_bytes=getPublicKeyForTxOut(tx_out, jelly.getNetworkParameters());
String public_key = null;
if (pub_key_bytes != null) public_key = Hex.encodeHexString(pub_key_bytes);
else public_key = "None";
String script_bytes = Hex.encodeHexString(tx_out.getScriptBytes());
String[] cmd=new String[3];
cmd[0]="python";
cmd[1]=jelly.getConfig().get("utxo_check_tool");
cmd[2]=script_bytes;
//System.out.println(script_bytes);
Process p = Runtime.getRuntime().exec(cmd);
Scanner scan = new Scanner(p.getInputStream());
String ele_key = scan.nextLine();
if (!ele_key.equals(public_key))
{
System.out.println("Mismatch on " + tx_out.getParentTransaction().getHash() + ":" + idx);
System.out.println(" Script: " + script_bytes);
System.out.println(" Jelectrum: " + public_key);
System.out.println(" Electrum: " + ele_key);
}
out_count++;
idx++;
}
tx_count++;
}
System.out.println("TX Count: " + tx_count);
System.out.println("Out Count: " + out_count);
}
}