/ src / proofofwork.py
proofofwork.py
  1  """
  2  Proof of work calculation
  3  """
  4  # pylint: disable=import-outside-toplevel
  5  
  6  import ctypes
  7  import hashlib
  8  import os
  9  import subprocess  # nosec B404
 10  import sys
 11  import tempfile
 12  import time
 13  from struct import pack, unpack
 14  
 15  from . import highlevelcrypto
 16  from . import openclpow
 17  from . import paths
 18  from . import queues
 19  from . import state
 20  from .bmconfigparser import config
 21  from .debug import logger
 22  from .defaults import (
 23      networkDefaultProofOfWorkNonceTrialsPerByte,
 24      networkDefaultPayloadLengthExtraBytes)
 25  from .tr import _translate
 26  
 27  
 28  bitmsglib = 'bitmsghash.so'
 29  bmpow = None
 30  
 31  
 32  class LogOutput(object):  # pylint: disable=too-few-public-methods
 33      """
 34      A context manager that block stdout for its scope
 35      and appends it's content to log before exit. Usage::
 36  
 37      with LogOutput():
 38          os.system('ls -l')
 39  
 40      https://stackoverflow.com/questions/5081657
 41      """
 42  
 43      def __init__(self, prefix='PoW'):
 44          self.prefix = prefix
 45          try:
 46              sys.stdout.flush()
 47              self._stdout = sys.stdout
 48              self._stdout_fno = os.dup(sys.stdout.fileno())
 49          except AttributeError:
 50              # NullWriter instance has no attribute 'fileno' on Windows
 51              self._stdout = None
 52          else:
 53              self._dst, self._filepath = tempfile.mkstemp()
 54  
 55      def __enter__(self):
 56          if not self._stdout:
 57              return
 58          stdout = os.dup(1)
 59          os.dup2(self._dst, 1)
 60          os.close(self._dst)
 61          sys.stdout = os.fdopen(stdout, 'w')
 62  
 63      def __exit__(self, exc_type, exc_val, exc_tb):
 64          if not self._stdout:
 65              return
 66          sys.stdout.close()
 67          sys.stdout = self._stdout
 68          sys.stdout.flush()
 69          os.dup2(self._stdout_fno, 1)
 70  
 71          with open(self._filepath) as out:
 72              for line in out:
 73                  logger.info('%s: %s', self.prefix, line)
 74          os.remove(self._filepath)
 75  
 76  
 77  def _set_idle():
 78      if 'linux' in sys.platform:
 79          os.nice(20)
 80      else:
 81          try:
 82              # pylint: disable=no-member,import-error
 83              sys.getwindowsversion()
 84              import win32api
 85              import win32process
 86              import win32con
 87  
 88              handle = win32api.OpenProcess(
 89                  win32con.PROCESS_ALL_ACCESS, True,
 90                  win32api.GetCurrentProcessId())
 91              win32process.SetPriorityClass(
 92                  handle, win32process.IDLE_PRIORITY_CLASS)
 93          except:  # nosec B110 # noqa:E722 pylint:disable=bare-except
 94              # Windows 64-bit
 95              pass
 96  
 97  
 98  def trial_value(nonce, initialHash):
 99      """Calculate PoW trial value"""
