Skip to content

New POW calculation module #1284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: v0.6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a181da3
New POW calculation module
Kleshni Jun 23, 2018
079326b
More precise exceptions
Kleshni Jun 23, 2018
46da7e8
Fixed indentation
Kleshni Jun 23, 2018
917b55d
Sorted imports
Kleshni Jun 23, 2018
2eeb58a
Fixed indentation
Kleshni Jun 23, 2018
43f7e1e
Improved unit tests loading
Kleshni Jun 23, 2018
30e7df6
Skip OpenCL test if unavailable
Kleshni Jun 23, 2018
721ad8c
Add "workprover" to packages list
Kleshni Jun 23, 2018
4f9274a
More build system integration for WorkProver
Kleshni Jun 25, 2018
cac0237
OS X compatibility
Kleshni Jun 25, 2018
4c0b0c7
Merge branch 'v0.6' into POW
Kleshni Jun 27, 2018
7999330
Downgrade OpenSSL version
Kleshni Jun 27, 2018
90ae95d
Added WorkProver to executable builders
Kleshni Jun 28, 2018
c28a4f6
Don't load heavy modules until needed
Kleshni Jun 28, 2018
6fb637d
Merge branch 'v0.6' into POW
Kleshni Jun 28, 2018
0c3ce79
Wrap "bitmessagemain.py" for use with "multiprocessing"
Kleshni Jun 30, 2018
5429191
Fix for running after installation
Kleshni Jun 30, 2018
3c0e235
Signal handling
Kleshni Jun 30, 2018
8e0bff7
Connect WorkProver to SingleWorker
Kleshni Jul 15, 2018
b24edfd
Merge branch 'v0.6' into POW
Kleshni Jul 15, 2018
e94bdf4
Fix "maxcores" setting
Kleshni Jul 15, 2018
d1a5c60
Deleted generated code for settings window
Kleshni Jul 20, 2018
b2441c7
Deleted generated code for main window
Kleshni Jul 22, 2018
c66156a
Added POW speed indicator
Kleshni Jul 22, 2018
ce4fe3a
Added sending cancellation
Kleshni Jul 23, 2018
174fb38
Forgotten litter
Kleshni Jul 23, 2018
f6415d6
Added "Edit and resend" option
Kleshni Jul 23, 2018
a27b5e9
Merge branch 'v0.6' into POW
Kleshni Jul 23, 2018
2c7d677
Added estimated POW time tooltip
Kleshni Jul 23, 2018
373157d
Added POW settings to settings window
Kleshni Jul 29, 2018
00c4ee8
Added API for raw objects
Kleshni Jul 29, 2018
02ece2d
Added more API for raw objects
Kleshni Jul 29, 2018
c22c5c2
Removed broken API methods
Kleshni Jul 29, 2018
a398354
Deleted old module
Kleshni Jul 29, 2018
8e1259d
More old code deleted
Kleshni Jul 29, 2018
0aaad8b
Fixed big-endian decoding
Kleshni Jul 30, 2018
192b083
Readability
Kleshni Jul 30, 2018
7ad1725
Applying solution I don't understand
Kleshni Aug 2, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/workprover/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Please keep this module independent from the outside code, so that it can be reused in other applications.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case why can't we make it a separate package and import it where required ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a Python package with the __init__.py file, and it's intended to be imported like import workprover. It could be moved to a separate repository, but I think, it's easier and safer to keep it in the same file tree.


If you are going to use it, you should wrap your program's main file in this:

```python
import workprover.dumbsolver

workprover.dumbsolver.libcrypto = ...

if __name__ == "__main__":
import multiprocessing

multiprocessing.freeze_support()

...
```

See the `multiprocessing` module documentation for explaination.

Build fast solver
-----------------

On Linux, BSDs or MacOS: `make -C fastsolver`.

On Windows:

