Onionr/src/onionrproofs/subprocesspow.py

140 lines
4.7 KiB
Python
Raw Normal View History

2019-03-12 18:23:46 +00:00
#!/usr/bin/env python3
"""Onionr - Private P2P Communication.
Multiprocess proof of work
"""
import os
from multiprocessing import Pipe, Process
import threading
import time
2020-09-19 21:01:31 +00:00
import secrets
import onionrproofs
import ujson as json
import logger
from onionrutils import bytesconverter
2020-09-19 21:01:31 +00:00
from onionrcrypto.hashers import sha3_hash
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
2019-03-12 18:23:46 +00:00
class SubprocessPOW:
2019-07-19 19:49:56 +00:00
def __init__(self, data, metadata, subproc_count=None):
"""
Onionr proof of work using multiple processes
2019-07-19 19:49:56 +00:00
Accepts block data, block metadata
if subproc_count is not set,
os.cpu_count() is used to determine the number of processes
Due to Python GIL multiprocessing/use of external libraries
is necessary to accelerate CPU bound tasks
"""
# No known benefit to using more processes than there are cores.
# Note: os.cpu_count perhaps not always accurate
if subproc_count is None:
subproc_count = os.cpu_count()
self.subproc_count = subproc_count
2019-03-12 18:23:46 +00:00
self.result = ''
self.shutdown = False
self.data = data
self.metadata = metadata
"""dump dict to measure bytes of json metadata
Cannot reuse later bc the pow token must be added
"""
json_metadata = json.dumps(metadata).encode()
2019-03-12 18:23:46 +00:00
self.data = bytesconverter.str_to_bytes(data)
compiled_data = bytes(json_metadata + b'\n' + self.data)
# Calculate difficulty. May use better algorithm in the future.
self.difficulty = onionrproofs.getDifficultyForNewBlock(compiled_data)
2019-07-20 00:01:16 +00:00
logger.info('Computing POW (difficulty: %s)...' % (self.difficulty,))
2019-03-12 18:23:46 +00:00
self.main_hash = '0' * 64
self.puzzle = self.main_hash[0:min(self.difficulty,
len(self.main_hash))]
2019-03-12 18:23:46 +00:00
self.shutdown = False
self.payload = None
def start(self):
"""spawn the multiproc handler threads"""
# Create a new thread for each subprocess
for _ in range(self.subproc_count): # noqa
2019-12-09 11:02:37 +00:00
threading.Thread(target=self._spawn_proc, daemon=True).start()
# Monitor the processes for a payload, shut them down when its found
2019-03-12 18:23:46 +00:00
while True:
if self.payload is None:
time.sleep(0.1)
else:
self.shutdown = True
return self.payload
2019-03-12 18:23:46 +00:00
def _spawn_proc(self):
"""Create a child proof of work process
wait for data and send shutdown signal when its found"""
2019-03-12 18:23:46 +00:00
parent_conn, child_conn = Pipe()
2019-12-09 11:02:37 +00:00
p = Process(target=self.do_pow, args=(child_conn,), daemon=True)
2019-03-12 18:23:46 +00:00
p.start()
p.join()
payload = None
try:
while True:
data = parent_conn.recv()
if len(data) >= 1:
payload = data
break
except KeyboardInterrupt:
pass
finally:
parent_conn.send('shutdown')
self.payload = payload
def do_pow(self, pipe):
"""find partial hash colision generating nonce for a block"""
nonce = 0
2019-03-12 18:23:46 +00:00
data = self.data
metadata = self.metadata
2020-09-19 21:01:31 +00:00
metadata['nonce'] = secrets.randbits(16)
2019-03-12 18:23:46 +00:00
puzzle = self.puzzle
difficulty = self.difficulty
2019-03-12 18:23:46 +00:00
while True:
# Break if shutdown received
try:
if pipe.poll() and pipe.recv() == 'shutdown':
break
except KeyboardInterrupt:
break
# Load nonce into block metadata
2019-04-04 21:56:18 +00:00
metadata['pow'] = nonce
# Serialize metadata, combine with block data
2019-03-12 18:23:46 +00:00
payload = json.dumps(metadata).encode() + b'\n' + data
# Check sha3_256 hash of block, compare to puzzle
# Send payload if puzzle finished
2020-09-19 21:01:31 +00:00
token = sha3_hash(payload)
# ensure token is string
token = bytesconverter.bytes_to_str(token)
2019-03-12 18:23:46 +00:00
if puzzle == token[0:difficulty]:
pipe.send(payload)
break
nonce += 1