Skip to content

Instantly share code, notes, and snippets.

@suprnrdy
Forked from YoRyan/excavator-driver.py
Created January 5, 2018 19:19
Show Gist options
  • Save suprnrdy/90ed47b91ba259b8d6ee3453fd309a01 to your computer and use it in GitHub Desktop.
Save suprnrdy/90ed47b91ba259b8d6ee3453fd309a01 to your computer and use it in GitHub Desktop.
Cross-platform controller for NiceHash Excavator for Nvidia.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Cross-platform controller for NiceHash Excavator for Nvidia."""
# Example usage:
# $ excavator -p 3456 &
# $ sleep 5
# $ python3 excavator-driver.py
__author__ = "Ryan Young"
__email__ = "rayoung@utexas.edu"
__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 = '3DzJYbtHt9QdZzQiDnq5tZD8VsDuwRnbdJ'
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': 300.363149,
'pascal': 673.490293e6,
'decred': 1.734736e9,
'sia': 1.070054e9,
'lbry': 179.286648e6,
'blake2s': 2.509039e9,
'lyra2rev2': 25.908014e6,
'cryptonight': 435.366891,
'daggerhashimoto': 22.009473e6,
'daggerhashimoto_pascal': [8.298873e6, 464.739199e6],
'daggerhashimoto_decred': [20.258170e6, 729.296593e6],
'daggerhashimoto_sia': [21.359257e6, 273.398483e6],
# test manually
'neoscrypt': 618.769067e3
}
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
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'),
'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 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 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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment