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()