100      trialValue, = unpack(
101          '>Q', highlevelcrypto.double_sha512(
102              pack('>Q', nonce) + initialHash)[0:8])
103      return trialValue
104  
105  
106  def _pool_worker(nonce, initialHash, target, pool_size):
107      _set_idle()
108      trialValue = float('inf')
109      while trialValue > target:
110          nonce += pool_size
111          trialValue = trial_value(nonce, initialHash)
112      return trialValue, nonce
113  
114  
115  def _doSafePoW(target, initialHash):
116      logger.debug('Safe PoW start')
117      nonce = 0
118      trialValue = float('inf')
119      while trialValue > target and state.shutdown == 0:
120          nonce += 1
121          trialValue = trial_value(nonce, initialHash)
122      if state.shutdown != 0:
123          raise StopIteration("Interrupted")
124      logger.debug('Safe PoW done')
125      return trialValue, nonce
126  
127  
128  def _doFastPoW(target, initialHash):
129      # pylint:disable=bare-except
130      logger.debug('Fast PoW start')
131      from multiprocessing import Pool, cpu_count
132      try:
133          pool_size = cpu_count()
134      except:  # noqa:E722
135          pool_size = 4
136      maxCores = config.safeGetInt('bitmessagesettings', 'maxcores', 99999)
137      pool_size = min(pool_size, maxCores)
138  
139      pool = Pool(processes=pool_size)
140      result = []
141      for i in range(pool_size):
142          result.append(pool.apply_async(
143              _pool_worker, args=(i, initialHash, target, pool_size)))
144  
145      while True:
146          if state.shutdown != 0:
147              try:
148                  pool.terminate()
149                  pool.join()
150              except:  # nosec B110 # noqa:E722
151                  pass
152              raise StopIteration("Interrupted")
153          for i in range(pool_size):
154              if result[i].ready():
155                  try:
156                      result[i].successful()
157                  except AssertionError:
158                      pool.terminate()
159                      pool.join()
160                      raise StopIteration("Interrupted")
161                  result = result[i].get()
162                  pool.terminate()
163                  pool.join()
164                  logger.debug('Fast PoW done')
165                  return result[0], result[1]
166          time.sleep(0.2)
167  
168  
169  def _doCPoW(target, initialHash):
170      with LogOutput():
171          h = initialHash
172          m = target
173          out_h = ctypes.pointer(ctypes.create_string_buffer(h, 64))
174          out_m = ctypes.c_ulonglong(m)
175          logger.debug('C PoW start')
176          nonce = bmpow(out_h, out_m)
177  
178      trialValue = trial_value(nonce, initialHash)
179      if state.shutdown != 0:
180          raise StopIteration("Interrupted")
181      logger.debug('C PoW done')
182      return trialValue, nonce
183  
184  
185  def _doGPUPoW(target, initialHash):
186      logger.debug('GPU PoW start')
187      nonce = openclpow.do_opencl_pow(initialHash.encode("hex"), target)
188      trialValue = trial_value(nonce, initialHash)
189      if trialValue > target:
190          deviceNames = ", ".join(gpu.name for gpu in openclpow.enabledGpus)
191          queues.UISignalQueue.put((
192              'updateStatusBar', (
193                  _translate(
194                      "MainWindow",
195                      "Your GPU(s) did not calculate correctly,"
196                      " disabling OpenCL. Please report to the developers."
197                  ), 1)
198          ))
199          logger.error(
200              'Your GPUs (%s) did not calculate correctly, disabling OpenCL.'
201              ' Please report to the developers.', deviceNames)
202          openclpow.enabledGpus = []
203          raise Exception("GPU did not calculate correctly.")
204      if state.shutdown != 0:
205          raise StopIteration("Interrupted")
206      logger.debug('GPU PoW done')
207      return trialValue, nonce
208  
209  
210  # def estimate(difficulty, fmt=False):
211  #     ret = difficulty / 10
212  #     if ret < 1:
213  #         ret = 1
214  #
215  #     if fmt:
216  #         out = str(int(ret)) + " seconds"
217  #         if ret > 60:
218  #             ret /= 60
219  #             out = str(int(ret)) + " minutes"
220  #         if ret > 60:
221  #             ret /= 60
222  #             out = str(int(ret)) + " hours"
223  #         if ret > 24:
224  #             ret /= 24
225  #             out = str(int(ret)) + " days"
226  #         if ret > 7:
227  #             out = str(int(ret)) + " weeks"
228  #         if ret > 31:
229  #             out = str(int(ret)) + " months"
230  #         if ret > 366:
231  #             ret /= 366
232  #             out = str(int(ret)) + " years"
233  #         ret = None  # Ensure legacy behaviour
234  #
235  #     return ret
236  
237  
238  def getPowType():
239      """Get the proof of work implementation"""
240  
241      if openclpow.openclEnabled():
242          return "OpenCL"
243      if bmpow:
244          return "C"
245      return "python"
246  
247  
248  def notifyBuild(tried=False):
249      """
250      Notify the user of the success or otherwise of building the PoW C module
251      """
252  
253      if bmpow:
254          queues.UISignalQueue.put(('updateStatusBar', (_translate(
255              "proofofwork", "C PoW module built successfully."), 1)))
256      elif tried:
257          queues.UISignalQueue.put(('updateStatusBar', (_translate(
258              "proofofwork",
259              "Failed to build C PoW module. Please build it manually."), 1)))
260      else:
261          queues.UISignalQueue.put(('updateStatusBar', (_translate(
262              "proofofwork", "C PoW module unavailable. Please build it."), 1)))
263  
264  
265  def buildCPoW():
266      """Attempt to build the PoW C module"""
267      if bmpow is not None:
268          return
269      if paths.frozen or sys.platform.startswith('win'):
270          notifyBuild(False)
271          return
272  
273      try:
274          # GNU make
275          make_cmd = ['make', '-C', os.path.join(paths.codePath(), 'bitmsghash')]
276          if "bsd" in sys.platform:
277              # BSD make
278              make_cmd += ['-f', 'Makefile.bsd']
279  
280          subprocess.check_call(make_cmd)  # nosec B603
281          if os.path.exists(
282              os.path.join(paths.codePath(), 'bitmsghash', 'bitmsghash.so')
283          ):
284              init()
285      except (OSError, subprocess.CalledProcessError):
286          pass
287      except:  # noqa:E722
288          logger.warning(
289              'Unexpected exception rised when tried to build bitmsghash lib',
290              exc_info=True)
291      notifyBuild(True)
292  
293  
294  def run(target, initialHash):
295      """Run the proof of work calculation"""
296  
297      if state.shutdown != 0:
298          raise StopIteration("Interrupted")
299      target = int(target)
300      if openclpow.openclEnabled():
301          return _doGPUPoW(target, initialHash)
302      if bmpow:
303          return _doCPoW(target, initialHash)
304      if paths.frozen == "macosx_app" or not paths.frozen:
305          # on my (Peter Surda) Windows 10, Windows Defender
306          # does not like this and fights with PyBitmessage
307          # over CPU, resulting in very slow PoW
308          # added on 2015-11-29: multiprocesing.freeze_support() doesn't help
309          return _doFastPoW(target, initialHash)
310  
311      return _doSafePoW(target, initialHash)
312  
313  
314  def getTarget(payloadLength, ttl, nonceTrialsPerByte, payloadLengthExtraBytes):
315      """Get PoW target for given length, ttl and difficulty params"""
316      return 2 ** 64 / (
317          nonceTrialsPerByte * (
318              payloadLength + 8 + payloadLengthExtraBytes + ((
319                  ttl * (
320                      payloadLength + 8 + payloadLengthExtraBytes
321                  )) / (2 ** 16))
322          ))
323  
324  
325  def calculate(
326      payload, ttl,
327      nonceTrialsPerByte=networkDefaultProofOfWorkNonceTrialsPerByte,
328      payloadLengthExtraBytes=networkDefaultPayloadLengthExtraBytes
329  ):
330      """Do the PoW for the payload and TTL with optional difficulty params"""
331      return run(getTarget(
332          len(payload), ttl, nonceTrialsPerByte, payloadLengthExtraBytes),
333          hashlib.sha512(payload).digest())
334  
335  
336  def resetPoW():
337      """Initialise the OpenCL PoW"""
338      openclpow.initCL()
339  
340  
341  # init
342  
343  
344  def init():
345      """Initialise PoW"""
346      # pylint: disable=broad-exception-caught,global-statement
347      global bitmsglib, bmpow
348  
349      openclpow.initCL()
350      if sys.platform.startswith('win'):
351          bitmsglib = (
352              'bitmsghash32.dll' if ctypes.sizeof(ctypes.c_voidp) == 4 else
353              'bitmsghash64.dll')
354          libfile = os.path.join(paths.codePath(), 'bitmsghash', bitmsglib)
355          try:
356              # MSVS
357              bso = ctypes.WinDLL(
358                  os.path.join(paths.codePath(), 'bitmsghash', bitmsglib))
359              logger.info('Loaded C PoW DLL (stdcall) %s', bitmsglib)
360              bmpow = bso.BitmessagePOW
361              bmpow.restype = ctypes.c_ulonglong
362              _doCPoW(2**63, "")
363              logger.info(
364                  'Successfully tested C PoW DLL (stdcall) %s', bitmsglib)
365          except ValueError:
366              try:
367                  # MinGW
368                  bso = ctypes.CDLL(libfile)
369                  logger.info('Loaded C PoW DLL (cdecl) %s', bitmsglib)
370                  bmpow = bso.BitmessagePOW
371                  bmpow.restype = ctypes.c_ulonglong
372                  _doCPoW(2**63, "")
373                  logger.info(
374                      'Successfully tested C PoW DLL (cdecl) %s', bitmsglib)
375              except Exception as e:
376                  logger.error('Error: %s', e, exc_info=True)
377          except Exception as e:
378              logger.error('Error: %s', e, exc_info=True)
379      else:
380          try:
381              bso = ctypes.CDLL(
382                  os.path.join(paths.codePath(), 'bitmsghash', bitmsglib))
383          except OSError:
384              import glob
385              try:
386                  bso = ctypes.CDLL(glob.glob(os.path.join(
387                      paths.codePath(), 'bitmsghash', 'bitmsghash*.so'
388                  ))[0])
389              except (OSError, IndexError):
390                  bso = None
391          except Exception:
392              bso = None
393          else:
394              logger.info('Loaded C PoW DLL %s', bitmsglib)
395          if bso:
396              try:
397                  bmpow = bso.BitmessagePOW
398                  bmpow.restype = ctypes.c_ulonglong
399              except Exception:
400                  logger.warning(
401                      'Failed to setup bmpow lib %s', bso, exc_info=True)
402                  return
403  
404      if bmpow is None:
405          buildCPoW()