-
Notifications
You must be signed in to change notification settings - Fork 27
/
solvers.py
155 lines (134 loc) · 5.76 KB
/
solvers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# Copyright (C) 2016-2018 Sebastian Stammler
#
# This file is part of GenesisZ.
# solvers.py - classes to run the solvers (currently silentarmy and tromp)
#
# It is subject to the license terms in the LICENSE file found in the top-level
# directory of this distribution.
#
# No part of GenesisZ, including this file, may be copied, modified,
# propagated, or distributed except according to the terms contained in the
# LICENSE file.
from abc import ABCMeta, abstractmethod
import asyncio, re
from bitcoin.core import x, b2x, b2lx
from bitcoin.core.serialize import uint256_from_str
from zcash.core import GetSolutionSize, IsValidSolution, IncrementNonce
from logger import warn, fatal, verb
def stri(b):
return b.decode('ascii').rstrip()
class Solver(metaclass=ABCMeta):
def __init__(self, path, header, rounds=1, start_nonce=32*b'\x00', threads=1):
self.path = path
self.header = header
self.start_nonce = start_nonce
self.rounds = rounds
self.threads = threads
self.cmdline = self.build_cmdline()
async def run(self):
verb('Starting solver with command {}'.format(self.cmdline))
create = asyncio.create_subprocess_exec(
*self.cmdline,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT)
try:
self.solver = await create
verb('Solver started.')
except FileNotFoundError as e:
raise SolverException("Could not find binary {}; is the path correct?".format(self.cmdline[0]))
except Exception as e:
raise SolverException("Failed to execute '{}': {}".format(self.cmdline, e))
self.banner = await self.eat_banner()
verb('Banner:\n' + self.banner)
try:
while True:
nonce, sols = await self.parse_solutions()
verb('Solver returned {:d} solutions for nonce {}'
.format(len(sols), b2lx(nonce)))
for sol in sols:
if IsValidSolution(self.header, nonce, sol):
return (sol, nonce)
finally:
self.solver.terminate()
await self.solver.communicate() # .wait() would deadlock if pipe is full
@abstractmethod
def build_cmdline(self): pass
"""Should assemble all parameters set in __init__ to the command line to
start the solver with, stored as a string list, and return it."""
@abstractmethod
async def eat_banner(self): pass
"""Should read from self.solver.stdout until the last line before content
appears that parse_solution understands. Should return the banner as a
single string."""
@abstractmethod
async def parse_solutions(self): pass
""""Should parse self.solver.stdout for solutions and return the current
nonce and list of solutions (as byte strings) when all solutions for this
nonce have been printed."""
class SolverException(Exception):
pass
class SilentarmySolver(Solver):
def build_cmdline(self):
return self.path + ['--nonces', str(self.rounds),
'-i', b2x(self.header.serialize())]
async def eat_banner(self):
# consume banner input until solutions pop up
banner = []
async for line in self.solver.stdout:
banner.append(stri(line))
# Last line before solutions pop up in sa-solver
if line.startswith(b'Running'):
break
return '\n'.join(banner)
async def parse_solutions(self):
sols, sol_size = [], GetSolutionSize()
async for line in self.solver.stdout:
line = stri(line)
if line.startswith('Nonce'):
break
if line.startswith('Total'):
raise SolverException('Solver stopped before valid solution found.')
if len(line) != sol_size*2:
raise SolverException("Solver returned unexpected solution of size != {:d}:\n{}"
.format(sol_size, line))
sols.append(x(line))
_, nonce, solc, _ = line.split()
nonce = x(nonce[:-1]) # TODO or lx?
return (nonce, sols)
class TrompSolver(Solver):
def build_cmdline(self):
return self.path + ['-s', '-c',
'-n', str(uint256_from_str(self.start_nonce)),
'-r', str(self.rounds),
'-t', str(self.threads),
'-x', b2x(self.header.serialize())]
async def eat_banner(self):
banner = []
async for line in self.solver.stdout:
banner.append(stri(line))
# Last line before solutions pop up in tromp solver
if stri(line).startswith('Using'):
break
# need to count nonce for tromp as it is not part of output
self.nonce = self.start_nonce
return '\n'.join(banner)
async def parse_solutions(self):
sols, sol_size, nonce = [], GetSolutionSize(), self.nonce
async for line in self.solver.stdout:
line = stri(line)
if re.match(r'Digit \d+', line):
continue
if re.match(r'\d+ solutions', line): # last line
# increment nonce for next round
self.nonce = IncrementNonce(self.nonce)
break
if re.match(r'\d+ total solutions', line):
raise SolverException('Solver stopped before valid solution found.')
if not line.startswith('Solution'):
raise SolverException('Unexpected solver output:\n' + line)
line = line.split()[1]
if len(line) != sol_size*2:
raise SolverException("Solver returned unexpected solution of size != {:d}:\n{}"
.format(sol_size, line))
sols.append(x(line))
return (nonce, sols)