/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt.mint; import nxt.Attachment; import nxt.Constants; import nxt.CurrencyMinting; import nxt.Nxt; import nxt.NxtException; import nxt.Transaction; import nxt.crypto.Crypto; import nxt.crypto.HashFunction; import nxt.http.API; import nxt.util.Convert; import nxt.util.Logger; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class MintWorker { // Verify-all name verifier private final static HostnameVerifier hostNameVerifier = (hostname, session) -> true; // Trust-all socket factory private static final SSLSocketFactory sslSocketFactory; static { TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) { } public void checkServerTrusted(X509Certificate[] certs, String authType) { } }}; SSLContext sc; try { sc = SSLContext.getInstance("TLS"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } try { sc.init(null, trustAllCerts, new java.security.SecureRandom()); } catch (KeyManagementException e) { throw new IllegalStateException(e); } sslSocketFactory = sc.getSocketFactory(); } public static void main(String[] args) { MintWorker mintWorker = new MintWorker(); mintWorker.mint(); } private void mint() { String currencyCode = Convert.emptyToNull(Nxt.getStringProperty("nxt.mint.currencyCode")); if (currencyCode == null) { throw new IllegalArgumentException("nxt.mint.currencyCode not specified"); } String secretPhrase = Convert.emptyToNull(Nxt.getStringProperty("nxt.mint.secretPhrase", null, true)); if (secretPhrase == null) { throw new IllegalArgumentException("nxt.mint.secretPhrase not specified"); } boolean isSubmitted = Nxt.getBooleanProperty("nxt.mint.isSubmitted"); boolean isStopOnError = Nxt.getBooleanProperty("nxt.mint.stopOnError"); byte[] publicKeyHash = Crypto.sha256().digest(Crypto.getPublicKey(secretPhrase)); long accountId = Convert.fullHashToId(publicKeyHash); String rsAccount = Convert.rsAccount(accountId); JSONObject currency = getCurrency(currencyCode); if (currency.get("currency") == null) { throw new IllegalArgumentException("Invalid currency code " + currencyCode); } long currencyId = Convert.parseUnsignedLong((String) currency.get("currency")); if (currency.get("algorithm") == null) { throw new IllegalArgumentException("Minting algorithm not specified, currency " + currencyCode + " is not mintable"); } byte algorithm = (byte)(long) currency.get("algorithm"); byte decimal = (byte)(long) currency.get("decimals"); String unitsStr = Nxt.getStringProperty("nxt.mint.unitsPerMint"); double wholeUnits = 1; if (unitsStr != null && unitsStr.length() > 0) { wholeUnits = Double.parseDouble(unitsStr); } long units = (long)(wholeUnits * Math.pow(10, decimal)); JSONObject mintingTarget = getMintingTarget(currencyId, rsAccount, units); long counter = (long) mintingTarget.get("counter"); byte[] target = Convert.parseHexString((String) mintingTarget.get("targetBytes")); BigInteger difficulty = new BigInteger((String)mintingTarget.get("difficulty")); long initialNonce = Nxt.getIntProperty("nxt.mint.initialNonce"); if (initialNonce == 0) { initialNonce = new Random().nextLong(); } int threadPoolSize = Nxt.getIntProperty("nxt.mint.threadPoolSize"); if (threadPoolSize == 0) { threadPoolSize = Runtime.getRuntime().availableProcessors(); Logger.logDebugMessage("Thread pool size " + threadPoolSize); } ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize); Logger.logInfoMessage("Mint worker started"); while (true) { counter++; try { JSONObject response = mintImpl(secretPhrase, accountId, units, currencyId, algorithm, counter, target, initialNonce, threadPoolSize, executorService, difficulty, isSubmitted); Logger.logInfoMessage("currency mint response:" + response.toJSONString()); } catch (Exception e) { Logger.logInfoMessage("mint error", e); if (isStopOnError) { Logger.logInfoMessage("stopping on error"); break; } else { Logger.logInfoMessage("continue"); } } mintingTarget = getMintingTarget(currencyId, rsAccount, units); target = Convert.parseHexString((String) mintingTarget.get("targetBytes")); difficulty = new BigInteger((String)mintingTarget.get("difficulty")); } } private JSONObject mintImpl(String secretPhrase, long accountId, long units, long currencyId, byte algorithm, long counter, byte[] target, long initialNonce, int threadPoolSize, ExecutorService executorService, BigInteger difficulty, boolean isSubmitted) { long startTime = System.currentTimeMillis(); List<Callable<Long>> workersList = new ArrayList<>(); for (int i=0; i < threadPoolSize; i++) { HashSolver hashSolver = new HashSolver(algorithm, currencyId, accountId, counter, units, initialNonce + i, target, threadPoolSize); workersList.add(hashSolver); } long solution = solve(executorService, workersList); long computationTime = System.currentTimeMillis() - startTime; if (computationTime == 0) { computationTime = 1; } long hashes = solution - initialNonce; float hashesPerDifficulty = BigInteger.valueOf(-1).equals(difficulty) ? 0 : (float) hashes / difficulty.floatValue(); Logger.logInfoMessage("solution nonce %d unitsNQT %d counter %d computed hashes %d time [sec] %.2f hash rate [KH/Sec] %d actual time vs. expected %.2f is submitted %b", solution, units, counter, hashes, (float) computationTime / 1000, hashes / computationTime, hashesPerDifficulty, isSubmitted); JSONObject response; if (isSubmitted) { response = currencyMint(secretPhrase, currencyId, solution, units, counter); } else { response = new JSONObject(); response.put("message", "nxt.mint.isSubmitted=false therefore currency mint transaction is not submitted"); } return response; } private long solve(Executor executor, Collection<Callable<Long>> solvers) { CompletionService<Long> ecs = new ExecutorCompletionService<>(executor); List<Future<Long>> futures = new ArrayList<>(solvers.size()); solvers.forEach(solver -> futures.add(ecs.submit(solver))); try { return ecs.take().get(); } catch (ExecutionException | InterruptedException e) { throw new IllegalStateException(e); } finally { for (Future<Long> f : futures) { f.cancel(true); } } } private JSONObject currencyMint(String secretPhrase, long currencyId, long nonce, long units, long counter) { JSONObject ecBlock = getECBlock(); Attachment attachment = new Attachment.MonetarySystemCurrencyMinting(nonce, currencyId, units, counter); Transaction.Builder builder = Nxt.newTransactionBuilder(Crypto.getPublicKey(secretPhrase), 0, Constants.ONE_NXT, (short) 120, attachment) .timestamp(((Long) ecBlock.get("timestamp")).intValue()) .ecBlockHeight(((Long) ecBlock.get("ecBlockHeight")).intValue()) .ecBlockId(Convert.parseUnsignedLong((String) ecBlock.get("ecBlockId"))); try { Transaction transaction = builder.build(secretPhrase); Map<String, String> params = new HashMap<>(); params.put("requestType", "broadcastTransaction"); params.put("transactionBytes", Convert.toHexString(transaction.getBytes())); return getJsonResponse(params); } catch (NxtException.NotValidException e) { Logger.logInfoMessage("local signing failed", e); JSONObject response = new JSONObject(); response.put("error", e.toString()); return response; } } private JSONObject getCurrency(String currencyCode) { Map<String, String> params = new HashMap<>(); params.put("requestType", "getCurrency"); params.put("code", currencyCode); return getJsonResponse(params); } private JSONObject getMintingTarget(long currencyId, String rsAccount, long units) { Map<String, String> params = new HashMap<>(); params.put("requestType", "getMintingTarget"); params.put("currency", Long.toUnsignedString(currencyId)); params.put("account", rsAccount); params.put("units", Long.toString(units)); return getJsonResponse(params); } private JSONObject getECBlock() { Map<String, String> params = new HashMap<>(); params.put("requestType", "getECBlock"); return getJsonResponse(params); } private JSONObject getJsonResponse(Map<String, String> params) { JSONObject response; HttpURLConnection connection = null; String host = Convert.emptyToNull(Nxt.getStringProperty("nxt.mint.serverAddress")); if (host == null) { try { host = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { host = "localhost"; } } String protocol = "http"; boolean useHttps = Nxt.getBooleanProperty("nxt.mint.useHttps"); if (useHttps) { protocol = "https"; HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory); HttpsURLConnection.setDefaultHostnameVerifier(hostNameVerifier); } int port = Constants.isTestnet ? API.TESTNET_API_PORT : Nxt.getIntProperty("nxt.apiServerPort"); String urlParams = getUrlParams(params); URL url; try { url = new URL(protocol, host, port, "/nxt?" + urlParams); } catch (MalformedURLException e) { throw new IllegalStateException(e); } try { Logger.logDebugMessage("Sending request to server: " + url.toString()); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { try (Reader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"))) { response = (JSONObject) JSONValue.parse(reader); } } else { response = null; } } catch (RuntimeException | IOException e) { Logger.logInfoMessage("Error connecting to server", e); if (connection != null) { connection.disconnect(); } throw new IllegalStateException(e); } if (response == null) { throw new IllegalStateException(String.format("Request %s response error", url)); } if (response.get("errorCode") != null) { throw new IllegalStateException(String.format("Request %s produced error response code %s message \"%s\"", url, response.get("errorCode"), response.get("errorDescription"))); } if (response.get("error") != null) { throw new IllegalStateException(String.format("Request %s produced error %s", url, response.get("error"))); } return response; } private static String getUrlParams(Map<String, String> params) { if (params == null) { return ""; } StringBuilder sb = new StringBuilder(); for (String key : params.keySet()) { try { sb.append(key).append("=").append(URLEncoder.encode(params.get(key), "utf8")).append("&"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } String rc = sb.toString(); if (rc.endsWith("&")) { rc = rc.substring(0, rc.length() - 1); } return rc; } private static class HashSolver implements Callable<Long> { private final HashFunction hashFunction; private final long currencyId; private final long accountId; private final long counter; private final long units; private final long nonce; private final byte[] target; private final int poolSize; private HashSolver(byte algorithm, long currencyId, long accountId, long counter, long units, long nonce, byte[] target, int poolSize) { this.hashFunction = HashFunction.getHashFunction(algorithm); this.currencyId = currencyId; this.accountId = accountId; this.counter = counter; this.units = units; this.nonce = nonce; this.target = target; this.poolSize = poolSize; } @Override public Long call() { long n = nonce; while (!Thread.currentThread().isInterrupted()) { byte[] hash = CurrencyMinting.getHash(hashFunction, n, currencyId, units, counter, accountId); if (CurrencyMinting.meetsTarget(hash, target)) { Logger.logDebugMessage("%s found solution hash %s nonce %d currencyId %d units %d counter %d accountId %d" + " hash %s meets target %s", Thread.currentThread().getName(), hashFunction, n, currencyId, units, counter, accountId, Arrays.toString(hash), Arrays.toString(target)); return n; } n+=poolSize; if (((n - nonce) % (poolSize * 1000000)) == 0) { Logger.logInfoMessage("%s computed %d [MH]", Thread.currentThread().getName(), (n - nonce) / poolSize / 1000000); } } return null; } } }