/* * ThunderNetwork - Server Client Architecture to send Off-Chain Bitcoin Payments * Copyright (C) 2015 Mats Jerratsch <matsjj@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package network.thunder.core.etc; import com.google.gson.Gson; import com.google.gson.internal.LinkedTreeMap; import com.sun.net.httpserver.HttpExchange; import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException; import com.sun.org.apache.xml.internal.security.utils.Base64; import org.bitcoinj.core.*; import org.bitcoinj.core.ECKey.ECDSASignature; import org.bitcoinj.core.Transaction.SigHash; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.DeterministicHierarchy; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.spongycastle.crypto.digests.RIPEMD160Digest; import java.io.*; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * The Class Tools. */ public class Tools { final protected static char[] hexArray = "0123456789abcdef".toCharArray(); final static Map<String, String> scriptMap = new LinkedTreeMap<>(); static { scriptMap.put("HASH160", "A9"); scriptMap.put("DUP", "76"); scriptMap.put("EQUAL", "87"); scriptMap.put("EQUALVERIFY", "88"); scriptMap.put("SWAP", "7C"); scriptMap.put("ADD", "93"); scriptMap.put("NOTIF", "64"); scriptMap.put("ELSE", "67"); scriptMap.put("ENDIF", "68"); scriptMap.put("IF", "63"); scriptMap.put("CLTV", "B1"); scriptMap.put("CSV", "B3"); scriptMap.put("2DROP", "6D"); scriptMap.put("DROP", "75"); scriptMap.put("VERIFY", "69"); scriptMap.put("CHECKSIGVERIFY", "AD"); scriptMap.put("CHECKMULTISIGVERIFY", "AF"); scriptMap.put("CHECKSIG", "AC"); scriptMap.put("CHECKMULTISIG", "AE"); scriptMap.put("OP_0", "00"); scriptMap.put("OP_1", "51"); scriptMap.put("OP_2", "52"); } public static byte[] scriptStringToByte (String script) { for (Map.Entry<String, String> a : scriptMap.entrySet()) { script = script.replaceAll(a.getKey(), a.getValue()); } return Tools.hexStringToByteArray(script); } public static int getRandom(int min, int max) { return new Random().nextInt(max+1-min)+min; } public static String InputStreamToString (InputStream in) { String qry = ""; try { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte buf[] = new byte[4096]; for (int n = in.read(buf); n > 0; n = in.read(buf)) { out.write(buf, 0, n); } qry = new String(out.toByteArray()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } try { return java.net.URLDecoder.decode(qry, "UTF-8"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } return qry; } public static boolean arrayListContainsByteArray (ArrayList<byte[]> arrayList, byte[] bytes) { //TODO: This is a probably slow hack, better way would be to use a helper class, as we can make use of hashCode then.. /* Example wrapper class to have more efficient contains(..) public final class Bytes { private final int hashCode; private final byte[] data; public Bytes(byte[] in) { this.data = in; this.hashCode = Arrays .hashCode(in); } @Override public boolean equals(Object other) {if (other == null) return false; if (!(other instanceof Bytes)) return false; if (( (Bytes) other).hashCode != hashCode) return false; return Arrays.equals(data, ((Bytes) other).data); } @Override public int hashCode() {return hashCode;} @Override public String toString() { ... do something useful here ... }} */ for (byte[] a : arrayList) { if (Arrays.equals(a, bytes)) { return true; } } return false; } /** * Bool to int. * * @param bool the bool * @return the int */ public static int boolToInt (boolean bool) { int a = 0; if (bool) { a = 1; } return a; } public static <T extends Object> T getRandomItemFromList (List<T> list) { System.out.println(list.size()); int randomNumber = new Random().nextInt(list.size()); return list.get(randomNumber); } public static <T> List<T> getRandomSubList (List<T> input, int subsetSize) { Random r = new Random(); int inputSize = input.size(); for (int i = 0; i < subsetSize; i++) { int indexToSwap = i + r.nextInt(inputSize - i); T temp = input.get(i); input.set(i, input.get(indexToSwap)); input.set(indexToSwap, temp); } return input.subList(0, subsetSize); } /** * Byte to string. * * @param array the array * @return the string */ // http://java-performance.info/base64-encoding-and-decoding-performance/ public static String byteToString (byte[] array) { return new String(Base64.encode(array)); } /** * Byte to string58. * * @param array the array * @return the string */ public static String byteToString58 (byte[] array) { return Base58.encode(array); } /** * Bytes to hex. * * @param bytes the bytes * @return the string */ public static String bytesToHex (byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } /** * Calculate server fee. * * @param amount the amount * @return the long */ public static long calculateServerFee (long amount) { long fee = (long) (amount * Constants.SERVER_FEE_PERCENTAGE + Constants.SERVER_FEE_FLAT); return Math.min(Constants.SERVER_FEE_MAX, Math.max(Constants.SERVER_FEE_MIN, fee)); } /** * Calculate server fee reverse. * * @param amount the amount * @return the long */ public static long calculateServerFeeReverse (long amount) { long a = Constants.SERVER_FEE_FLAT; long b = amount + a; long c = (long) ((b / (1 - Constants.SERVER_FEE_PERCENTAGE)) - b); return Math.min(Constants.SERVER_FEE_MAX, Math.max(Constants.SERVER_FEE_MIN, c + a)); } /** * Check server fee. * * @param amountIs the amount is * @param amountShouldBe the amount should be * @return true, if successful */ public static boolean checkServerFee (long amountIs, long amountShouldBe) { long fee = amountShouldBe - amountIs; long feeShouldBe = calculateServerFee(amountShouldBe); return (fee <= feeShouldBe); } /** * Check signature. * * @param transaction the transaction * @param index the index * @param outputToSpend the output to spend * @param key the key * @param signature the signature * @return true, if successful */ public static boolean checkSignature (Transaction transaction, int index, TransactionOutput outputToSpend, ECKey key, byte[] signature) { Sha256Hash hash = transaction.hashForSignature(index, outputToSpend.getScriptBytes(), SigHash.ALL, false); return key.verify(hash, ECDSASignature.decodeFromDER(signature)); } /** * Check transaction fees. * * @param size the size * @param transaction the transaction * @param output the output * @return true, if successful */ public static boolean checkTransactionFees (int size, Transaction transaction, TransactionOutput output) { long in = output.getValue().value; long out = 0; for (TransactionOutput o : transaction.getOutputs()) { out += o.getValue().value; } long diff = in - out; float f = ((float) diff) / size; if (f >= Constants.FEE_PER_BYTE_MIN) { if (f <= Constants.FEE_PER_BYTE_MAX) { return true; } } System.out.println("Fee not correct. Total Fee: " + diff + " Per Byte: " + f + " Size: " + size); return false; } /** * Check transaction lock time. * * @param transaction the transaction * @param locktime the locktime * @return true, if successful */ public static boolean checkTransactionLockTime (Transaction transaction, int locktime) { if (Math.abs(transaction.getLockTime() - locktime) > 5 * 60) { System.out.println("Locktime not correct. Should be: " + locktime + " Is: " + transaction.getLockTime() + " Diff: " + Math.abs(transaction .getLockTime() - locktime)); return false; } if (locktime == 0) { return true; } for (TransactionInput input : transaction.getInputs()) { if (input.getSequenceNumber() == 0) { return true; } } System.out.println("No Sequence Number is 0.."); return false; } /** * Compare hash. * * @param hash1 the hash1 * @param hash2 the hash2 * @return true, if successful */ public static boolean compareHash (Sha256Hash hash1, Sha256Hash hash2) { return hash1.toString().equals(hash2.toString()); } // public static void emailException (Exception e, Message m, Channel c, Payment p, Transaction channelTransaction, Transaction t) throws // AddressException { // String to = "matsjj@gmail.com"; // String from = "exception@thunder.network"; // String host = "localhost"; // Properties properties = System.getProperties(); // properties.setProperty("mail.smtp.host", host); // Session session = Session.getDefaultInstance(properties); /** * Current time. * * @return the int */ public static int currentTime () { return ((int) (System.currentTimeMillis() / 1000)); } public static int currentTimeFlooredToCurrentDay () { int time = currentTime(); int diff = time % 86400; return time - diff; } // try { // MimeMessage message = new MimeMessage(session); // message.setFrom(new InternetAddress(from)); // message.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(to)); /** * Gets the coin value from input. * * @param peer the peer * @param input the input * @return the coin value from input * @throws InterruptedException the interrupted exception * @throws ExecutionException the execution exception * @throws TimeoutException the timeout exception */ public static long getCoinValueFromInput (Peer peer, List<TransactionInput> input) throws InterruptedException, ExecutionException, TimeoutException { List<TransactionOutPoint> outpointList = new ArrayList<TransactionOutPoint>(); for (TransactionInput t : input) { outpointList.add(t.getOutpoint()); } UTXOsMessage m = peer.getUTXOs(outpointList).get(100, TimeUnit.SECONDS); long value = 0; for (TransactionOutput out : m.getOutputs()) { value += out.getValue().value; } System.out.println(m); if (outpointList.size() != m.getOutputs().size()) { boolean found = false; for (TransactionOutPoint o : outpointList) { // for(TransactionOutput u : m.getOutputs()) { // if(o.getHash().toString().equals(u.getParentTransactionHash().toString())) { // found = true; // break; // } // } // if(found) break; System.out.println("Transaction Input not found in UTXO: " + o.getHash() + " " + o.getIndex()); } } return value; } // message.setSubject("New Critical Exception thrown.."); /** * Gets the coin value from output. * * @param output the output * @return the coin value from output */ public static long getCoinValueFromOutput (List<TransactionOutput> output) { long sumOutputs = 0; for (TransactionOutput out : output) { sumOutputs += out.getValue().value; } return sumOutputs; } // String text = ""; /** * Gets the dummy script. * * @return the dummy script */ public static Script getDummyScript () { ScriptBuilder builder = new ScriptBuilder(); builder.smallNum(0); return builder.build(); } // text += Tools.stacktraceToString(e); // text += "\n"; // if (m != null) { // text += m; // } // text += "\n"; // if (c != null) { // text += c; // } // text += "\n"; // if (p != null) { // text += p; // } // text += "\n"; // if (channelTransaction != null) { // text += channelTransaction; // } // text += "\n"; // if (t != null) { // text += t; // } /** * Gets the four character hash. * * @param s the s * @return the four character hash * @throws NoSuchAlgorithmException the no such algorithm exception */ public static String getFourCharacterHash (String s) throws NoSuchAlgorithmException { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); messageDigest.update(s.getBytes()); String encryptedString = Tools.byteToString58(messageDigest.digest()); return encryptedString.substring(0, 3); } // // Now set the actual message // message.setText(text); /** * Call to get the MasterKey for a new Channel. * TODO: Change to request master node key.. * * @param number Query the Database to get the latest unused number * @return DeterministicKey for the new Channel */ public static DeterministicKey getMasterKey (int number) { DeterministicKey hd = DeterministicKey.deserializeB58(SideConstants.KEY_B58, Constants.getNetwork()); // DeterministicKey hd = DeterministicKey.deserializeB58(null,KEY_B58); // DeterministicKey hd = HDKeyDerivation.createMasterPrivateKey(KEY.getBytes()); DeterministicHierarchy hi = new DeterministicHierarchy(hd); List<ChildNumber> childList = new ArrayList<ChildNumber>(); ChildNumber childNumber = new ChildNumber(number, true); childList.add(childNumber); DeterministicKey key = hi.get(childList, true, true); return key; } // // Send message // Transport.send(message); // System.out.println("Sent message successfully...."); // } catch (javax.mail.MessagingException e1) { // } // } // /** // * Gets the channel refund transaction. // * // * @param channel the channel // * @return the channel refund transaction // * @throws SQLException the SQL exception // * @throws AddressFormatException the address format exception // */ // public static TransactionWrapper getChannelRefundTransaction (Channel channel) throws SQLException, AddressFormatException { // Transaction refundTransaction = new Transaction(Constants.getNetwork()); public static Message getMessage (String data) throws IOException { data = java.net.URLDecoder.decode(data, "UTF-8"); Gson gson = new Gson(); Message message = gson.fromJson(data, Message.class); return message; } // refundTransaction.addOutput(Coin.valueOf(channel.getInitialAmountClient() - Tools.getTransactionFees(1, 2)), channel // .getChangeAddressClientAsAddress // ()); // refundTransaction.addOutput(Coin.valueOf(channel.getInitialAmountServer()), new Address(Constants.getNetwork(), channel.getChangeAddressServer())); /** * Gets the multisig input script. * * @param client the client * @param server the server * @return the multisig input script */ public static Script getMultisigInputScript (ECDSASignature client, ECDSASignature server) { ArrayList<TransactionSignature> signList = new ArrayList<TransactionSignature>(); signList.add(new TransactionSignature(client, SigHash.ALL, false)); signList.add(new TransactionSignature(server, SigHash.ALL, false)); Script inputScript = ScriptBuilder.createMultiSigInputScript(signList); /* * Seems there is a bug here, * https://groups.google.com/forum/#!topic/bitcoinj/A9R8TdUsXms */ Script workaround = new Script(inputScript.getProgram()); return workaround; } // refundTransaction.setLockTime(channel.getTimestampClose()); public static byte[] getRandomByte (int amount) { byte[] b = new byte[amount]; Random r = new Random(); r.nextBytes(b); return b; } public static List<byte[]> byteBufferListToByteArrayList (List<ByteBuffer> byteBufferList) { List<byte[]> byteArrayList = new ArrayList<>(byteBufferList.size()); for (ByteBuffer b : byteBufferList) { byteArrayList.add(b.array()); } return byteArrayList; } public static byte[] copyRandomByteInByteArray (byte[] dest, int offset, int length) { byte[] error = getRandomByte(length); System.arraycopy(error, 0, dest, offset, length); return dest; } // refundTransaction.addInput(channel.getOpeningTx().getOutput(0)); // refundTransaction.getInput(0).setSequenceNumber(0); public static TransactionSignature getSignature (Transaction transactionToSign, int index, TransactionOutput outputToSpend, ECKey key) { return getSignature(transactionToSign, index, outputToSpend.getScriptBytes(), key); } public static TransactionSignature getSignature (Transaction transactionToSign, int index, byte[] outputToSpend, ECKey key) { Sha256Hash hash = transactionToSign.hashForSignature(index, outputToSpend, SigHash.ALL, false); ECDSASignature signature = key.sign(hash).toCanonicalised(); return new TransactionSignature(signature, SigHash.ALL, false); } // Sha256Hash sighash = refundTransaction.hashForSignature(0, channel.getOpeningTx().getOutput(0).getScriptPubKey(), SigHash.ALL, false); // ECDSASignature signature = channel.getServerKeyOnServer().sign(sighash); /** * Gets the transaction fees. * * @param size the size * @return the transaction fees */ public static long getTransactionFees (int size) { // return (Math.ceil( ( (float) size )/1000) ) * 1000 * 500 ; return (long) (size * Constants.FEE_PER_BYTE); } // return new TransactionWrapper(refundTransaction, signature); // } //TODO: These 2 methods are pretty slow, change them to Base64 in production: /** * With reference to * http://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending * * @param inputs the inputs * @param outputs the outputs * @return the transaction fees */ public static long getTransactionFees (int inputs, int outputs) { /* * One output will pay to multi sig, 144 bytes */ int size = inputs * 180 + (outputs - 1) * 34 + 144 + 10 + 40; return Tools.getTransactionFees(size); } public static byte[] hashSecret (byte[] secret) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(secret); byte[] digest = md.digest(); RIPEMD160Digest dig = new RIPEMD160Digest(); dig.update(digest, 0, digest.length); byte[] out = new byte[20]; dig.doFinal(out, 0); return out; } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static Transaction addOutAndInputs (long value, Wallet wallet, HashMap<TransactionOutPoint, Integer> lockedOutputs, Script script) { final int TIME_LOCK_IN_SECONDS = 60; Transaction transaction = new Transaction(Constants.getNetwork()); long totalInput = 0; long neededAmount = value + Tools.getTransactionFees(20, 2); ArrayList<TransactionOutput> outputList = new ArrayList<>(); ArrayList<TransactionOutput> spendable = new ArrayList<>(wallet.calculateAllSpendCandidates()); ArrayList<TransactionOutput> tempList = new ArrayList<>(spendable); for (TransactionOutput o : tempList) { Integer timeLock = lockedOutputs.get(o); if (timeLock != null && timeLock > Tools.currentTime()) { spendable.remove(o); } } for (TransactionOutput o : wallet.calculateAllSpendCandidates()) { if (o.getValue().value > neededAmount) { /* * Ok, found a suitable output, need to split the change * TODO: Change (a few things), such that there will be no output < 500... */ outputList.add(o); totalInput += o.getValue().value; } } if (totalInput == 0) { /* * None of our outputs alone is sufficient, have to add multiples.. */ for (TransactionOutput o : wallet.calculateAllSpendCandidates()) { if (totalInput >= neededAmount) { continue; } totalInput += o.getValue().value; outputList.add(o); } } if (totalInput < value) { /* * Not enough outputs in total to pay for the channel.. */ throw new RuntimeException("Wallet Balance not sufficient"); //TODO } else { transaction.addOutput(Coin.valueOf(value), script); transaction.addOutput(Coin.valueOf(totalInput - value - Tools.getTransactionFees(2, 2)), wallet.freshReceiveAddress()); for (TransactionOutput o : outputList) { transaction.addInput(o); } /* * Sign all of our inputs.. */ int j = 0; for (int i = 0; i < outputList.size(); i++) { TransactionOutput o = outputList.get(i); ECKey key = wallet.findKeyFromPubHash(o.getAddressFromP2PKHScript(Constants.getNetwork()).getHash160()); System.out.println(key.toAddress(Constants.getNetwork())); TransactionSignature sig = Tools.getSignature(transaction, i, o, key); byte[] s = sig.encodeToBitcoin(); ScriptBuilder builder = new ScriptBuilder(); builder.data(s); builder.data(key.getPubKey()); transaction.getInput(i).setScriptSig(builder.build()); //TODO: Currently only working if we have P2PKH outputs in our wallet } } return transaction; } /** * Hash.java secret. * * @param secret the secret * @return the string * @throws UnsupportedEncodingException the unsupported encoding exception * @throws NoSuchAlgorithmException the no such algorithm exception */ public static String hashSecretToString (byte[] secret) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(secret); byte[] digest = md.digest(); RIPEMD160Digest dig = new RIPEMD160Digest(); dig.update(digest, 0, digest.length); byte[] out = new byte[20]; dig.doFinal(out, 0); return Tools.byteToString(out); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static byte[] hashSha (byte[] secret) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(secret); byte[] digest = md.digest(); return digest; } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static byte[] hashSha (byte[] secret, int rounds) { try { byte[] digest = secret; for (int i = 0; i < rounds; i++) { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(digest); digest = md.digest(); } return digest; } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static byte[] hexStringToByteArray (String s) { s = s.replaceAll(" ", "").toLowerCase(); int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); } return data; } /** * Int to bool. * * @param i the i * @return true, if successful */ public static boolean intToBool (int i) { boolean a = true; if (i == 0) { a = false; } return a; } public static byte[] intToByte (int i) { //TODO: Add functionality.. return new byte[4]; } /** * Send message. * * @param httpExchange the http exchange * @param message the message * @throws IOException Signals that an I/O exception has occurred. */ public static void sendMessage (HttpExchange httpExchange, Message message) throws IOException { String response = new Gson().toJson(message); httpExchange.sendResponseHeaders(200, response.length()); OutputStream os = httpExchange.getResponseBody(); os.write(response.getBytes()); os.close(); } /** * Sets the transaction lock time. * * @param transaction the transaction * @param locktime the locktime * @return the transaction */ public static Transaction setTransactionLockTime (Transaction transaction, int locktime) { transaction.setLockTime(locktime); for (TransactionInput input : transaction.getInputs()) { input.setSequenceNumber(0); } return transaction; } /** * Stacktrace to string. * * @param e the e * @return the string */ public static String stacktraceToString (Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); return sw.toString(); } /** * String to byte. * * @param string the string * @return the byte[] */ public static byte[] stringToByte (String string) { try { com.sun.org.apache.xml.internal.security.Init.init(); return Base64.decode(string); } catch (Base64DecodingException e) { throw new RuntimeException(e); } } /** * String to byte58. * * @param string the string * @return the byte[] */ public static byte[] stringToByte58 (String string) { try { return Base58.decode(string); } catch (AddressFormatException e) { throw new RuntimeException(e); } } // // // // // // // // // // // }