- Install OpenSSL. Build it yourself or install [third-party](https://wiki.openssl.org/index.php/Binaries) prebuilt binaries.

- Install MSVC as part of Visual Studio or standalone. Official offline installer: https://aka.ms/vcpython27.

- Open its command line and go to the `fastsolver` directory.

- Add OpenSSL paths to environment variables:

```bat
set INCLUDE=C:\OpenSSL-Win64\include;%INCLUDE%
set LIB=C:\OpenSSL-Win64\lib;%LIB%
```

- Do `cl @options.txt`.

- Append the `-32` or `-64` suffix to the DLL file name.
238 changes: 238 additions & 0 deletions src/workprover/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import Queue

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please place all the imports in a alphabetic order.

import collections
import multiprocessing
import struct
import sys
import threading
import time

import dumbsolver
import fastsolver
import forkingsolver
import gpusolver
import utils

timeout = .5

class Stop(Exception):
pass

class Task(object):
previous = None
next = None

def __init__(self, headlessPayload, TTL, expiryTime, target):
self.headlessPayload = headlessPayload
self.TTL = TTL
self.expiryTime = expiryTime
self.target = target

class WorkProver(threading.Thread):
def __init__(self, codePath, GPUVendors, seed, statusUpdated):
super(self.__class__, self).__init__()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc string required here

self.availableSolvers = {
"dumb": dumbsolver.DumbSolver(codePath)
}

# Comment from the previous version:

# on my (Peter Surda) Windows 10, Windows Defender
# does not like this and fights with PyBitmessage
# over CPU, resulting in very slow PoW
# added on 2015-11-29: multiprocesing.freeze_support() doesn't help

if not hasattr(sys, "frozen") or sys.frozen == "macosx_app":
self.availableSolvers["forking"] = forkingsolver.ForkingSolver(codePath)

try:
self.availableSolvers["fast"] = fastsolver.FastSolver(codePath)
except fastsolver.FastSolverError:
pass

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass is the dangerous statement, please log it or please write a print statement atleast...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.availableSolvers is visible to the outside code, so it can log or print if fast is missing. It should show a message in GUI status bar like the current code does.


try:
self.availableSolvers["gpu"] = gpusolver.GPUSolver(codePath, GPUVendors)
except gpusolver.GPUSolverError:
pass

try:
self.defaultParallelism = multiprocessing.cpu_count()
except NotImplementedError:
self.defaultParallelism = 1

self.seed = seed
self.roundsCounter = 0
self.statusUpdated = statusUpdated

self.commandsQueue = Queue.Queue()
self.resultsQueue = Queue.Queue()

self.solverName = None
self.solver = None

self.lastTime = utils.getTimePoint()
self.timedIntervals = collections.deque()
self.speed = 0

self.tasks = {}
self.currentTaskID = None

def notifyStatus(self):
if self.statusUpdated is None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not self.statusUpdated:
return

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's less acurate.

self.statusUpdated must be either a function or a None. If it's 0 or an empty string, it would be a programming error and further call would rise an exception making the error noticable.

return

if self.solver is None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parallelism = self.solver.parallelism if self.solver else 0

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's less readable.

parallelism = 0
else:
parallelism = self.solver.parallelism

self.statusUpdated((self.solverName, parallelism, self.speed))

def setSolver(self, name, parallelism):
if name is None and self.solverName is None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not name and not self.solverName

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logging is required for this functionality.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It calls the self.statusUpdated callback and the outside code should log this and display in the GUI.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not name and not self.solverName:
pass

pass
elif name == self.solverName:
if self.solver.parallelism != parallelism:
self.solver.setParallelism(parallelism)
else:
if self.solver is not None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not self.solver

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if self.solver:
….

self.solver.setParallelism(0)
self.solverName = None
self.solver = None

if name is not None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not name

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if name:
….

if name not in self.availableSolvers:
name, parallelism = "dumb", 1

self.solverName = name
self.solver = self.availableSolvers[name]
self.solver.setParallelism(parallelism)

self.notifyStatus()

def updateSpeed(self, iterationsCount):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring here

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pep8 validation

currentTime = utils.getTimePoint()
duration = currentTime - self.lastTime
self.lastTime = currentTime

self.timedIntervals.append((currentTime, iterationsCount, duration))

for i in xrange(len(self.timedIntervals)):
time, iterationsCount, duration = self.timedIntervals[0]

if time + duration < currentTime - 3:
self.timedIntervals.popleft()

totalDuration = 0
totalIterationsCount = 0

for time, iterationsCount, duration in self.timedIntervals:
totalIterationsCount += iterationsCount
totalDuration += duration

if totalDuration < .25:
self.speed = 0
else:
self.speed = totalIterationsCount / totalDuration

self.notifyStatus()

def addTask(self, ID, headlessPayload, TTL, expiryTime, byteDifficulty, lengthExtension):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add docs to the function.

target = utils.calculateTarget(8 + 8 + len(headlessPayload), TTL, byteDifficulty, lengthExtension)

task = Task(headlessPayload, TTL, expiryTime, target)

self.tasks[ID] = task

if self.currentTaskID is None:
task.previous = ID
task.next = ID

self.currentTaskID = ID
else:
task.previous = self.currentTaskID
task.next = self.tasks[self.currentTaskID].next

self.tasks[task.previous].next = ID
self.tasks[task.next].previous = ID

def cancelTask(self, ID):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring here please

if ID not in self.tasks:
return

task = self.tasks.pop(ID)

if len(self.tasks) == 0:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not self.tasks

self.currentTaskID = None
else:
self.tasks[task.previous].next = task.next
self.tasks[task.next].previous = task.previous

if self.currentTaskID == ID:
self.currentTaskID = task.next

def nextTask(self):
self.currentTaskID = self.tasks[self.currentTaskID].next

def shutdown(self):
self.setSolver(None, 0)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pep8 validation


for i in self.tasks.keys():
self.cancelTask(i)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PeterSurda , I think we need optimization here.


raise Stop()

def processCommand(self, command, *arguments):
getattr(self, command)(*arguments)

def round(self):
while True:
try:
self.processCommand(*self.commandsQueue.get_nowait())
except Queue.Empty:
break

while self.solver is None or self.currentTaskID is None:
try:
self.processCommand(*self.commandsQueue.get(True, timeout))
except Queue.Empty:
self.updateSpeed(0)

task = self.tasks[self.currentTaskID]

if task.expiryTime is None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not task.expiryTime

expiryTime = int(time.time() + task.TTL)
else:
expiryTime = task.expiryTime

initialPayload = struct.pack(">Q", expiryTime) + task.headlessPayload
initialHash = utils.calculateInitialHash(initialPayload)

appendedSeed = self.seed + struct.pack(">Q", self.roundsCounter)
self.roundsCounter += 1

try:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pep8 validation

nonce, iterationsCount = self.solver.search(initialHash, task.target, appendedSeed, timeout)
except gpusolver.GPUSolverError:
self.setSolver("dumb", 1)
self.availableSolvers.pop("gpu")

nonce, iterationsCount = None, 0

self.updateSpeed(iterationsCount)

if nonce is None:
self.nextTask()
else:
self.resultsQueue.put(("taskDone", self.currentTaskID, nonce, expiryTime))

self.cancelTask(self.currentTaskID)

def run(self):
try:
while True:
self.round()
except Stop:
return
except Exception as exception:
self.resultsQueue.put(exception)
70 changes: 70 additions & 0 deletions src/workprover/dumbsolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import ctypes
import hashlib
import struct

import utils

libcrypto = None

class DumbSolver(object):
def __init__(self, codePath):
libcrypto.SHA512.restype = ctypes.c_void_p

self.prefixes = [chr(i) for i in xrange(256)]

if ctypes.c_size_t is ctypes.c_uint:
self.proofLength = 8 + 64
self.hashLength = 64
else:
# Using the wrapper instead of a clear number slows the work down, but otherwise seems to be unsafe

self.proofLength = ctypes.c_size_t(8 + 64)
self.hashLength = ctypes.c_size_t(64)

self.firstHash = ctypes.create_string_buffer(64)
self.secondHash = ctypes.create_string_buffer(64)

self.parallelism = 1

def search(self, initialHash, target, seed, timeout):
startTime = utils.getTimePoint()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs here and pep8 validation.


sha512 = libcrypto.SHA512

prefixes = self.prefixes
proofLength = self.proofLength
hashLength = self.hashLength
firstHash = self.firstHash
secondHash = self.secondHash

encodedTarget = struct.pack(">Q", target)

solutions = []
i = 0

while True:
randomness = hashlib.sha512(seed + struct.pack(">Q", i)).digest()
i += 1

suffix = randomness[: 7] + initialHash

for j in prefixes:
proof = j + suffix

sha512(j + suffix, proofLength, firstHash)
sha512(firstHash, hashLength, secondHash)

if secondHash[: 8] <= encodedTarget:
solutions.append(proof[: 8])

if len(solutions) != 0:
index, = struct.unpack(">Q", randomness[7: 15])
nonce = solutions[index % len(solutions)]

return nonce, 256 * i

if utils.getTimePoint() - startTime >= timeout:
return None, 256 * i

def setParallelism(self, parallelism):
pass
Loading