pyminer: Fix memory leak, refactor to be more pythonic, maintainable, and possibly faster

Pull #4483.

Note that pyminer is not currently functional due to the removal of
getwork in cf0c47b.
This commit is contained in:
Clinton Christian 2014-07-07 23:22:23 -04:00 committed by Wladimir J. van der Laan
parent 93659379bd
commit 9192ca9e33

View File

@ -5,248 +5,265 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
# #
import time
import json
import pprint
import hashlib
import struct
import re
import base64
import httplib
import sys import sys
from multiprocessing import Process from multiprocessing import Process
import time
import struct
import hashlib
import base64
import re
import httplib
import json
ERR_SLEEP = 15 ERR_SLEEP = 15
MAX_NONCE = 1000000L MAX_NONCE = 1000000L
settings = {} settings = {}
pp = pprint.PrettyPrinter(indent=4)
class BitcoinRPC: class BitcoinRPC:
OBJID = 1 object_id = 1
def __init__(self, host, port, username, password): def __init__(self, host, port, username, password):
authpair = "%s:%s" % (username, password) authpair = "{0}:{1}".format(username, password)
self.authhdr = "Basic %s" % (base64.b64encode(authpair)) self.authhdr = "Basic {0}".format(base64.b64encode(authpair))
self.conn = httplib.HTTPConnection(host, port, False, 30) self.conn = httplib.HTTPConnection(host, port, strict=False, timeout=30)
def rpc(self, method, params=None):
self.OBJID += 1
obj = { 'version' : '1.1',
'method' : method,
'id' : self.OBJID }
if params is None:
obj['params'] = []
else:
obj['params'] = params
self.conn.request('POST', '/', json.dumps(obj),
{ 'Authorization' : self.authhdr,
'Content-type' : 'application/json' })
resp = self.conn.getresponse() def rpc(self, method, params=None):
if resp is None: self.object_id += 1
print "JSON-RPC: no response" obj = {'version' : '1.1',
return None 'method' : method,
'id' : self.object_id,
'params' : params or []}
body = resp.read() self.conn.request('POST', '/', json.dumps(obj),
resp_obj = json.loads(body) { 'Authorization' : self.authhdr,
if resp_obj is None: 'Content-type' : 'application/json' })
print "JSON-RPC: cannot JSON-decode body"
return None
if 'error' in resp_obj and resp_obj['error'] != None:
return resp_obj['error']
if 'result' not in resp_obj:
print "JSON-RPC: no result in object"
return None
return resp_obj['result'] resp = self.conn.getresponse()
def getblockcount(self):
return self.rpc('getblockcount') if resp is None:
def getwork(self, data=None): print("JSON-RPC: no response")
return self.rpc('getwork', data) return None
body = resp.read()
resp_obj = json.loads(body)
if resp_obj is None:
print("JSON-RPC: cannot JSON-decode body")
return None
if 'error' in resp_obj and resp_obj['error'] != None:
return resp_obj['error']
if 'result' not in resp_obj:
print("JSON-RPC: no result in object")
return None
return resp_obj['result']
def getblockcount(self):
return self.rpc('getblockcount')
def getwork(self, data=None):
return self.rpc('getwork', data)
def uint32(x): def uint32(x):
return x & 0xffffffffL return x & 0xffffffffL
def bytereverse(x): def bytereverse(x):
return uint32(( ((x) << 24) | (((x) << 8) & 0x00ff0000) | return uint32(( ((x) << 24) | (((x) << 8) & 0x00ff0000) |
(((x) >> 8) & 0x0000ff00) | ((x) >> 24) )) (((x) >> 8) & 0x0000ff00) | ((x) >> 24) ))
def bufreverse(in_buf): def bufreverse(in_buf):
out_words = [] out_words = []
for i in range(0, len(in_buf), 4):
word = struct.unpack('@I', in_buf[i:i+4])[0] for i in range(0, len(in_buf), 4):
out_words.append(struct.pack('@I', bytereverse(word))) word = struct.unpack('@I', in_buf[i:i+4])[0]
return ''.join(out_words) out_words.append(struct.pack('@I', bytereverse(word)))
return ''.join(out_words)
def wordreverse(in_buf): def wordreverse(in_buf):
out_words = [] out_words = []
for i in range(0, len(in_buf), 4):
out_words.append(in_buf[i:i+4]) for i in range(0, len(in_buf), 4):
out_words.reverse() out_words.append(in_buf[i:i+4])
return ''.join(out_words)
out_words.reverse()
return ''.join(out_words)
class Miner: class Miner:
def __init__(self, id): def __init__(self, id):
self.id = id self.id = id
self.max_nonce = MAX_NONCE self.max_nonce = MAX_NONCE
def work(self, datastr, targetstr): def work(self, datastr, targetstr):
# decode work data hex string to binary # decode work data hex string to binary
static_data = datastr.decode('hex') static_data = datastr.decode('hex')
static_data = bufreverse(static_data) static_data = bufreverse(static_data)
# the first 76b of 80b do not change # the first 76b of 80b do not change
blk_hdr = static_data[:76] blk_hdr = static_data[:76]
# decode 256-bit target value # decode 256-bit target value
targetbin = targetstr.decode('hex') targetbin = targetstr.decode('hex')
targetbin = targetbin[::-1] # byte-swap and dword-swap targetbin = targetbin[::-1] # byte-swap and dword-swap
targetbin_str = targetbin.encode('hex') targetbin_str = targetbin.encode('hex')
target = long(targetbin_str, 16) target = long(targetbin_str, 16)
# pre-hash first 76b of block header # pre-hash first 76b of block header
static_hash = hashlib.sha256() static_hash = hashlib.sha256()
static_hash.update(blk_hdr) static_hash.update(blk_hdr)
for nonce in xrange(self.max_nonce): for nonce in xrange(self.max_nonce):
# encode 32-bit nonce value # encode 32-bit nonce value
nonce_bin = struct.pack("<I", nonce) nonce_bin = struct.pack("<I", nonce)
# hash final 4b, the nonce value # hash final 4b, the nonce value
hash1_o = static_hash.copy() hash1_o = static_hash.copy()
hash1_o.update(nonce_bin) hash1_o.update(nonce_bin)
hash1 = hash1_o.digest() hash1 = hash1_o.digest()
# sha256 hash of sha256 hash # sha256 hash of sha256 hash
hash_o = hashlib.sha256() hash_o = hashlib.sha256()
hash_o.update(hash1) hash_o.update(hash1)
hash = hash_o.digest() hash = hash_o.digest()
# quick test for winning solution: high 32 bits zero? # quick test for winning solution: high 32 bits zero?
if hash[-4:] != '\0\0\0\0': if hash[-4:] != '\0\0\0\0':
continue continue
# convert binary hash to 256-bit Python long # convert binary hash to 256-bit Python long
hash = bufreverse(hash) hash = bufreverse(hash)
hash = wordreverse(hash) hash = wordreverse(hash)
hash_str = hash.encode('hex') hash_str = hash.encode('hex')
l = long(hash_str, 16) long_hash = long(hash_str, 16)
# proof-of-work test: hash < target # proof-of-work test: hash < target
if l < target: if long_hash < target:
print time.asctime(), "PROOF-OF-WORK found: %064x" % (l,) print(time.asctime(), "PROOF-OF-WORK found: "
return (nonce + 1, nonce_bin) "{0:064x}".format(long_hash))
else: return (nonce + 1, nonce_bin)
print time.asctime(), "PROOF-OF-WORK false positive %064x" % (l,) else:
# return (nonce + 1, nonce_bin) print(time.asctime(), "PROOF-OF-WORK false"
"positive {0:064x}".format(long_hash))
return (nonce + 1, None) return (nonce + 1, None)
def submit_work(self, rpc, original_data, nonce_bin): def submit_work(self, rpc, original_data, nonce_bin):
nonce_bin = bufreverse(nonce_bin) nonce_bin = bufreverse(nonce_bin)
nonce = nonce_bin.encode('hex') nonce = nonce_bin.encode('hex')
solution = original_data[:152] + nonce + original_data[160:256] solution = original_data[:152] + nonce + original_data[160:256]
param_arr = [ solution ] param_arr = [ solution ]
result = rpc.getwork(param_arr) result = rpc.getwork(param_arr)
print time.asctime(), "--> Upstream RPC result:", result
def iterate(self, rpc): print(time.asctime(), "--> Upstream RPC result:", result)
work = rpc.getwork()
if work is None:
time.sleep(ERR_SLEEP)
return
if 'data' not in work or 'target' not in work:
time.sleep(ERR_SLEEP)
return
time_start = time.time() def iterate(self, rpc):
work = rpc.getwork()
(hashes_done, nonce_bin) = self.work(work['data'], if work is None:
work['target']) time.sleep(ERR_SLEEP)
return
time_end = time.time() if 'data' not in work or 'target' not in work:
time_diff = time_end - time_start time.sleep(ERR_SLEEP)
return
self.max_nonce = long( time_start = time.time()
(hashes_done * settings['scantime']) / time_diff)
if self.max_nonce > 0xfffffffaL:
self.max_nonce = 0xfffffffaL
if settings['hashmeter']: (hashes_done, nonce_bin) = self.work(work['data'],
print "HashMeter(%d): %d hashes, %.2f Khash/sec" % ( work['target'])
self.id, hashes_done,
(hashes_done / 1000.0) / time_diff)
if nonce_bin is not None: time_end = time.time()
self.submit_work(rpc, work['data'], nonce_bin) time_diff = time_end - time_start
def loop(self): self.max_nonce = long(
rpc = BitcoinRPC(settings['host'], settings['port'], (hashes_done * settings['scantime']) / time_diff)
settings['rpcuser'], settings['rpcpass'])
if rpc is None: if self.max_nonce > 0xfffffffaL:
return self.max_nonce = 0xfffffffaL
if settings['hashmeter']:
print("HashMeter({:d}): {:d} hashes, {:.2f} Khash/sec".format(
self.id, hashes_done, (hashes_done / 1000.0) / time_diff))
if nonce_bin is not None:
self.submit_work(rpc, work['data'], nonce_bin)
def loop(self):
rpc = BitcoinRPC(settings['host'], settings['port'],
settings['rpcuser'], settings['rpcpass'])
if rpc is not None:
while True:
self.iterate(rpc)
self.conn.close()
while True:
self.iterate(rpc)
def miner_thread(id): def miner_thread(id):
miner = Miner(id) miner = Miner(id)
miner.loop() miner.loop()
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) != 2: if len(sys.argv) != 2:
print "Usage: pyminer.py CONFIG-FILE" print("Usage: pyminer.py CONFIG-FILE")
sys.exit(1) sys.exit(1)
f = open(sys.argv[1]) with open(sys.argv[1]) as f:
for line in f:
# skip comment lines
m = re.search('^\s*#', line)
if m:
continue
# parse key=value lines for line in f:
m = re.search('^(\w+)\s*=\s*(\S.*)$', line) # skip comment lines
if m is None: m = re.search('^\s*#', line)
continue if m:
settings[m.group(1)] = m.group(2) continue
f.close()
if 'host' not in settings: # parse key=value lines
settings['host'] = '127.0.0.1' m = re.search('^(\w+)\s*=\s*(\S.*)$', line)
if 'port' not in settings: if m is None:
settings['port'] = 8332 continue
if 'threads' not in settings:
settings['threads'] = 1
if 'hashmeter' not in settings:
settings['hashmeter'] = 0
if 'scantime' not in settings:
settings['scantime'] = 30L
if 'rpcuser' not in settings or 'rpcpass' not in settings:
print "Missing username and/or password in cfg file"
sys.exit(1)
settings['port'] = int(settings['port']) settings[m.group(1)] = m.group(2)
settings['threads'] = int(settings['threads'])
settings['hashmeter'] = int(settings['hashmeter'])
settings['scantime'] = long(settings['scantime'])
thr_list = [] settings.setdefault('host', '127.0.0.1')
for thr_id in range(settings['threads']): settings.setdefault('port', 8332)
p = Process(target=miner_thread, args=(thr_id,)) settings.setdefault('threads', 1)
p.start() settings.setdefault('hashmeter', 0)
thr_list.append(p) settings.setdefault('scantime', 30L)
time.sleep(1) # stagger threads
print settings['threads'], "mining threads started" if 'rpcuser' not in settings or 'rpcpass' not in settings:
print("Missing username and/or password in cfg file")
sys.exit(1)
print time.asctime(), "Miner Starts - %s:%s" % (settings['host'], settings['port']) settings['port'] = int(settings['port'])
try: settings['threads'] = int(settings['threads'])
for thr_proc in thr_list: settings['hashmeter'] = int(settings['hashmeter'])
thr_proc.join() settings['scantime'] = long(settings['scantime'])
except KeyboardInterrupt:
pass
print time.asctime(), "Miner Stops - %s:%s" % (settings['host'], settings['port'])
thread_list = []
for thread_id in range(settings['threads']):
p = Process(target=miner_thread, args=(thread_id,))
p.start()
thread_list.append(p)
time.sleep(1) # stagger threads
print(settings['threads'], "mining threads started")
print(time.asctime(), "Miner Starts - {0}:{1}".format(settings['host'],
settings['port']))
try:
for thread_process in thread_list:
thread_process.join()
except KeyboardInterrupt:
pass
print(time.asctime(), "Miner Stops - {0}:{1}".format(settings['host'],
settings['port']))