Skip to content

Instantly share code, notes, and snippets.

@YoRyan
Last active April 2, 2022 20:36
Show Gist options
  • Save YoRyan/6471e364501e7c62a0c41d92fa6090bc to your computer and use it in GitHub Desktop.
Save YoRyan/6471e364501e7c62a0c41d92fa6090bc to your computer and use it in GitHub Desktop.
Cross-platform controller for NiceHash Excavator for Nvidia (aka, NiceHash 2 for Linux). This is no longer maintained, please see https://github.com/YoRyan/nuxhash
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Cross-platform controller for NiceHash Excavator for Nvidia."""
# Example usage:
# $ excavator -p 3456 &
# $ python3 excavator-driver.py
# History:
# 2017-12-03: initial version
# 2018-01-25: group devices by common algorithm; wait for excavator on startup
__author__ = "Ryan Young"
__email__ = "[email protected]"
__license__ = "public domain"
import json
import logging
import signal
import socket
import sys
import urllib.error
import urllib.request
from time import sleep
WALLET_ADDR = '32RPicPbRK18S2fzY4cEwNUy17iygJyPjF'
WORKER_NAME = 'worker1'
REGION = 'usa' # eu, usa, hk, jp, in, br
EXCAVATOR_ADDRESS = ('127.0.0.1', 3456)
# copy the numbers from excavator-benchmark (test one device at a time with -d <n>)
# convert to the base unit, H/s
# x H/s -> x
# x kH/s -> x*1e3
# x MH/s -> x*1e6
# x GH/s -> x*1e9
BENCHMARKS = {}
# device 0: GTX 1060 6GB
BENCHMARKS[0] = {
'equihash': 325.964731,
'pascal': 687.796633e6,
'decred': 1.896621e9,
'sia': 1.205557e9,
'lbry': 185.736261e6,
'blake2s': 2.767859e9,
'lyra2rev2': 26.157357e6,
'cryptonight': 443.131955,
'daggerhashimoto': 19.965252e6,
'daggerhashimoto_pascal': [8.847941e6, 495.485485e6],
'daggerhashimoto_decred': [19.843944e6, 714.382018e6],
'daggerhashimoto_sia': [19.908869e6, 254.833522e6],
# test manually
'neoscrypt': 732.554438e3,
'nist5': 32.031877e6
}
PROFIT_SWITCH_THRESHOLD = 0.1
UPDATE_INTERVAL = 60
EXCAVATOR_TIMEOUT = 10
NICEHASH_TIMEOUT = 20
### here be dragons
class ExcavatorError(Exception):
pass
class ExcavatorAPIError(ExcavatorError):
"""Exception returned by excavator."""
def __init__(self, response):
self.response = response
self.error = response['error']
def nicehash_multialgo_info():
"""Retrieves pay rates and connection ports for every algorithm from the NiceHash API."""
response = urllib.request.urlopen('https://api.nicehash.com/api?method=simplemultialgo.info',
None, NICEHASH_TIMEOUT)
query = json.loads(response.read().decode('ascii')) #json.load(response)
paying = {}
ports = {}
for algorithm in query['result']['simplemultialgo']:
name = algorithm['name']
paying[name] = float(algorithm['paying'])
ports[name] = int(algorithm['port'])
return paying, ports
def nicehash_mbtc_per_day(device, paying):
"""Calculates the BTC/day amount for every algorithm.
device -- excavator device id for benchmarks
paying -- algorithm pay information from NiceHash
"""
benchmarks = BENCHMARKS[device]
pay = lambda algo, speed: paying[algo]*speed*(24*60*60)*1e-11
def pay_benched(algo):
if '_' in algo:
return sum([pay(multi_algo, benchmarks[algo][i]) for
i, multi_algo in enumerate(algo.split('_'))])
else:
return pay(algo, benchmarks[algo])
return dict([(algo, pay_benched(algo)) for algo in benchmarks.keys()])
def do_excavator_command(method, params):
"""Sends a command to excavator, returns the JSON-encoded response.
method -- name of the command to execute
params -- list of arguments for the command
"""
BUF_SIZE = 1024
command = {
'id': 1,
'method': method,
'params': params
}
s = socket.create_connection(EXCAVATOR_ADDRESS, EXCAVATOR_TIMEOUT)
# send newline-terminated command
s.sendall((json.dumps(command).replace('\n', '\\n') + '\n').encode())
response = ''
while True:
chunk = s.recv(BUF_SIZE).decode()
# excavator responses are newline-terminated too
if '\n' in chunk:
response += chunk[:chunk.index('\n')]
break
else:
response += chunk
s.close()
response_data = json.loads(response)
if response_data['error'] is None:
return response_data
else:
raise ExcavatorAPIError(response_data)
def excavator_algorithm_params(algo, ports):
"""Return the required list of parameters to add an algorithm to excavator.
algo -- the algorithm to run
ports -- algorithm port information from NiceHash
"""
AUTH = '%s.%s:x' % (WALLET_ADDR, WORKER_NAME)
stratum = lambda algo: '%s.%s.nicehash.com:%s' % (algo, REGION, ports[algo])
return [algo] + sum([[stratum(multi_algo), AUTH] for multi_algo in
algo.split('_')], [])
def main():
"""Main program."""
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
level=logging.INFO)
# dict of algorithm name -> (excavator id, [attached devices])
algorithm_status = {}
# dict of device id -> excavator worker id
worker_status = {}
device_algorithm = lambda device: [a for a in algorithm_status.keys() if
device in algorithm_status[a][1]][0]
def dispatch_device(device, algo, ports):
if algo in algorithm_status:
algo_id = algorithm_status[algo][0]
algorithm_status[algo][1].append(device)
else:
response = do_excavator_command('algorithm.add',
excavator_algorithm_params(algo, ports))
algo_id = response['algorithm_id']
algorithm_status[algo] = (algo_id, [device])
response = do_excavator_command('worker.add', [str(algo_id), str(device)])
worker_status[device] = response['worker_id']
def free_device(device):
algo = device_algorithm(device)
algorithm_status[algo][1].remove(device)
worker_id = worker_status[device]
worker_status.pop(device)
do_excavator_command('worker.free', [str(worker_id)])
if len(algorithm_status[algo][1]) == 0: # no more devices attached
algo_id = algorithm_status[algo][0]
algorithm_status.pop(algo)
do_excavator_command('algorithm.remove', [str(algo_id)])
def sigint_handler(signum, frame):
logging.info('cleaning up!')
active_devices = list(worker_status.keys())
for device in active_devices:
free_device(device)
sys.exit(0)
signal.signal(signal.SIGINT, sigint_handler)
def contact_excavator():
try:
do_excavator_command('message', ['%s connected' % sys.argv[0]])
except (socket.timeout, socket.error):
return False
else:
return True
logging.info('connecting to excavator at %s:%d' % EXCAVATOR_ADDRESS)
while not contact_excavator():
sleep(5)
while True:
try:
paying, ports = nicehash_multialgo_info()
except urllib.error.URLError as err:
logging.warning('failed to retrieve NiceHash stats: %s' % err.reason)
except urllib.error.HTTPError as err:
logging.warning('server error retrieving NiceHash stats: %s %s'
% (err.code, err.reason))
except socket.timeout:
logging.warning('failed to retrieve NiceHash stats: timed out')
except (json.decoder.JSONDecodeError, KeyError):
logging.warning('failed to parse NiceHash stats')
else:
for device in BENCHMARKS.keys():
payrates = nicehash_mbtc_per_day(device, paying)
best_algo = max(payrates.keys(), key=lambda algo: payrates[algo])
if device not in worker_status:
logging.info('device %s initial algorithm is %s (%.2f mBTC/day)'
% (device, best_algo, payrates[best_algo]))
dispatch_device(device, best_algo, ports)
else:
current_algo = device_algorithm(device)
if current_algo != best_algo and \
(payrates[current_algo] == 0 or \
payrates[best_algo]/payrates[current_algo] >= 1.0 + PROFIT_SWITCH_THRESHOLD):
logging.info('switching device %s to %s (%.2f mBTC/day)'
% (device, best_algo, payrates[best_algo]))
free_device(device)
dispatch_device(device, best_algo, ports)
sleep(UPDATE_INTERVAL)
if __name__ == '__main__':
main()
@exploitagency
Copy link

exploitagency commented Jan 16, 2018

Modified for use with python2.7

Thanks for your hard work, this seems like it will be really handy!

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

"""Cross-platform controller for NiceHash Excavator for Nvidia."""

# Example usage:
#   $ excavator -p 3456 &
#   $ sleep 5
#   $ python excavator-driver.py

__author__ = "Ryan Young"
__email__ = "[email protected]"
__license__ = "public domain"

import json
import logging
import signal
import socket
import sys
import urllib2
from time import sleep

WALLET_ADDR = '1egacySQXJA8bLHnFhdQQjZBLW1gxSAjc'
WORKER_NAME = 'excavator'
REGION = 'usa' # eu, usa, hk, jp, in, br

EXCAVATOR_ADDRESS = ('127.0.0.1', 3456)

# copy the numbers from excavator-benchmark (test one device at a time with -d <n>)
# convert to the base unit, H/s
#   x H/s   ->  x
#   x kH/s  ->  x*1e3
#   x MH/s  ->  x*1e6
#   x GH/s  ->  x*1e9
BENCHMARKS = {}
# device 0: GTX 1070 8GB
BENCHMARKS[0] = {
    'equihash': 420.171542,
    'pascal': 911.915085e6,
    'decred': 2.478674e9,
    'sia': 1.547728e9,
    'lbry': 240.247888e6,
    'blake2s': 3.613956e9,
    'lyra2rev2': 33.962112e6,
    'cryptonight': 486.620601,
    'daggerhashimoto': 25.786212e6,
    'daggerhashimoto_pascal': [23.547899e6, 251.178424e6],
    'daggerhashimoto_decred': [24.961415e6, 798.765281e6],
    'daggerhashimoto_sia': [19.935504e6, 318.968064e6],
    # test manually
#    'neoscrypt': 851.769067e3
    'neoscrypt': 925.689703e3,
    'nist5': 41.276407e6
    }

PROFIT_SWITCH_THRESHOLD = 0.1
UPDATE_INTERVAL = 60

EXCAVATOR_TIMEOUT = 10
NICEHASH_TIMEOUT = 20

### here be dragons

class ExcavatorError(Exception):
    pass

class ExcavatorAPIError(ExcavatorError):
    """Exception returned by excavator."""
    def __init__(self, response):
        self.response = response
        self.error = response['error']

def nicehash_multialgo_info():
    """Retrieves pay rates and connection ports for every algorithm from the NiceHash API."""
    response = urllib2.urlopen('https://api.nicehash.com/api?method=simplemultialgo.info',
                                      None, NICEHASH_TIMEOUT)
    query = json.loads(response.read().decode('ascii')) #json.load(response)
    paying = {}
    ports = {}
    for algorithm in query['result']['simplemultialgo']:
        name = algorithm['name']
        paying[name] = float(algorithm['paying'])
        ports[name] = int(algorithm['port'])
    return paying, ports

def nicehash_mbtc_per_day(device, paying):
    """Calculates the BTC/day amount for every algorithm.

    device -- excavator device id for benchmarks
    paying -- algorithm pay information from NiceHash
    """

    benchmarks = BENCHMARKS[device]
    pay = lambda algo, speed: paying[algo]*speed*(24*60*60)*1e-11
    pay_benched = lambda algo: pay(algo, benchmarks[algo])

    dual_dp = pay('daggerhashimoto', benchmarks['daggerhashimoto_pascal'][0]) \
                     + pay('pascal', benchmarks['daggerhashimoto_pascal'][1])
    dual_dd = pay('daggerhashimoto', benchmarks['daggerhashimoto_decred'][0]) \
                     + pay('decred', benchmarks['daggerhashimoto_decred'][1])
    dual_ds = pay('daggerhashimoto', benchmarks['daggerhashimoto_sia'][0]) \
                        + pay('sia', benchmarks['daggerhashimoto_sia'][1])
    payrates = {
        'equihash':         pay_benched('equihash'),
        'pascal':           pay_benched('pascal'),
        'decred':           pay_benched('decred'),
        'sia':              pay_benched('sia'),
        'lbry':             pay_benched('lbry'),
        'blake2s':          pay_benched('blake2s'),
        'lyra2rev2':        pay_benched('lyra2rev2'),
        'cryptonight':      pay_benched('cryptonight'),
        'daggerhashimoto':  pay_benched('daggerhashimoto'),
        'neoscrypt':        pay_benched('neoscrypt'),
        'nist5':            pay_benched('nist5'),
        'daggerhashimoto_pascal':   dual_dp,
        'daggerhashimoto_decred':   dual_dd,
        'daggerhashimoto_sia':      dual_ds
        }
    return payrates

def do_excavator_command(method, params):
    """Sends a command to excavator, returns the JSON-encoded response.

    method -- name of the command to execute
    params -- list of arguments for the command
    """

    BUF_SIZE = 1024
    command = {
        'id': 1,
        'method': method,
        'params': params
        }
    s = socket.create_connection(EXCAVATOR_ADDRESS, EXCAVATOR_TIMEOUT)
    # send newline-terminated command
    s.sendall((json.dumps(command).replace('\n', '\\n') + '\n').encode())
    response = ''
    while True:
        chunk = s.recv(BUF_SIZE).decode()
        # excavator responses are newline-terminated too
        if '\n' in chunk:
            response += chunk[:chunk.index('\n')]
            break
        else:
            response += chunk
    s.close()

    response_data = json.loads(response)
    if response_data['error'] is None:
        return response_data
    else:
        raise ExcavatorAPIError(response_data)

def add_excavator_algorithm(algo, device, ports):
    """Runs an algorithm on a device, returns the new excavator algorithm id.

    algo -- the algorithm to run
    device -- excavator device id of the target device
    ports -- algorithm port information from NiceHash
    """

    AUTH = '%s.%s:x' % (WALLET_ADDR, WORKER_NAME)
    stratum = lambda algo: '%s.%s.nicehash.com:%s' % (algo, REGION, ports[algo])
    if algo == 'daggerhashimoto_decred':
        add_params = [algo, stratum('daggerhashimoto'), AUTH,
                            stratum('decred'), AUTH]
    elif algo == 'daggerhashimoto_pascal':
        add_params = [algo, stratum('daggerhashimoto'), AUTH,
                            stratum('pascal'), AUTH]
    elif algo == 'daggerhashimoto_sia':
        add_params = [algo, stratum('daggerhashimoto'), AUTH,
                            stratum('sia'), AUTH]
    else:
        add_params = [algo, stratum(algo), AUTH]

    response = do_excavator_command('algorithm.add', add_params)
    algo_id = response['algorithm_id']

    do_excavator_command('worker.add', [str(algo_id), str(device)])

    return algo_id

def remove_excavator_algorithm(algo_id):
    """Removes an algorithm from excavator and all (one) associated workers.

    algo_id -- excavator algorithm id to remove
    """

    do_excavator_command('algorithm.remove', [str(algo_id)])

def main():
    """Main program."""
    logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',
                        level=logging.INFO)

    device_status = {}

    def sigint_handler(signum, frame):
        logging.info('cleaning up!')

        for device in device_status:
            current_algo_id = device_status[device][1]
            remove_excavator_algorithm(current_algo_id)
        sys.exit(0)
    signal.signal(signal.SIGINT, sigint_handler)

    while True:
        try:
            paying, ports = nicehash_multialgo_info()
        except urllib2.URLError as err:
            logging.warning('failed to retrieve NiceHash stats: %s' % err.reason)
        except urllib2.HTTPError as err:
            logging.warning('server error retrieving NiceHash stats: %s %s'
                            % (err.code, err.reason))
        except socket.timeout:
            logging.warning('failed to retrieve NiceHash stats: timed out')
        except (ValueError, KeyError):
            logging.warning('failed to parse NiceHash stats')
        else:
            for device in BENCHMARKS.keys():
                payrates = nicehash_mbtc_per_day(device, paying)
                best_algo = max(payrates.keys(), key=lambda algo: payrates[algo])

                if device not in device_status:
                    logging.info('device %s initial algorithm is %s' % (device, best_algo))

                    new_algo_id = add_excavator_algorithm(best_algo, device, ports)
                    device_status[device] = (best_algo, new_algo_id)
                else:
                    current_algo = device_status[device][0]
                    current_algo_id = device_status[device][1]

                    if current_algo != best_algo and \
                       (payrates[current_algo] == 0 or \
                        payrates[best_algo]/payrates[current_algo] >= 1.0 + PROFIT_SWITCH_THRESHOLD):
                        logging.info('switching device %s to %s' % (device, best_algo))

                        remove_excavator_algorithm(current_algo_id)
                        new_algo_id = add_excavator_algorithm(best_algo, device, ports)
                        device_status[device] = (best_algo, new_algo_id)
        sleep(UPDATE_INTERVAL)

if __name__ == '__main__':
    main()

@pbutenee
Copy link

Thanks for this script! Haven't tried it on excavator yet, but I adapted it for CPU mining:

https://github.com/pbutenee/docker-cpu-miner

If you want me to credit you, just tell me how and where.

@YoRyan
Copy link
Author

YoRyan commented Jan 26, 2018

No credit needed. Just happy to see people finding my code useful. :-)

@BaGRoS
Copy link

BaGRoS commented Feb 9, 2018

Hello.Can you give more details? How to benchmark it, etc.? He would be happy to start using it under Linux.

@YoRyan
Copy link
Author

YoRyan commented Feb 12, 2018

Hi, sorry for missing your comment. Please look at the documentation and constants in the script's header. It's not meant for anyone without at least some basic Linux and programming experience.

If you're looking for something easier to use, you may be interested in my new project.

@listenlight
Copy link

Thank you so much for this!

@listenlight
Copy link

@YoRyan Is your new projext, nuxhash, working better than the scripts here?

@YoRyan
Copy link
Author

YoRyan commented Feb 16, 2018

Yes, nuxhash currently does everything this script does. The killer improvement is an automatic benchmarking interface. However, it hasn't yet been tested for multiple graphics cards and other corner cases. Please feel 100% free to try it out and open some issues.

@yeguang-xue
Copy link

Thanks for the nice script! Adapted to support pools outside NiceHash, like MiningPoolHub, based on real-time network difficulty and value. See this gist.

@YoRyan
Copy link
Author

YoRyan commented Apr 9, 2018

Excavator has a new API now, this script no longer works and I will not be maintaining it. Please see my new project: https://github.com/YoRyan/nuxhash

@igorvoltaic
Copy link

Yet another adaptation of this script which uses just any miner out there: nicewitch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment