Skip to content
This repository has been archived by the owner on May 13, 2022. It is now read-only.

add Dockerfile and entrypoint.py #636

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.git/
env/
54 changes: 54 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#
# Quick and easy joinmarket
#
# docker build -t "bwstitt/joinmarket:latest" .
#
# Copying of the code is delayed as long as possible so that rebuilding the container while developing is faster
# This also means some of the install steps aren't in the most obvious order and the virtualenv is outside the code
#

FROM bwstitt/python-jessie:python2

# Install packages for joinmarket
RUN docker-apt-install \
gcc \
libsodium13 \
python-dev

# i needed these when compiling myself, but new versions of pip with wheels save us
#libatlas-dev \
#libblas-dev \
#libfreetype6-dev \
#libpng12-dev \
#libsodium-dev \
#pkg-config \
#python-dev \

# install deps that don't depend on the code as the user and fix /pyenv/local/lib/python2.7/site-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
# TODO: double check that this actually builds the font caches
RUN chroot --userspec=abc / pip install \
"matplotlib==2.0.0" \
"scipy==0.19.0" \
&& chroot --userspec=abc / python -c \
"from matplotlib.font_manager import FontManager; print(FontManager())"

# copy requirements before code. this will make the image builds faster when code changes but requirements don't
COPY requirements.txt /src/
RUN chroot --userspec=abc / pip install -r /src/requirements.txt

# install the code
# todo: i wish copy would keep the user...
COPY . /src/
WORKDIR /src
RUN chown -R abc:abc .

# setup data volumes for logs and wallets
# todo: handle the blacklist and commitments
VOLUME /src/logs /src/wallets

USER abc
ENV MPLBACKEND=agg
ENTRYPOINT ["python", "/src/docker/entrypoint.py"]
CMD ["ob_watcher", "-H", "0.0.0.0"]

EXPOSE 62601
Empty file modified broadcast-tx.py
100644 → 100755
Empty file.
25 changes: 25 additions & 0 deletions check-config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import absolute_import

import sys

from joinmarket import get_log, load_program_config


log = get_log()


def main():
"""Simple command to make sure the config loads.

This will exit 1 if the config cannot be loaded or the blockchaininterface
doesn't respond.
"""
try:
load_program_config()
except Exception:
log.exception("Error while loading config")
return 1


if __name__ == "__main__":
sys.exit(main())
Empty file modified create-unsigned-tx.py
100644 → 100755
Empty file.
6 changes: 6 additions & 0 deletions docker/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
todo:
- explain how to setup Tor
- explain how to setup bitcoind
- explain how to setup the config (and figure out how to keep sensitive things out of git)
- maybe use click instead of argparse
- use --entry-help instead of --help so we can still see joinmarket's help
208 changes: 208 additions & 0 deletions docker/entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/usr/bin/env python2
"""Entrypoints for Docker containers to run joinmarket.

todo: wait_for_file should probably be a decorator
todo: either always force wallet.json or make waiting for the wallet smarter
"""
import argparse
import logging
import os
import os.path
import subprocess
import sys
import time


#
# Helpers
#

DEBUG_LOG_FORMAT = (
'-' * 80 + '\n' +
'%(asctime)s %(levelname)s in %(name)s @ %(threadName)s:\n' +
'%(pathname)s:%(lineno)d\n' +
'%(message)s\n' +
'-' * 80
)
DEFAULT_LOG_FORMAT = '%(asctime)s %(levelname)s: %(threadName)s: %(name)s: %(message)s' # noqa

log = logging.getLogger(__name__)


def run(*args):
"""Run a python command inside the joinmarket virtualenv.

Raises subprocess.CalledProcessError if the command fails.
"""
if not args:
raise ValueError("run needs at least one arg")

command_list = [sys.executable] + map(str, args)

log.info("Running %s...", command_list[1])
log.debug("Full command: %s", command_list)

return subprocess.check_call(command_list, env=os.environ)


def run_or_exit(*args):
"""Run a python command inside the joinmarket virtualenv.

Logs and exits if the command fails.
"""
try:
return run(*args)
except subprocess.CalledProcessError as e:
log.error("%s", e)
sys.exit(e.returncode)


def wait_for_config(*args):
"""Sleep until config loads.

Config loading includes bitcoind responding to getblockchaininfo

Todo: exponential backoff of the sleep. maybe log less, too
Todo: args here are hacky. make this function and the command seperate
"""
while True:
try:
run('check-config.py')
except subprocess.CalledProcessError as e:
# TODO: this is too verbose
log.error("Unable to load config: %s. Sleeping..." % e)
time.sleep(60)
else:
break

return True


def wait_for_file(filename, sleep=10):
"""Sleep until a given file exists."""
if os.path.exists(filename):
return

log.info("'%s' does not exist. Check the README", filename)

log.info("Sleeping until '%s' exists...", filename)
while not os.path.exists(filename):
time.sleep(sleep)

log.info("Found '%s'", filename)
return


#
# Commands
#

