Last active
          April 9, 2018 07:29 
        
      - 
      
- 
        Save tweinfeld/b28e9b8b22b4a17bdc96e284eecb92f0 to your computer and use it in GitHub Desktop. 
    A short Stratum client for mining BTCs
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | const | |
| _ = require('lodash'), | |
| kefir = require('kefir'), | |
| net = require('net'), | |
| https = require('https'), | |
| bigInt = require('big-integer'), | |
| { createHash } = require('crypto'); | |
| const | |
| LOGGLY_API_KEY = "b8d2377d-63d8-4f4c-ae07-fb05bb999b3c", | |
| TERMINATION_TIMER = 1000 * 60, | |
| DIFFICULTY_1 = bigInt('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16), | |
| CONNECTION_RETRIAL_COUNT = 20, | |
| POOLS = _.shuffle([ | |
| { | |
| hostname: "us-east.stratum.slushpool.com", | |
| port: 3333, | |
| user: "jeffrycoal" | |
| }, { | |
| hostname: "eu.stratum.slushpool.com", | |
| port: 3333, | |
| user: "jeffrycoal" | |
| }, { | |
| hostname: "sg.stratum.slushpool.com", | |
| port: 3333, | |
| user: "jeffrycoal" | |
| }, { | |
| hostname: "jp.stratum.slushpool.com", | |
| port: 3333, | |
| user: "jeffrycoal1" | |
| } | |
| ]), | |
| WORKER_ID = Math.random().toString(36).split('').slice(2).join(''); | |
| let log = (function(){ | |
| const | |
| localDispatcher = (level, ...message)=> console[{ "trace": "log" }[level] || level](...message), | |
| logglyDispatcher = (level, ...message)=> https.get(`https://logs-01.loggly.com/inputs/${LOGGLY_API_KEY}.gif?source=${WORKER_ID}&type=${level}&data=${encodeURIComponent(message.join(' '))}`), | |
| dispatcherMap = { | |
| "info": [localDispatcher, logglyDispatcher], | |
| "warn": [localDispatcher, logglyDispatcher], | |
| "trace": [localDispatcher] | |
| }; | |
| return _.keys(dispatcherMap).map((name)=>({ [name]: (...args)=> dispatcherMap[name].forEach((dispatcher)=> dispatcher(name, ...args)) })).reduce(_.assign); | |
| })(); | |
| log.info('Started'); | |
| ["SIGTERM", "SIGINT"].forEach((signal)=> process.on(signal, ()=> { log.warn('Received request for termination'); process.exit(0); })); | |
| const | |
| dsha256 = (function(sha256){ return _.flow(sha256, sha256); })((input)=> createHash('sha256').update(input).digest()), | |
| iteratorFactory = (difficulty, merkleTree, extraNonceOne, extraNonce2Size, coinBaseA, coinBaseB, previousBlock, blockVersion, nBits, nTime)=> { | |
| const target = DIFFICULTY_1.divide(difficulty); | |
| const createCoinBaseHashMerkle = _.memoize((extraNonce2)=> { | |
| let coinBase = [coinBaseA, extraNonceOne, _.padStart(extraNonce2.toString(16), extraNonce2Size * 2, '0'), coinBaseB].join(''), | |
| coinBaseHash = dsha256(Buffer.from(coinBase, 'hex')).toString('hex'); | |
| return merkleTree.reduce((a, c)=> dsha256(Buffer.from([a, c].join(''), 'hex')).toString('hex'), coinBaseHash); | |
| }); | |
| return (start, iterations)=> { | |
| let index = bigInt(start), | |
| end = bigInt(start).add(bigInt(iterations)); | |
| log.trace(`Sweeping range ${[index, end].map((n)=> n.toString(16)).join(' -> ') }`); | |
| while(index.lesser(end)){ | |
| let nonce2 = index.divide(0xffffffff), | |
| nonce = index.mod(0xffffffff), | |
| header = [ | |
| blockVersion, | |
| previousBlock, | |
| createCoinBaseHashMerkle(nonce2), | |
| nTime, | |
| nBits, | |
| _.padStart(nonce.toString(16), 8, '0') | |
| ].join(''); | |
| let headerHash = dsha256(Buffer.from(header, 'hex')).toString('hex'); | |
| if(bigInt(_.chunk(headerHash,2).map((x)=> x.join('')).reverse().join(''), 16).lesser(target)){ | |
| return { | |
| nonce: _.padStart(nonce.toString(16), 8, '0'), | |
| nonce2: _.padStart(nonce2.toString(16), extraNonce2Size * 2, '0'), | |
| ntime: nTime | |
| }; | |
| } | |
| index = index.add(1); | |
| } | |
| }; | |
| }; | |
| kefir.repeat((trialCount)=> { | |
| let pool = (function(p){ p.push(...p.splice(0, 1)); return p[0]; })(POOLS), | |
| client = net.createConnection(..._.at(pool, ["port", "hostname"])); | |
| return trialCount < CONNECTION_RETRIAL_COUNT && kefir | |
| .fromEvents(client, 'connect') | |
| .takeUntilBy(kefir.fromEvents(client, 'close').take(1)) | |
| .flatMap(()=> { | |
| log.info(`Connected to "${pool["hostname"]}"`); | |
| let serverStream = kefir | |
| .fromEvents(client, 'data', (chunk)=> chunk.toString('utf8')) | |
| .flatMap((function(strBuffer){ | |
| return (txt)=> { | |
| strBuffer += txt; | |
| return kefir.sequentially(0, | |
| (function(){ | |
| let arr = strBuffer.split('\n'); | |
| strBuffer = arr.splice(-1)[0]; | |
| return arr; | |
| })() | |
| ); | |
| }; | |
| })("")) | |
| .map(JSON.parse) | |
| .takeUntilBy(kefir.fromEvents(client, 'end')) | |
| .onValue(_.noop); | |
| const invoke = (function(id){ | |
| return (methodName, ...args)=> { | |
| let commandId = id++; | |
| client.write([JSON.stringify({ id: commandId, method: methodName, params: args }), "\n"].join('')); | |
| return serverStream | |
| .filter(_.matchesProperty('id', commandId)) | |
| .take(1) | |
| .flatMap(({ error, result })=> (error || !result) ? kefir.constantError(error) : kefir.constant(result)) | |
| .takeErrors(1) | |
| .toPromise(); | |
| }; | |
| })(0); | |
| let | |
| workerName = [pool["user"], WORKER_ID].join('.'), | |
| initializationProperty = kefir | |
| .concat([ | |
| kefir.fromPromise(invoke('mining.subscribe', 'fastmine.ch')), | |
| kefir | |
| .fromPromise(invoke('mining.authorize', workerName)) | |
| .onValue(()=> log.info(`Authenticated as "${workerName}"`)) | |
| .ignoreValues() | |
| .mapErrors(()=> `Authorization failed for "${workerName}" ${pool["hostname"]}`) | |
| ]) | |
| .toProperty(); | |
| let | |
| extraNonceOneProperty = initializationProperty.map(_.property('1')), | |
| extraNonceTwoSizeProperty = initializationProperty.map(_.property('2')), | |
| jobProperty = serverStream.filter(_.matches({ "method": "mining.notify", id: null })).map(_.property('params')).toProperty(), | |
| difficultyProperty = serverStream.filter(_.matches({ "method": "mining.set_difficulty", id: null })).map(_.flow(_.property('params'), _.first)).toProperty(), | |
| jobId = jobProperty.map(_.property('0')), | |
| previousBlock = jobProperty.map(_.property('1')), | |
| coinBaseA = jobProperty.map(_.property('2')), | |
| coinBaseB = jobProperty.map(_.property('3')), | |
| merkleTree = jobProperty.map(_.property('4')), | |
| blockVersion = jobProperty.map(_.property('5')), | |
| nBits = jobProperty.map(_.property('6')), | |
| nTime = jobProperty.map(_.property('7')), | |
| clearFlag = jobProperty.map(_.property('8')).skipDuplicates(); | |
| return kefir | |
| .combine([difficultyProperty, jobId, nTime, nBits, previousBlock, merkleTree, coinBaseA, coinBaseB, blockVersion, extraNonceTwoSizeProperty, extraNonceOneProperty, clearFlag]) | |
| .debounce() | |
| .flatMapLatest(([difficulty, jobId, nTime, nBits, previousBlock, merkleTree, coinBaseA, coinBaseB, blockVersion, extraNonce2Size, extraNonceOne])=> { | |
| log.trace(`New Job set (${jobId}) with difficulty ${difficulty}`); | |
| let hashIterator = iteratorFactory(difficulty, merkleTree, extraNonceOne, extraNonce2Size, coinBaseA, coinBaseB, previousBlock, blockVersion, nBits, nTime), | |
| domain = bigInt(0xffffffff).multiply(_.random(0xffffffff)); | |
| return kefir | |
| .sequentially(0, _.range(0, 0xffffffff, 0xfff)) | |
| .map((sector)=> hashIterator(domain.add(sector), 0xfff)) | |
| .filter(Boolean) | |
| .flatMap(({ nonce, nonce2, ntime })=> | |
| kefir | |
| .fromPromise(invoke('mining.submit', workerName, jobId, nonce2, ntime, nonce)) | |
| .map(()=> `PoW submitted ${jobId} => ${[nonce, nonce2, ntime].join('/')}`) | |
| ); | |
| }); | |
| }) | |
| .takeErrors(1) | |
| .beforeEnd(_.constant('Terminating')) | |
| }) | |
| .onValue(log.info) | |
| .onError(log.warn); | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment