package org.ripple.power.txns.btc; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.LogManager; import org.ripple.power.config.LSystem; import org.ripple.power.ui.UIRes; import org.ripple.power.ui.view.log.ErrorLog; import org.ripple.power.utils.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BTCLoader { /** Application properties file */ private static File propFile; /** Peer addresses file */ private static File peersFile; public static boolean rpcOpen = false; public static boolean loadingChain = false; /** Test network */ public static boolean testNetwork = false; public static Properties properties; /** Host name */ private static String hostName; /** Listen port */ public static int listenPort = 8333; /** Maximum number of connections */ private static int maxConnections = 32; /** Maximum number of outbound connections */ private static int maxOutbound = 8; /** RPC port */ private static int rpcPort = 8332; /** RPC allowed hosts */ private static final List<InetAddress> rpcAllowIp = new ArrayList<>(); /** RPC user */ private static String rpcUser = ""; /** RPC password */ private static String rpcPassword = ""; /** Create bootstrap files */ private static boolean createBootstrap = false; /** Compact database */ private static boolean compactDatabase = false; /** Load block chain */ private static boolean loadBlockChain = false; /** Retry block */ private static boolean retryBlock = false; /** Bypass block verification */ private static boolean verifyBlocks = true; /** Block chain data directory for load */ private static String blockChainPath; /** Starting block number */ private static int startBlock; /** Stop block number */ private static int stopBlock; /** Retry block hash */ private static Sha256Hash retryHash; /** Peer addresses */ private static PeerAddress[] peerAddressesArray; /** Peer blacklist */ private static List<NetworkHandler.BlacklistEntry> peerBlacklist = new ArrayList<>(); /** Thread group */ private static ThreadGroup threadGroup; /** Worker threads */ private static final List<Thread> threads = new ArrayList<>(5); // Short-term lock object public static final Object lock = new Object(); /** Database listener */ public static DatabaseHandler databaseHandler; /** Message handler */ private static MessageHandler messageHandler; /** RPC handler */ private static RpcHandler rpcHandler; /** Application shutdown started */ public static boolean shutdown = true; // 1 Satoshi = 0.00000001 BTC private static final BigDecimal SATOSHI = new BigDecimal("100000000"); // Minimum transaction fee public static final BigInteger MIN_TX_FEE = new BigInteger("1000", 10); // Dust transaction value public static final BigInteger DUST_TRANSACTION = new BigInteger("546", 10); // Address list public static List<Address> addresses; // Bloom filter public static BloomFilter bloomFilter; // Key list public static List<ECKey> keys; // Change key public static ECKey changeKey; // Transaction maturity public static final int TRANSACTION_CONFIRMED = 6; /** Minimum protocol version */ public static final int MIN_PROTOCOL_VERSION = 60001; /** Genesis block bytes */ public static byte[] GENESIS_BLOCK_BYTES; /** Default network port */ public static final int DEFAULT_PORT = 8333; /** Coinbase transaction maturity */ public static final int COINBASE_MATURITY = 100; /** Minimum transaction relay fee */ public static final BigInteger MIN_TX_RELAY_FEE = new BigInteger("1000", 10); /** Maximum free transaction size */ public static final int MAX_FREE_TX_SIZE = 10000; /** Maximum ban score before a peer is disconnected */ public static final int MAX_BAN_SCORE = 100; /** Maximum peer address age (seconds) */ public static final int MAX_PEER_ADDRESS_AGE = 2 * 60 * 60; /** Block store */ public static BlockStore blockStore; /** Block chain */ public static BlockChain blockChain; /** Network handler */ public static NetworkHandler networkHandler; /** Network message handler */ public static NetworkMessageListener networkMessageListener; /** Local listen address */ public static PeerAddress listenAddress; /** Number of blocks received */ public static final AtomicLong blocksReceived = new AtomicLong(); /** Number of blocks sent */ public static final AtomicLong blocksSent = new AtomicLong(); /** Number of filtered blocks sent */ public static final AtomicLong filteredBlocksSent = new AtomicLong(); /** Number of transactions received */ public static final AtomicLong txReceived = new AtomicLong(); /** Number of transactions sent */ public static final AtomicLong txSent = new AtomicLong(); /** Number of transactions rejected */ public static final AtomicLong txRejected = new AtomicLong(); /** Network chain height */ public static int networkChainHeight; /** * List of peer requests that are waiting to be sent - synchronized on * pendingRequests */ public static final List<PeerRequest> pendingRequests = new LinkedList<>(); /** * List of peer requests that are waiting for a response - synchronized on * pendingRequests */ public static final List<PeerRequest> processedRequests = new LinkedList<>(); /** * Map of transactions in the memory pool (txHash, tx) - synchronized on * txMap */ public static final Map<Sha256Hash, StoredTransaction> txMap = new HashMap<>( 250); /** Map of recent transactions (txHash, txHash) - synchronized on txMap */ public static final Map<Sha256Hash, Sha256Hash> recentTxMap = new HashMap<>( 250); /** * Map of orphan transactions (parentTxHash, orphanTxList) - synchronized on * txMap */ public static final Map<Sha256Hash, List<StoredTransaction>> orphanTxMap = new HashMap<>( 250); /** * Map of recent spent outputs (Outpoint. spendingTxHash) - synchronized on * txMap */ public static final Map<OutPoint, Sha256Hash> spentOutputsMap = new HashMap<>( 250); /** List of Bloom filters - synchronized on bloomFilters */ public static final List<BloomFilter> bloomFilters = new LinkedList<>(); /** Database handler message queue */ public static final LinkedBlockingQueue<Object> databaseQueue = new LinkedBlockingQueue<>(); /** Message handler message queue */ public static final LinkedBlockingQueue<Message> messageQueue = new LinkedBlockingQueue<>( 250); /** Peer addresses - synchronized on peerAddresses */ public static final List<PeerAddress> peerAddresses = new LinkedList<>(); /** Peer address map - synchronized on peerAddresses */ public static final Map<PeerAddress, PeerAddress> peerMap = new HashMap<>( 250); /** Completed messages */ public static final ConcurrentLinkedQueue<Message> completedMessages = new ConcurrentLinkedQueue<>(); /** Alert list */ public static final List<Alert> alerts = new ArrayList<Alert>(); public static final Logger log = LoggerFactory .getLogger("org.ripple.power.btc"); public static void info(String message) { if (testNetwork) { log.info(message); } } public static void debug(String message) { if (testNetwork) { log.debug(message); } } public static void debug(String message, Throwable thr) { if (testNetwork) { log.debug(message, thr); } } public static void warn(String message) { if (testNetwork) { log.warn(message); } } public static void warn(String message, Throwable thr) { if (testNetwork) { log.warn(message, thr); } } public static void error(String message) { if (testNetwork) { log.error(message); } } public static void error(String message, Throwable thr) { if (testNetwork) { log.error(message, thr); } } public static void start(String[] cmds) { try { shutdown = false; String dataPath = LSystem.getBitcionDirectory(); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { shutdown(); } }); String pString = System.getProperty("bitcoin.verify.blocks"); if (pString != null && pString.equals("0")) { verifyBlocks = false; } if (cmds.length != 0) { processArguments(cmds); } if (testNetwork) { dataPath = dataPath + LSystem.FS + "TestNet"; } File dirFile = new File(dataPath); if (!dirFile.exists()) { FileUtils.makedirs(dirFile); } File logFile = new File(dataPath + LSystem.FS + "logging.properties"); if (logFile.exists()) { FileInputStream inStream = new FileInputStream(logFile); LogManager.getLogManager().readConfiguration(inStream); } BriefLogFormatter.init(); processConfig(); if (testNetwork && peerAddressesArray == null && maxOutbound != 0) { throw new IllegalArgumentException( "You must specify at least one peer for the test network"); } String genesisName = (testNetwork ? "GenesisBlockTest.dat" : "GenesisBlockProd.dat"); try (InputStream classStream = UIRes.getStream(genesisName)) { if (classStream == null) { throw new IOException("Genesis block resource not found"); } BTCLoader.GENESIS_BLOCK_BYTES = new byte[classStream .available()]; classStream.read(BTCLoader.GENESIS_BLOCK_BYTES); } propFile = new File(dataPath + LSystem.FS + "btc.properties"); properties = new Properties(); if (propFile.exists()) { try (FileInputStream in = new FileInputStream(propFile)) { properties.load(in); } } // // Initialize the Bitcoin consensus library // BitcoinConsensus.init(); // // Initialize the BitcoinCore library // NetParams.configure(testNetwork, BTCLoader.MIN_PROTOCOL_VERSION, NetParams.NODE_NETWORK); blockStore = new BlockStoreDataBase(dataPath); // // Compact the database // if (compactDatabase) { blockStore.compactDatabase(); shutdown(); } // // Create the block chain // blockChain = new BlockChain(verifyBlocks); // // Retry a held block and then exit // if (retryBlock) { StoredBlock storedBlock = blockStore.getStoredBlock(retryHash); if (storedBlock != null) { if (!storedBlock.isOnChain()) { blockChain.updateBlockChain(storedBlock); } else { log.error(String.format( "Block is already on the chain\n Block %s", retryHash.toString())); } } else { log.error(String.format("Block not found\n Block %s", retryHash.toString())); } shutdown(); } // // Load the block chain from disk and then exit // if (loadBlockChain) { LoadBlockChain.load(blockChainPath, startBlock, stopBlock); shutdown(); } // // Create the bootstrap files and then exit // if (createBootstrap) { CreateBootstrap.process(blockChainPath, startBlock, stopBlock); shutdown(); } // // Get the peer addresses // peersFile = new File(String.format("%s%speers.dat", dataPath, LSystem.FS)); if (peersFile.exists() && peersFile.length() > 0) { byte[] fileBuffer = new byte[(int) peersFile.length()]; try (FileInputStream inStream = new FileInputStream(peersFile)) { inStream.read(fileBuffer); } SerializedBuffer inBuffer = new SerializedBuffer(fileBuffer); while (inBuffer.available() > 0) { PeerAddress peerAddress = new PeerAddress(inBuffer); BTCLoader.peerAddresses.add(peerAddress); BTCLoader.peerMap.put(peerAddress, peerAddress); } } // // Get the address and key lists // addresses = blockStore.getAddressList(); keys = blockStore.getKeyList(); // // Locate the change key and create it if we don't have one yet // for (ECKey key : keys) { if (key.isChange()) { changeKey = key; break; } } if (changeKey == null) { ECKey changeKey = new ECKey(); changeKey.setLabel("<Change>"); changeKey.setChange(true); blockStore.storeKey(changeKey); keys.add(changeKey); } // // Create our bloom filter // int elementCount = keys.size() * 2 + 15; BloomFilter filter = new BloomFilter(elementCount); for (ECKey key : keys) { filter.insert(key.getPubKey()); filter.insert(key.getPubKeyHash()); } bloomFilter = filter; // // Start the worker threads // threadGroup = new ThreadGroup("Workers"); databaseHandler = new DatabaseHandler(); Thread thread = new Thread(threadGroup, databaseHandler, "Database Handler"); thread.start(); threads.add(thread); BTCLoader.networkMessageListener = new NetworkMessageListener(); BTCLoader.networkHandler = new NetworkHandler(maxConnections, maxOutbound, hostName, listenPort, peerAddressesArray, peerBlacklist); thread = new Thread(threadGroup, BTCLoader.networkHandler, "Network Handler"); thread.start(); threads.add(thread); messageHandler = new MessageHandler(); thread = new Thread(threadGroup, messageHandler, "Message Handler"); thread.start(); threads.add(thread); if (rpcOpen) { // // Start the RPC handler // rpcHandler = new RpcHandler(rpcPort, rpcAllowIp, rpcUser, rpcPassword); } } catch (Exception ex) { ex.printStackTrace(); } } public static void shutdown() { if (shutdown) { return; } shutdown = true; log.info(LSystem.applicationName + " shutdown started"); if (!threads.isEmpty()) { try { if (BTCLoader.networkHandler != null) { BTCLoader.networkHandler.shutdown(); } if (databaseHandler != null) { databaseHandler.shutdown(); } if (messageHandler != null) { messageHandler.shutdown(); } if (rpcHandler != null) { rpcHandler.shutdown(); } for (Thread thread : threads) { thread.join(120000); } } catch (InterruptedException exc) { } } if (blockStore != null) { blockStore.close(); } if (!BTCLoader.peerAddresses.isEmpty()) { try { try (FileOutputStream outStream = new FileOutputStream( peersFile)) { int peerCount = 0; for (PeerAddress peerAddress : BTCLoader.peerAddresses) { if (!peerAddress.isStatic()) { outStream.write(peerAddress.getBytes()); peerCount++; if (peerCount >= 50) break; } } } } catch (IOException exc) { log.error("Unable to save peer addresses", exc); } } if (propFile != null) { saveProperties(); } log.info("RipplePower shutdown completed"); if (LogManager.getLogManager() instanceof LogManagerOverride) ((LogManagerOverride) LogManager.getLogManager()).logShutdown(); } public static void saveProperties() { try { try (FileOutputStream out = new FileOutputStream(propFile)) { properties.store(out, "RipplePower Properties"); } } catch (Exception exc) { ErrorLog.get().logException( "Exception while saving application properties", exc); } } private static void processArguments(String[] args) throws UnknownHostException { switch (args[0].toLowerCase()) { case "bootstrap": createBootstrap = true; if (args.length < 2) throw new IllegalArgumentException( "Specify PROD or TEST with the BOOTSTRAP option"); if (args[1].equalsIgnoreCase("TEST")) { testNetwork = true; } else if (!args[1].equalsIgnoreCase("PROD")) { throw new IllegalArgumentException( "Specify PROD or TEST after the BOOTSTRAP option"); } if (args.length < 3) { throw new IllegalArgumentException( "You must specify the bootstrap directory"); } blockChainPath = args[2]; if (args.length > 3) { startBlock = Integer.parseInt(args[3]); if (startBlock < 0) throw new IllegalArgumentException( "Start height is less than 0"); } else { startBlock = 0; } if (args.length > 4) { stopBlock = Integer.parseInt(args[4]); if (stopBlock < startBlock) throw new IllegalArgumentException( "Stop height is less than start height"); } else { stopBlock = Integer.MAX_VALUE; } if (args.length > 5) throw new IllegalArgumentException( "Unrecognized command line parameter"); break; case "compact": compactDatabase = true; if (args.length < 2) throw new IllegalArgumentException( "Specify PROD or TEST with the COMPACT option"); if (args[1].equalsIgnoreCase("TEST")) { testNetwork = true; } else if (!args[1].equalsIgnoreCase("PROD")) { throw new IllegalArgumentException( "Specify PROD or TEST after the COMPACT option"); } if (args.length > 2) throw new IllegalArgumentException( "Unrecognized command line parameter"); break; case "load": loadBlockChain = true; if (args.length < 2) throw new IllegalArgumentException( "Specify PROD or TEST with the LOAD option"); if (args[1].equalsIgnoreCase("TEST")) { testNetwork = true; } else if (!args[1].equalsIgnoreCase("PROD")) { throw new IllegalArgumentException( "Specify PROD or TEST after the LOAD option"); } if (args.length > 2) { blockChainPath = args[2]; } else { blockChainPath = LSystem.getBitcionDirectory(); } if (args.length > 3) { startBlock = Integer.parseInt(args[3]); if (startBlock < 0) throw new IllegalArgumentException( "Start block is less than 0"); } else { startBlock = 0; } if (args.length > 4) { stopBlock = Integer.parseInt(args[4]); if (stopBlock < startBlock) throw new IllegalArgumentException( "Stop block is less than start block"); } else { stopBlock = Integer.MAX_VALUE; } if (args.length > 5) throw new IllegalArgumentException( "Unrecognized command line parameter"); break; case "retry": retryBlock = true; if (args.length < 3) throw new IllegalArgumentException( "Specify PROD or TEST followed by the block hash"); if (args[1].equalsIgnoreCase("TEST")) { testNetwork = true; } else if (!args[1].equalsIgnoreCase("PROD")) { throw new IllegalArgumentException( "Specify PROD or TEST after the RETRY option"); } retryHash = new Sha256Hash(args[2]); if (args.length > 3) throw new IllegalArgumentException( "Unrecognized command line parameter"); break; case "test": testNetwork = true; if (args.length > 1) throw new IllegalArgumentException( "Unrecognized command line parameter"); break; case "prod": if (args.length > 1) { throw new IllegalArgumentException( "Unrecognized command line parameter"); } break; default: throw new IllegalArgumentException( "Unrecognized command line parameter"); } } /** * Process the configuration file * * @throws IllegalArgumentException * Invalid configuration option * @throws IOException * Unable to read configuration file * @throws UnknownHostException * Invalid peer address specified */ private static void processConfig() throws IOException, IllegalArgumentException, UnknownHostException { // // Use the defaults if there is no configuration file // File configFile = new File(LSystem.getBitcionDirectory() + LSystem.FS + "rpbitcoin.conf"); if (!configFile.exists()) { return; } // // Process the configuration file // List<PeerAddress> addressList = new ArrayList<>(5); try (BufferedReader in = new BufferedReader(new FileReader(configFile))) { String line; while ((line = in.readLine()) != null) { line = line.trim(); if (line.length() == 0 || line.charAt(0) == '#') continue; int sep = line.indexOf('='); if (sep < 1) throw new IllegalArgumentException(String.format( "Invalid configuration option: %s", line)); String option = line.substring(0, sep).trim().toLowerCase(); String value = line.substring(sep + 1).trim(); switch (option) { case "blacklistpeer": sep = value.indexOf('/'); InetAddress blacklistAddr; int mask; if (sep < 0 || sep == value.length() - 1) { blacklistAddr = InetAddress.getByName(value); mask = -1; } else if (sep == 0) { throw new IllegalArgumentException( "Invalid blacklist address: " + value); } else { blacklistAddr = InetAddress.getByName(value.substring( 0, sep)); mask = Integer.parseInt(value.substring(sep + 1)); } peerBlacklist.add(new NetworkHandler.BlacklistEntry( blacklistAddr, mask)); log.info(value + " added to peer blacklist"); break; case "connect": PeerAddress addr = new PeerAddress(value); addressList.add(addr); break; case "hostname": hostName = value; break; case "maxconnections": maxConnections = Integer.parseInt(value); break; case "maxoutbound": maxOutbound = Integer.parseInt(value); break; case "port": listenPort = Integer.parseInt(value); break; case "rpcallowip": InetAddress[] inetAddrs = InetAddress.getAllByName(value); rpcAllowIp.addAll(Arrays.asList(inetAddrs)); rpcOpen = true; break; case "rpcpassword": rpcPassword = value; rpcOpen = true; break; case "rpcport": rpcPort = Integer.parseInt(value); rpcOpen = true; break; case "rpcuser": rpcUser = value; rpcOpen = true; break; default: throw new IllegalArgumentException(String.format( "Invalid configuration option: %s", line)); } } } if (!addressList.isEmpty()) { peerAddressesArray = addressList .toArray(new PeerAddress[addressList.size()]); } } public static BigInteger stringToSatoshi(String value) throws NumberFormatException { if (value == null) { throw new IllegalArgumentException("No string value provided"); } if (value.isEmpty()) { return BigInteger.ZERO; } BigDecimal decValue = new BigDecimal(value); return decValue.multiply(SATOSHI).toBigInteger(); } public static String satoshiToString(BigInteger value) { BigInteger bvalue = value; boolean negative = bvalue.compareTo(BigInteger.ZERO) < 0; if (negative) { bvalue = bvalue.negate(); } BigDecimal dvalue = new BigDecimal(bvalue, 8); String formatted = dvalue.toPlainString(); int decimalPoint = formatted.indexOf("."); int toDelete = 0; for (int i = formatted.length() - 1; i > decimalPoint + 4; i--) { if (formatted.charAt(i) == '0') { toDelete++; } else { break; } } String text = (negative ? "-" : "") + formatted.substring(0, formatted.length() - toDelete); return text; } public static void dumpData(String text, byte[] data) { dumpData(text, data, 0, data.length); } public static void dumpData(String text, byte[] data, int length) { dumpData(text, data, 0, length); } public static void dumpData(String text, byte[] data, int offset, int length) { StringBuilder outString = new StringBuilder(512); outString.append(text); outString.append("\n"); for (int i = 0; i < length; i++) { if (i % 32 == 0) outString.append(String.format(" %14X ", i)); else if (i % 4 == 0) outString.append(" "); outString.append(String.format("%02X", data[offset + i])); if (i % 32 == 31) outString.append("\n"); } if (length % 32 != 0) { outString.append("\n"); } info(outString.toString()); } }