Add reference python miner, in contrib/pyminer/
authorJeff Garzik <jeff@garzik.org>
Wed, 31 Aug 2011 16:27:41 +0000 (12:27 -0400)
committerJeff Garzik <jgarzik@redhat.com>
Wed, 31 Aug 2011 16:27:41 +0000 (12:27 -0400)
contrib/pyminer/README [new file with mode: 0644]
contrib/pyminer/example-config.cfg [new file with mode: 0644]
contrib/pyminer/pyminer.py [new file with mode: 0755]

diff --git a/contrib/pyminer/README b/contrib/pyminer/README
new file mode 100644 (file)
index 0000000..d159657
--- /dev/null
@@ -0,0 +1,6 @@
+
+This is a 'getwork' CPU mining client for bitcoin.
+
+It is pure-python, and therefore very, very slow.  The purpose is to
+provide a reference implementation of a miner, for study.
+
diff --git a/contrib/pyminer/example-config.cfg b/contrib/pyminer/example-config.cfg
new file mode 100644 (file)
index 0000000..103e7c1
--- /dev/null
@@ -0,0 +1,32 @@
+
+#
+# RPC login details
+#
+host=127.0.0.1
+port=8332
+
+rpcuser=myusername
+rpcpass=mypass
+
+
+#
+# mining details
+#
+
+threads=4
+
+# periodic rate for requesting new work, if solution not found
+scantime=60
+
+
+#
+# misc.
+#
+
+# not really used right now
+logdir=/tmp/pyminer
+
+# set to 1, to enable hashmeter output
+hashmeter=0
+
+
diff --git a/contrib/pyminer/pyminer.py b/contrib/pyminer/pyminer.py
new file mode 100755 (executable)
index 0000000..2887aba
--- /dev/null
@@ -0,0 +1,252 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2011 The Bitcoin developers
+# Distributed under the MIT/X11 software license, see the accompanying
+# file license.txt 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
+from multiprocessing import Process
+
+ERR_SLEEP = 15
+MAX_NONCE = 1000000L
+
+settings = {}
+pp = pprint.PrettyPrinter(indent=4)
+
+class BitcoinRPC:
+       OBJID = 1
+
+       def __init__(self, host, port, username, password):
+               authpair = "%s:%s" % (username, password)
+               self.authhdr = "Basic %s" % (base64.b64encode(authpair))
+               self.conn = httplib.HTTPConnection(host, port, False, 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()
+               if resp is None:
+                       print "JSON-RPC: no response"
+                       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):
+       return x & 0xffffffffL
+
+def bytereverse(x):
+       return uint32(( ((x) << 24) | (((x) << 8) & 0x00ff0000) |
+                       (((x) >> 8) & 0x0000ff00) | ((x) >> 24) ))
+
+def bufreverse(in_buf):
+       out_words = []
+       for i in range(0, len(in_buf), 4):
+               word = struct.unpack('@I', in_buf[i:i+4])[0]
+               out_words.append(struct.pack('@I', bytereverse(word)))
+       return ''.join(out_words)
+
+def wordreverse(in_buf):
+       out_words = []
+       for i in range(0, len(in_buf), 4):
+               out_words.append(in_buf[i:i+4])
+       out_words.reverse()
+       return ''.join(out_words)
+
+class Miner:
+       def __init__(self, id):
+               self.id = id
+               self.max_nonce = MAX_NONCE
+
+       def work(self, datastr, targetstr):
+               # decode work data hex string to binary
+               static_data = datastr.decode('hex')
+               static_data = bufreverse(static_data)
+
+               # the first 76b of 80b do not change
+               blk_hdr = static_data[:76]
+
+               # decode 256-bit target value
+               targetbin = targetstr.decode('hex')
+               targetbin = targetbin[::-1]     # byte-swap and dword-swap
+               targetbin_str = targetbin.encode('hex')
+               target = long(targetbin_str, 16)
+
+               # pre-hash first 76b of block header
+               static_hash = hashlib.sha256()
+               static_hash.update(blk_hdr)
+
+               for nonce in xrange(self.max_nonce):
+
+                       # encode 32-bit nonce value
+                       nonce_bin = struct.pack("<I", nonce)
+
+                       # hash final 4b, the nonce value
+                       hash1_o = static_hash.copy()
+                       hash1_o.update(nonce_bin)
+                       hash1 = hash1_o.digest()
+
+                       # sha256 hash of sha256 hash
+                       hash_o = hashlib.sha256()
+                       hash_o.update(hash1)
+                       hash = hash_o.digest()
+
+                       # quick test for winning solution: high 32 bits zero?
+                       if hash[-4:] != '\0\0\0\0':
+                               continue
+
+                       # convert binary hash to 256-bit Python long
+                       hash = bufreverse(hash)
+                       hash = wordreverse(hash)
+
+                       hash_str = hash.encode('hex')
+                       l = long(hash_str, 16)
+
+                       # proof-of-work test:  hash < target
+                       if l < target:
+                               print time.asctime(), "PROOF-OF-WORK found: %064x" % (l,)
+                               return (nonce + 1, nonce_bin)
+                       else:
+                               print time.asctime(), "PROOF-OF-WORK false positive %064x" % (l,)
+#                              return (nonce + 1, nonce_bin)
+
+               return (nonce + 1, None)
+
+       def submit_work(self, rpc, original_data, nonce_bin):
+               nonce_bin = bufreverse(nonce_bin)
+               nonce = nonce_bin.encode('hex')
+               solution = original_data[:152] + nonce + original_data[160:256]
+               param_arr = [ solution ]
+               result = rpc.getwork(param_arr)
+               print time.asctime(), "--> Upstream RPC result:", result
+
+       def iterate(self, rpc):
+               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()
+
+               (hashes_done, nonce_bin) = self.work(work['data'],
+                                                    work['target'])
+
+               time_end = time.time()
+               time_diff = time_end - time_start
+
+               self.max_nonce = long(
+                       (hashes_done * settings['scantime']) / time_diff)
+               if self.max_nonce > 0xfffffffaL:
+                       self.max_nonce = 0xfffffffaL
+
+               if settings['hashmeter']:
+                       print "HashMeter(%d): %d hashes, %.2f Khash/sec" % (
+                             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 None:
+                       return
+
+               while True:
+                       self.iterate(rpc)
+
+def miner_thread(id):
+       miner = Miner(id)
+       miner.loop()
+
+if __name__ == '__main__':
+       if len(sys.argv) != 2:
+               print "Usage: pyminer.py CONFIG-FILE"
+               sys.exit(1)
+
+       f = open(sys.argv[1])
+       for line in f:
+               # skip comment lines
+               m = re.search('^\s*#', line)
+               if m:
+                       continue
+
+               # parse key=value lines
+               m = re.search('^(\w+)\s*=\s*(\S.*)$', line)
+               if m is None:
+                       continue
+               settings[m.group(1)] = m.group(2)
+       f.close()
+
+       if 'host' not in settings:
+               settings['host'] = '127.0.0.1'
+       if 'port' not in settings:
+               settings['port'] = 8332
+       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['threads'] = int(settings['threads'])
+       settings['hashmeter'] = int(settings['hashmeter'])
+       settings['scantime'] = long(settings['scantime'])
+
+       thr_list = []
+       for thr_id in range(settings['threads']):
+               p = Process(target=miner_thread, args=(thr_id,))
+               p.start()
+               thr_list.append(p)
+               time.sleep(1)                   # stagger threads
+
+       print settings['threads'], "mining threads started"
+
+       print time.asctime(), "Miner Starts - %s:%s" % (settings['host'], settings['port'])
+       try:
+               for thr_proc in thr_list:
+                       thr_proc.join()
+       except KeyboardInterrupt:
+               pass
+       print time.asctime(), "Miner Stops - %s:%s" % (settings['host'], settings['port'])
+