def get_parser():
"""Create an argument parser that routes to the command functions."""
# create the top-level parser
parser = argparse.ArgumentParser()
# todo: configurable log level
subparsers = parser.add_subparsers()

# create the parser for the "maker" command
parser_maker = subparsers.add_parser('maker')
parser_maker.set_defaults(func=maker)

# create the parser for the "ob_watcher" command
parser_ob_watcher = subparsers.add_parser('ob_watcher')
parser_ob_watcher.set_defaults(func=ob_watcher)

# create the parser for the "sendpayment" command
parser_sendpayment = subparsers.add_parser('sendpayment')
parser_sendpayment.set_defaults(func=sendpayment)

# create the parser for the "taker" command
parser_tumbler = subparsers.add_parser('tumbler')
parser_tumbler.set_defaults(func=tumbler)

# create the parser for the "wallet_tool" command
parser_wallet_tool = subparsers.add_parser('wallet_tool')
parser_wallet_tool.set_defaults(func=wallet_tool)

# other scripts might find waiting for the config helpful, too
parser_wait_for_config = subparsers.add_parser('wait_for_config')
parser_wait_for_config.set_defaults(func=wait_for_config)

return parser


def maker(args):
"""Earn Bitcoins and privacy."""
wallet_filename = 'wallet.json'
wait_for_file("wallets/%s" % wallet_filename)

# wait for bitcoind to respond
wait_for_config()

run_or_exit('yg-pe.py', wallet_filename, *args)


def ob_watcher(args):
"""Watch the orderbook."""
# wait for bitcoind to respond
# todo: although, why does the orderbook need bitcoind?
wait_for_config()

run_or_exit('ob-watcher.py', *args)


def sendpayment(args):
""""Send Bitcoins with privacy.

todo: make sure we only sendpayment with coins that have already been
joined at least once.
"""
wallet_filename = 'wallet.json'
wait_for_file("wallets/%s" % wallet_filename)

# wait for bitcoind to respond
wait_for_config()

run_or_exit('sendpayment.py', *args)


def tumbler(args):
""""Send Bitcoins with layers of privacy."""
wallet_filename = 'wallet.json'
wait_for_file("wallets/%s" % wallet_filename)

# wait for bitcoind to respond
wait_for_config()

run_or_exit('tumbler.py', *args)


def wallet_tool(args):
"""Inspect and manage your Bitcoin wallet."""
run_or_exit('wallet-tool.py', *args)


def main():
"""Manage joinmarket."""
parser = get_parser()
args, passthrough_args = parser.parse_known_args()

log_level = logging.DEBUG # todo: get log_level from args

if log_level == logging.DEBUG:
log_format = DEBUG_LOG_FORMAT
else:
log_format = DEFAULT_LOG_FORMAT

logging.basicConfig(
format=log_format,
level=log_level,
stream=sys.stdout,
)
log.debug("Hello!")

args.func(passthrough_args)


if __name__ == '__main__':
sys.exit(main())
2 changes: 1 addition & 1 deletion joinmarket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .slowaes import decryptData, encryptData
from .taker import Taker, OrderbookWatch, CoinJoinTX
from .wallet import AbstractWallet, BitcoinCoreInterface, Wallet, \
BitcoinCoreWallet
BitcoinCoreWallet, get_wallet_pass
from .configure import load_program_config, jm_single, get_p2pk_vbyte, \
get_network, jm_single, get_network, validate_address, get_irc_mchannels, \
check_utxo_blacklist
Expand Down
2 changes: 1 addition & 1 deletion joinmarket/blockchaininterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,7 @@ def add_watchonly_addresses(self, addr_list, wallet_name):
print('restart Bitcoin Core with -rescan if you\'re '
'recovering an existing wallet from backup seed')
print(' otherwise just restart this joinmarket script')
sys.exit(0)
sys.exit(2)

def sync_wallet(self, wallet, fast=False):
#trigger fast sync if the index_cache is available
Expand Down
14 changes: 8 additions & 6 deletions joinmarket/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def __init__(self, obj):
self.code = obj["code"]
self.message = obj["message"]

def __str__(self):
return "%s %s: %s" % (self.__class__.__name__, self.code, self.message)


class JsonRpcConnectionError(Exception):
"""
Expand Down Expand Up @@ -78,19 +81,18 @@ def queryHTTP(self, obj):
conn.request("POST", "", body, headers)
response = conn.getresponse()

if response.status == 401:
if response.status in [401, 403]:
conn.close()
raise JsonRpcConnectionError(
"authentication for JSON-RPC failed")

# All of the codes below are 'fine' from a JSON-RPC point of view.
if response.status not in [200, 404, 500]:
conn.close()
raise JsonRpcConnectionError("unknown error in JSON-RPC")

data = response.read()
conn.close()

# All of the codes below are 'fine' from a JSON-RPC point of view.
if response.status not in [200, 404, 500]:
raise JsonRpcConnectionError("unknown error %s in JSON-RPC: %s" % (response.status, data))

return json.loads(data)

except JsonRpcConnectionError as exc:
Expand Down
Loading