Skip to content

Commit

Permalink
Add solo pool
Browse files Browse the repository at this point in the history
  • Loading branch information
MentalCollatz committed Apr 3, 2019
1 parent 2e41a9b commit 511c973
Show file tree
Hide file tree
Showing 7 changed files with 709 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -1,5 +1,7 @@
*.[oa]
*.db_info
*.pyc
__pycache__
src/verilog/odo_gen
src/pool/fakepool
src/projects/*/build_files/*
Expand Down
13 changes: 11 additions & 2 deletions README.md
Expand Up @@ -20,6 +20,14 @@ your installation. For example ``export QUARTUSPATH="/home/miner/altera/18.1/qu
This line should be added to your ``~/.profile`` file or another file that is sourced whenever a
shell is launched. Don't forget to source the file after editing it.

Solo Mining
-----------

Install and start a full node via <https://github.com/digibyte/digibyte>.

* A python interpreter is required and pip is recommended - ``apt install python python-pip`` (Python 3 should also work, but most testing has been done in Python 2).
* Python modules base58 and requests - ``pip install base58 requests``

Additional Files
----------------

Expand All @@ -44,7 +52,8 @@ Starting to Mine

This will require multiple terminal windows. A screen multiplexer such as [tmux](https://github.com/tmux/tmux/wiki) or [screen](https://www.gnu.org/software/screen/) may make things easier for you.

* Ensure your DigiByte node is running. It is recommended that you do not specify an rpcpassword in digibyte.conf. The rpcuser and rpcpassword options will soon be deprecated.
* In one terminal, go to the ``src`` directory and run ``./autocompile.sh --testnet cyclone_v_gx_starter_kit de10_nano``
* In another terminal, go to the ``src/pool`` directory and run ``make fakepool`` followed by ``./fakepool --testnet``
* Finally, for each mining fpga open a terminal in the ``src/miner`` directory and run ``$QUARTUSPATH/quartus_stp -t mine.tcl``
* In another terminal, go to the ``src/pool/solo`` directory and run ``python pool.py --testnet <dgb_address>``
* Finally, for each mining fpga open a terminal in the ``src/miner`` directory and run ``$QUARTUSPATH/quartus_stp -t mine.tcl [hardware_name]``. The ``hardware_name`` argument is optional, and if not specified the script will prompt you to select one of the detected mining devices.

118 changes: 118 additions & 0 deletions src/pool/solo/config.py
@@ -0,0 +1,118 @@
# Copyright (C) 2019 MentalCollatz
#
# 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 <http://www.gnu.org/licenses/>.

import argparse
from base64 import b64encode
import os
import platform
import sys

from template import Script

DEFAULT_LISTEN_PORT = 17064

MAINNET_RPC_PORT = 14022
TESTNET_RPC_PORT = 18332

MAINNET_ADDR_FORMAT = {"bech32_hrp": "dgb", "prefix_pubkey": 30, "prefix_script": 63 }
TESTNET_ADDR_FORMAT = {"bech32_hrp": "dgbt", "prefix_pubkey": 126, "prefix_script": 140 }

def data_dir():
if platform.system() == "Windows":
return os.path.join(os.environ["APPDATA"], "DigiByte")
elif platform.system() == "Darwin":
return os.path.expanduser("~/Library/Application Support/DigiByte/")
else:
return os.path.expanduser("~/.digibyte/")

# base64 encode that works the same in both Python 2 and 3
def b64encode_helper(s):
res = b64encode(s.encode())
if type(res) == bytes:
res = res.decode()
return res

params = {}

def init(argv):
parser = argparse.ArgumentParser(description="Solo-mining pool.")
parser.add_argument("-t", "--testnet", help="use testnet params", action="store_true")
parser.add_argument("-H", "--host", help="rpc host", dest="rpc_host", default="localhost")
parser.add_argument("-p", "--port", help="rpc port", dest="rpc_port", type=int)
parser.add_argument("--user", help="rpc user (discouraged, --auth is preferred)")
parser.add_argument("--password", help="rpc password (discouraged, --auth is preferred)")
parser.add_argument("-a", "--auth", help="rpc authorization file", type=argparse.FileType("r"))
parser.add_argument("-l", "--listen", help="port to listen for miners on", dest="listen_port", default=DEFAULT_LISTEN_PORT, type=int)
parser.add_argument("-r", "--remote", help="allow remote miners to connect", action="store_true")
parser.add_argument("--coinbase", help="coinbase string", type=str, default="/odo-miner-solo/")
parser.add_argument("address", help="address to mine to", type=str)
args = parser.parse_args(argv[1:])

global params
params = {key: getattr(args, key) for key in ["rpc_host", "listen_port", "testnet"]}

addr_format = TESTNET_ADDR_FORMAT if args.testnet else MAINNET_ADDR_FORMAT
cbscript = Script.from_address(args.address, **addr_format)
if cbscript is None:
other_addr_format = MAINNET_ADDR_FORMAT if args.testnet else TESTNET_ADDR_FORMAT
cbscript = Script.from_address(args.address, **other_addr_format)
if cbscript is not None:
if args.testnet:
parser.error("mainnet address specified with --testnet")
else:
parser.error("testnet address specified without --testnet")
else:
parser.error("invalid address")
params["cbscript"] = cbscript.data
params["cbstring"] = args.coinbase

if args.user and args.password:
if args.auth:
parser.error("argument --auth is not allowed with arguments --user and --password")
if ':' in args.user:
parser.error("user may not contain `:`")
rpc_auth = args.user + ':' + args.password
elif args.user or args.password:
parser.error("--user and --password must both be present or neither present")
elif args.auth:
rpc_auth = args.auth.read()
else:
cookie = data_dir()
if args.testnet:
cookie = os.path.join(cookie, "testnet3")
cookie = os.path.join(cookie, ".cookie")
try:
with open(cookie, "r") as f:
# Note: if the user restarts the server, they will need to
# restart the pool also
rpc_auth = f.read()
except IOError as e:
parser.error("Unable to read default auth file `%s`, please specify auth file or user and password.")
params["rpc_auth"] = "Basic " + b64encode_helper(rpc_auth)

if args.rpc_port:
rpc_port = args.rpc_port
else:
rpc_port = TESTNET_RPC_PORT if args.testnet else MAINNET_RPC_PORT
params["rpc_port"] = rpc_port
params["rpc_url"] = "http://%s:%d" % (params["rpc_host"], params["rpc_port"])

params["bind_addr"] = "" if args.remote else "localhost"

def get(key):
global params
if not params:
init(sys.argv)
return params[key]
176 changes: 176 additions & 0 deletions src/pool/solo/pool.py
@@ -0,0 +1,176 @@
#!/usr/bin/env python

# Copyright (C) 2019 MentalCollatz
#
# 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 <http://www.gnu.org/licenses/>.

import socket
import threading
import time

import config
import rpc
from template import BlockTemplate

def get_templates(callback):
longpollid = None
last_errno = None
while True:
try:
template = rpc.get_block_template(longpollid)
if "coinbaseaux" not in template:
template["coinbaseaux"] = {}
template["coinbaseaux"]["cbstring"] = config.get("cbstring")
callback(template)
longpollid = template["longpollid"]
if last_errno != 0:
print("%s: successfully acquired template" % time.asctime())
last_errno = 0
except (rpc.RpcError, socket.error) as e:
if last_errno == 0:
callback(None)
if e.errno != last_errno:
last_errno = e.errno
print("%s: %s (errno %d)" % (time.asctime(), e.strerror, e.errno))
time.sleep(1)

class Manager(threading.Thread):
def __init__(self, cbscript):
threading.Thread.__init__(self)
self.cbscript = cbscript
self.template = None
self.extra_nonce = 0
self.miners = []
self.cond = threading.Condition()

def add_miner(self, miner):
with self.cond:
self.miners.append(miner)
self.cond.notify()

def remove_miner(self, miner):
with self.cond:
self.miners.remove(miner)

def push_template(self, template):
with self.cond:
if template is None:
self.template = None
else:
self.template = BlockTemplate(template, self.cbscript)
self.extra_nonce = 0
for miner in self.miners:
miner.next_refresh = 0
self.cond.notify()

def run(self):
while True:
with self.cond:
now = time.time()
next_refresh = now + 1000
for miner in self.miners:
if miner.next_refresh < now:
miner.push_work(self.template, self.extra_nonce)
self.extra_nonce += 1
next_refresh = min(next_refresh, miner.next_refresh)
wait_time = max(0, next_refresh - time.time())
self.cond.wait(wait_time)

class Miner(threading.Thread):
def __init__(self, conn, manager):
threading.Thread.__init__(self)
self.conn = conn
self.manager = manager
self.lock = threading.Lock()
self.conn_lock = threading.Lock()
self.work_items = []
self.next_refresh = 0
self.refresh_interval = 10
manager.add_miner(self)
self.start()

def push_work(self, template, extra_nonce):
if template is None:
workstr = "work %s %s %d" % ("0"*64, "0"*64, 0)
else:
work = template.get_work(extra_nonce)
workstr = "work %s %s %d" % (work, template.target, template.odo_key)
with self.lock:
if template is None:
self.work_items = []
else:
self.work_items.insert(0, (work, template, extra_nonce))
if len(self.work_items) > 2:
self.work_items.pop()
self.next_refresh = time.time() + self.refresh_interval
try:
self.send(workstr)
except socket.error as e:
# let the other thread clean it up
pass

def send(self, s):
with self.conn_lock:
self.conn.sendall((s + "\n").encode())

def submit(self, work):
with self.lock:
for work_item in self.work_items:
if work_item[0][0:152] == work[0:152]:
template = work_item[1]
extra_nonce = work_item[2]
submit_data = work + template.get_data(extra_nonce)
break
else:
return "stale"
try:
return rpc.submit_work(submit_data)
except (rpc.RpcError, socket.error) as e:
print("failed to submit: %s (errno %d)" % (e.strerror, e.errno));
return "error"

def run(self):
while True:
try:
data = self.conn.makefile().readline().rstrip()
if not data:
break
parts = data.split()
command, args = parts[0], parts[1:]
if command == "submit" and len(args) == 1:
result = self.submit(*args)
self.send("result %s" % result)
else:
print("unknown command: %s" % data)
except socket.error as e:
break
self.manager.remove_miner(self)
self.conn.close()

if __name__ == "__main__":
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((config.get("bind_addr"), config.get("listen_port")))
listener.listen(10)

manager = Manager(config.get("cbscript"))
manager.start()

callback = lambda t: manager.push_template(t)
threading.Thread(target=get_templates, args=(callback,)).start()

while True:
conn, addr = listener.accept()
Miner(conn, manager)

51 changes: 51 additions & 0 deletions src/pool/solo/rpc.py
@@ -0,0 +1,51 @@
# Copyright (C) 2019 MentalCollatz
#
# 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 <http://www.gnu.org/licenses/>.

import requests
import json

import config

class RpcError(Exception):
def __init__(self, **kwargs):
self.strerror = kwargs["message"]
self.errno = kwargs["code"]

def json_request(method, *params):
jdata = {"method": method, "params": params}
headers = {"Content-Type": "application/json", "Authorization": config.get("rpc_auth")}
response = requests.post(config.get("rpc_url"), headers=headers, json=jdata)

try:
data = response.json()
except ValueError as e:
if response.status_code != requests.codes.ok:
raise RpcError(code=response.status_code, message="HTTP status code")
raise RpcError(code=500, message=str(e))

if data["error"] is None:
return data["result"]
raise RpcError(**data["error"])

def get_block_template(longpollid):
params = {"rules":["segwit"]}
algo = "odo"
if longpollid is not None:
params["longpollid"] = longpollid
return json_request("getblocktemplate", params, algo)

def submit_work(submit_data):
return json_request("submitblock", submit_data) or "accepted"

0 comments on commit 511c973

Please sign in to comment.