package snowblossom.miner.plow; import com.google.protobuf.ByteString; import duckutil.Config; import duckutil.ConfigFile; import duckutil.PeriodicThread; import duckutil.TimeRecord; import duckutil.jsonrpc.JsonRpcServer; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; import java.math.BigInteger; import java.text.DecimalFormat; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; import snowblossom.client.StubUtil; import snowblossom.lib.*; import snowblossom.lib.db.DB; import snowblossom.lib.db.lobstack.LobstackDB; import snowblossom.lib.db.rocksdb.JRocksDB; import snowblossom.mining.proto.*; import snowblossom.proto.*; import snowblossom.proto.UserServiceGrpc.UserServiceBlockingStub; import snowblossom.proto.UserServiceGrpc.UserServiceStub; public class MrPlow { private static final Logger logger = Logger.getLogger("snowblossom.miner"); public static final int BACK_BLOCKS=5; // Roughly how many blocks back to keep shares for PPLNS // Basically read this as if there are SHARES_IN_VIEW_FOR_RETARGET // inside a single SHARE_VIEW_WINDOW, then move the miner up one difficulty // So as it is set, if a miner gets 12 shares inside of 2 minutes, move them up. public static final long SHARE_VIEW_WINDOW = 120000L; public static final int SHARES_IN_VIEW_FOR_UPTARGET = 12; public static final int SHARES_IN_VIEW_FOR_DOWNTARGET = 4; public static final long TEMPLATE_AGE_MAX_MS=40000L; //Should get updates every 30 seconds public static ByteString BLOCK_KEY = ByteString.copyFrom(new String("blocks_found").getBytes()); public static void main(String args[]) throws Exception { Globals.addCryptoProvider(); if (args.length != 1) { logger.log(Level.SEVERE, "Incorrect syntax. Syntax: MrPlow <config_file>"); System.exit(-1); } ConfigFile config = new ConfigFile(args[0]); LogSetup.setup(config); MrPlow miner = new MrPlow(config); } private volatile Block last_block_template; private volatile long last_block_template_time; private UserServiceStub asyncStub; private UserServiceBlockingStub blockingStub; private StreamObserver<SubscribeBlockTemplateRequest> template_update_observer; private final NetworkParams params; private AtomicLong op_count = new AtomicLong(0L); private long last_stats_time = System.currentTimeMillis(); private Config config; private TimeRecord time_record; private MiningPoolServiceAgent agent; private ShareManager share_manager; private DB db; private ReportManager report_manager; private final int min_diff; private final PlowLoop loop; public MrPlow(Config config) throws Exception { this.config = config; logger.info(String.format("Starting MrPlow version %s", Globals.VERSION)); config.require("pool_address"); config.require("pool_fee"); config.require("db_type"); config.require("db_path"); min_diff = config.getIntWithDefault("min_diff", 22); params = NetworkParams.loadFromConfig(config); if (config.getBoolean("display_timerecord")) { time_record = new TimeRecord(); TimeRecord.setSharedRecord(time_record); } int port = config.getIntWithDefault("mining_pool_port",23380); agent = new MiningPoolServiceAgent(this); double pool_fee = config.getDouble("pool_fee"); double duck_fee = config.getDoubleWithDefault("pay_the_duck", 0.0); TreeMap<String, Double> fixed_fee_map = new TreeMap<>(); fixed_fee_map.put( AddressUtil.getAddressString(params.getAddressPrefix(), getPoolAddress()), pool_fee ); if (duck_fee > 0.0) { fixed_fee_map.put( "snow:crqls8qkumwg353sfgf5kw2lw2snpmhy450nqezr", duck_fee); } loadDB(); PPLNSState pplns_state = null; try { pplns_state = PPLNSState.parseFrom(db.getSpecialMap().get("pplns_state")); logger.info(String.format("Loaded PPLNS state with %d entries", pplns_state.getShareEntriesCount())); } catch(Throwable t) { logger.log(Level.WARNING, "Unable to load PPLNS state, starting fresh:" + t); } share_manager = new ShareManager(fixed_fee_map, pplns_state); report_manager = new ReportManager(); subscribe(); Server s = ServerBuilder .forPort(port) .addService(agent) .build(); if (config.isSet("rpc_port")) { JsonRpcServer json_server = new JsonRpcServer(config, false); new MrPlowJsonHandler(this).registerHandlers(json_server); } s.start(); loop = new PlowLoop(); loop.start(); } public int getMinDiff() { return min_diff; } private void loadDB() throws Exception { String db_type = config.get("db_type"); if(db_type.equals("rocksdb")) { db = new DB(config, new JRocksDB(config)); } else if (db_type.equals("lobstack")) { db = new DB(config, new LobstackDB(config)); } else { logger.log(Level.SEVERE, String.format("Unknown db_type: %s", db_type)); throw new RuntimeException("Unable to load DB"); } db.open(); } public class PlowLoop extends PeriodicThread { private long last_report; public PlowLoop() { super(20000); last_report = System.currentTimeMillis(); setDaemon(false); setName("PlowLoop"); } @Override public void runPass() throws Exception { printStats(); // Subscribe causes a new block template to be built from the // share manager so has to be run subscribe(); prune(); saveState(); if (config.isSet("report_path")) { if (last_report + 60000L < System.currentTimeMillis()) { report_manager.writeReport(config.get("report_path")); last_report = System.currentTimeMillis(); } } } } public DB getDB() {return db;} private void saveState() { PPLNSState state = share_manager.getState(); db.getSpecialMap().put("pplns_state", state.toByteString()); } private ManagedChannel channel; public void recordHashes(long n) { op_count.addAndGet(n); } private void prune() { Block b = last_block_template; if (b!=null) { double diff_delta = PowUtil.getDiffForTarget(BlockchainUtil.targetBytesToBigInteger(b.getHeader().getTarget())) - getMinDiff(); long shares_to_keep = Math.round( Math.pow(2, diff_delta) * BACK_BLOCKS); logger.fine(String.format("Pruning to %d shares", shares_to_keep)); share_manager.prune(shares_to_keep); } } private void subscribe() throws Exception { if (channel != null) if (last_block_template_time + TEMPLATE_AGE_MAX_MS < System.currentTimeMillis()) { logger.info(String.format("No block template in %s ms. Restarting connection.", TEMPLATE_AGE_MAX_MS)); channel.shutdownNow(); channel = null; } if (channel == null) { channel = StubUtil.openChannel(config, params); asyncStub = UserServiceGrpc.newStub(channel); blockingStub = UserServiceGrpc.newBlockingStub(channel); template_update_observer = asyncStub.subscribeBlockTemplateStream(new BlockTemplateEater()); } CoinbaseExtras.Builder extras = CoinbaseExtras.newBuilder(); if (config.isSet("remark")) { extras.setRemarks(ByteString.copyFrom(config.get("remark").getBytes())); } if (config.isSet("vote_yes")) { List<String> lst = config.getList("vote_yes"); for(String s : lst) { extras.addMotionsApproved( Integer.parseInt(s)); } } if (config.isSet("vote_no")) { List<String> lst = config.getList("vote_no"); for(String s : lst) { extras.addMotionsRejected( Integer.parseInt(s)); } } Map<String, Double> rates = share_manager.getPayRatios(); template_update_observer.onNext( SubscribeBlockTemplateRequest.newBuilder() .putAllPayRatios( rates ) .setExtras(extras.build()).build()); logger.info("Block template updated - " + rates); } private AddressSpecHash getPoolAddress() throws Exception { String address = config.get("pool_address"); AddressSpecHash to_addr = new AddressSpecHash(address, params); return to_addr; } public void stop() { terminate = true; loop.halt(); } private volatile boolean terminate = false; public NetworkParams getParams() {return params;} public UserServiceBlockingStub getBlockingStub(){return blockingStub;} public ShareManager getShareManager(){return share_manager;} public ReportManager getReportManager(){return report_manager;} public MiningPoolServiceAgent getAgent(){return agent;} public void printStats() { long now = System.currentTimeMillis(); double count = op_count.getAndSet(0L); double time_ms = now - last_stats_time; double time_sec = time_ms / 1000.0; double rate = count / time_sec; DecimalFormat df = new DecimalFormat("0.000"); String block_time_report = ""; if (last_block_template != null) { BigInteger target = BlockchainUtil.targetBytesToBigInteger(last_block_template.getHeader().getTarget()); double diff = PowUtil.getDiffForTarget(target); double block_time_sec = Math.pow(2.0, diff) / rate; double hours = block_time_sec / 3600.0; block_time_report = String.format("- at this rate %s hours per block", df.format(hours)); } logger.info(String.format("Mining rate: %s", report_manager.getTotalRate().getReportLong(df))); logger.info(String.format("Mining rate: %s/sec %s", df.format(rate), block_time_report)); last_stats_time = now; if (config.getBoolean("display_timerecord")) { TimeRecord old = time_record; time_record = new TimeRecord(); TimeRecord.setSharedRecord(time_record); old.printReport(System.out); } } public Block getBlockTemplate() { return last_block_template; } public class BlockTemplateEater implements StreamObserver<Block> { public void onCompleted() {} public void onError(Throwable t) { logger.info("Got error:" + t); last_block_template_time = 0L; } public void onNext(Block b) { logger.info("Got block template: height:" + b.getHeader().getBlockHeight() + " transactions:" + b.getTransactionsCount()); last_block_template = b; last_block_template_time = System.currentTimeMillis(); agent.updateBlockTemplate(b); } } }