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.HashMap; import java.util.HashSet; import java.util.Map; import java.nio.ByteBuffer; 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.script.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.ByteArrayInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.PrintStream; import com.google.protobuf.ByteString; import org.apache.commons.codec.binary.Hex; import java.text.DecimalFormat; import java.util.Random; import lobstack.SerialUtil; import org.junit.Assert; import java.io.OutputStream; /** * This class stores a node in the UTXO trie structure */ public class UtxoTrieNode implements java.io.Serializable { /** * This is the key prefix of this node. * It is in hex, 20 bytes (40 characters) of address public key * then 32 bytes (64 characters) of transaction hash * then 4 bytes of transaction output offset (integer) * but none of those code cares about that. It is just a string * that could be up to 56*2 characters long */ private String prefix; /** * This is a map children of this node. * For space efficency, the string keys in this map * are only the part after this prefix. * So if this node is "12" and the child prefix is "12af" then this map will just have "af". * * The hash value is the hash of the subtree or null if it needs to be recalculated. * this way, when getHash() is called we know which children we need to recurse into */ private TreeMap<String, Sha256Hash> springs; /** * For some serialization methods we want a special null value * so this value, which is a hash of "null" is our chosen null value. */ private static Sha256Hash hash_null = new Sha256Hash("74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b"); public static final long serialVersionUID = 2675325841660230241L; public UtxoTrieNode(String prefix) { springs = new TreeMap<String, Sha256Hash>(); this.prefix = prefix; } /** * Deserialize from a byte string */ public UtxoTrieNode(ByteString bs) { try { DataInputStream din=new DataInputStream(bs.newInput()); long ver = din.readLong(); Assert.assertEquals(serialVersionUID, ver); prefix = SerialUtil.readString(din); int count = din.readInt(); springs = new TreeMap<String, Sha256Hash>(); for(int i=0; i<count; i++) { byte hash_bytes[]=new byte[32]; String sub = SerialUtil.readString(din); din.readFully(hash_bytes); Sha256Hash hash = new Sha256Hash(hash_bytes); if (hash.equals(hash_null)) { hash=null; springs.put(sub, null); } else { springs.put(sub, hash); } } } catch(java.io.IOException e) { throw new RuntimeException(e); } } public String getPrefix() { return prefix; } public Map<String, Sha256Hash> getSprings() { return springs; } public void dumpDB(OutputStream out, UtxoTrieMgr mgr) throws java.io.IOException { ByteString self = serialize(); byte[] sz = new byte[4]; ByteBuffer bb = ByteBuffer.wrap(sz); bb.putInt(self.size()); out.write(sz); out.write(self.toByteArray()); for(Map.Entry<String, Sha256Hash> me : springs.entrySet()) { String sub = prefix + me.getKey(); if (sub.length() < UtxoTrieMgr.ADDR_SPACE*2) { mgr.getByKey(sub).dumpDB(out, mgr); } } } /** * Serialize to a byte string */ public ByteString serialize() { try { ByteArrayOutputStream b_out = new ByteArrayOutputStream(); DataOutputStream d_out = new DataOutputStream(b_out); d_out.writeLong(serialVersionUID); SerialUtil.writeString(d_out, prefix); d_out.writeInt(springs.size()); for(Map.Entry<String, Sha256Hash> me : springs.entrySet()) { SerialUtil.writeString(d_out, me.getKey()); Sha256Hash hash = me.getValue(); if (hash == null) hash = hash_null; d_out.write(hash.getBytes()); } d_out.flush(); return ByteString.copyFrom(b_out.toByteArray()); } catch(java.io.IOException e) { throw new RuntimeException(e); } } /** * This should be called add child and mark as needing to be rehashed */ public void addSpring(String s, UtxoTrieMgr mgr) { // Mark that we don't have the hash springs.put(s, null); // Mark this node as changes to it needs to be saved on next flush to db mgr.putSaveSet(prefix, this); } /** * Return an ordered set of keys matching the given 'start' prefix */ public Collection<String> getKeysByPrefix(String start, UtxoTrieMgr mgr) { LinkedList<String> lst = new LinkedList<>(); for(String sub : springs.keySet()) { String name = prefix+sub; if (name.startsWith(start) || start.startsWith(name)) { //If it is the expected total length, then it is just a leaf node //and we can just put it on the list if (name.length() == UtxoTrieMgr.ADDR_SPACE*2) { lst.add(name); } else { UtxoTrieNode n = mgr.getByKey(name); if (n == null) System.out.println("Missing: " + name + " from " + prefix); lst.addAll(n.getKeysByPrefix(start, mgr)); } } } return lst; } public void cacheNodes(String key, UtxoTrieMgr mgr) { mgr.putSaveSet(prefix, this); for(String sub : springs.keySet()) { String name = prefix+sub; if (key.startsWith(name)) { if (name.length() < UtxoTrieMgr.ADDR_SPACE*2) { UtxoTrieNode n = mgr.getByKey(name); n.cacheNodes(key, mgr); } } } } public void addHash(String key, UtxoTrieMgr mgr) { mgr.putSaveSet(prefix, this); //If we are here, we are assuming that the start of 'key' and //my 'prefix' are already matching and that 'key' is longer. //So get just the part of 'key' that is past 'prefix'. //Example: // If this node is "abc7" and we are adding "abc7f8f8fe" // Then next will be "f8f8fe" Assert.assertTrue(key.startsWith(prefix)); String next = key.substring(prefix.length()); for(String sub : springs.keySet()) { //If the new key simply fits into a sub node we have already, send it there if (next.startsWith(sub)) { String name = prefix+sub; if (name.length() < UtxoTrieMgr.ADDR_SPACE*2) { //if statement avoids the strange txid d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599 issue //If the sub name is the entire key space, then we are adding a duplicate transaction and //are just going to leave that alone //Otherwise, add it to the node below us UtxoTrieNode n = mgr.getByKey(prefix+sub); if (n == null) System.out.println("Missing: " + prefix + sub + " from " + prefix); n.addHash(key, mgr); springs.put(sub, null); } return; } } for(String sub : springs.keySet()) { int common = UtxoTrieMgr.commonLength(sub, next); //If the new entry has a common start with a previous entry //Make a new sub node that will contain them both if (common >= 2) { String common_str = sub.substring(0, common); springs.remove(sub); springs.put(common_str, null); UtxoTrieNode n = new UtxoTrieNode(prefix + common_str); n.addHash(key, mgr); n.addSpring(sub.substring(common), mgr); return; } } //If it doesn't go into a sub node //and it has no common node, just save it directly to this node springs.put(next, null); } public String removeHash(String key, UtxoTrieMgr mgr) { mgr.putSaveSet(prefix, this); String rest = key.substring(prefix.length()); if (springs.containsKey(rest)) { springs.remove(rest); } else { for(String sub : springs.keySet()) { String full = prefix + sub; if (rest.startsWith(sub)) { String next_sub = mgr.getByKey(prefix+sub).removeHash(key, mgr); springs.put(sub, null); if (next_sub == null) { springs.remove(sub); } if (next_sub != full) { springs.remove(sub); springs.put(next_sub.substring(prefix.length()),null); } break; } } } if (springs.size() == 0) { //node_map.remove(prefix); return null; } if (springs.size() == 1) { String ret = prefix + springs.firstKey(); springs.put(springs.firstKey(), null); //node_map.remove(prefix); //springs.clear(); //We are not clearing springs in case we get a partial save //and this node is still referenced //in which case, we don't want to lose track of the nodes under this one return ret; } return prefix; } public Sha256Hash getHash(String skip_string, UtxoTrieMgr mgr) { LinkedList<Sha256Hash> lst=new LinkedList<Sha256Hash>(); boolean changed=false; for(String sub : springs.keySet()) { /*if (hash_null.equals(springs.get(sub))) { springs.put(sub, null); }*/ if (springs.get(sub) != null) { //If we have a hash for a child already, just use it lst.add(springs.get(sub)); } else { String sub_skip_str = ""; if (sub.length() > 2) { sub_skip_str = sub.substring(2); } String full_sub = prefix+sub; Sha256Hash h = null; if (full_sub.length() == UtxoTrieMgr.ADDR_SPACE*2) { // If the sub is a leaf, just get the tx hash h = UtxoTrieMgr.getHashFromKey(full_sub); } else { // Otherwise, recurse h = mgr.getByKey(full_sub).getHash(sub_skip_str, mgr); } lst.add(h); // Save any hash we calculate for the child for later use springs.put(sub, h); changed=true; } } if (changed) { mgr.putSaveSet(prefix, this); } Sha256Hash hash = null; if ((lst.size() == 1) && (prefix.length() >= 2)) { // I don't want to talk about it hash = lst.get(0); } else { //Take the skip list and the sub hashes and hash them hash = UtxoTrieMgr.hashThings(skip_string,lst); } return hash; } public void printTree(PrintStream out, int indent, UtxoTrieMgr mgr) { for(int i=0; i<indent; i++) out.print(" "); out.println("Node: ." + prefix +"."); indent++; for(Map.Entry<String, Sha256Hash> me : springs.entrySet()) { String sub = prefix + me.getKey(); for(int i=0; i<indent; i++) out.print(" "); out.println(sub + " - " + me.getValue()); if (sub.length() < UtxoTrieMgr.ADDR_SPACE*2) { mgr.getByKey(sub).printTree(out, indent+1, mgr); } } } }