Browse Source

basicswap-prepare can enable and disable tor config.

pivx
tecnovert 2 years ago
parent
commit
d1e015962c
No known key found for this signature in database GPG Key ID: 8ED6D8750C4E3F93
  1. 22
      basicswap/basicswap.py
  2. 4
      basicswap/basicswap_util.py
  3. 25
      basicswap/interface_btc.py
  4. 15
      basicswap/interface_part.py
  5. 2
      basicswap/interface_xmr.py
  6. 350
      basicswap/util.py
  7. 187
      basicswap/util/__init__.py
  8. 128
      basicswap/util/address.py
  9. 38
      basicswap/util/ecc.py
  10. 31
      basicswap/util/rfc2440.py
  11. 71
      basicswap/util/script.py
  12. 355
      bin/basicswap_prepare.py
  13. 36
      doc/tor.md
  14. 7
      docker/docker-compose.yml
  15. 44
      docker/docker-compose_with_tor.yml
  16. 2
      docker/production/.env
  17. 6
      docker/production/docker-compose.yml
  18. 2
      docker/production/example.env
  19. 8
      docker/tor/Dockerfile
  20. 1
      requirements.txt
  21. 1
      setup.py
  22. 4
      tests/basicswap/extended/test_network.py
  23. 2
      tests/basicswap/extended/test_nmc.py
  24. 12
      tests/basicswap/test_other.py
  25. 4
      tests/basicswap/test_xmr.py

22
basicswap/basicswap.py

@ -38,23 +38,27 @@ from . import __version__
from .rpc_xmr import make_xmr_rpc2_func
from .util import (
TemporaryError,
pubkeyToAddress,
format_amount,
format_timestamp,
encodeAddress,
decodeAddress,
DeserialiseNum,
decodeWif,
toWIF,
getKeyID,
make_int,
getP2SHScriptForHash,
getP2WSH,
ensure,
)
from .util.script import (
getP2WSH,
getP2SHScriptForHash,
)
from .util.address import (
toWIF,
getKeyID,
decodeWif,
decodeAddress,
encodeAddress,
pubkeyToAddress,
)
from .chainparams import (
chainparams,
Coins,
chainparams,
)
from .script import (
OpCodes,

4
basicswap/basicswap_util.py

@ -8,9 +8,9 @@
import struct
import hashlib
from enum import IntEnum, auto
from .util import (
encodeAddress,
from .util.address import (
decodeAddress,
encodeAddress,
)
from .chainparams import (
chainparams,

25
basicswap/interface_btc.py

@ -16,17 +16,26 @@ from basicswap.contrib.test_framework import segwit_addr
from .util import (
dumpj,
toWIF,
ensure,
make_int,
b2h, i2b, b2i, i2h)
from .util.ecc import (
ep,
pointToCPK, CPKToPoint,
getSecretInt)
from .util.script import (
decodeScriptNum,
getCompactSizeLen,
SerialiseNumCompact,
getWitnessElementLen,
)
from .util.address import (
toWIF,
b58encode,
decodeWif,
decodeAddress,
decodeScriptNum,
pubkeyToAddress,
getCompactSizeLen,
SerialiseNumCompact,
getWitnessElementLen)
)
from coincurve.keys import (
PrivateKey,
PublicKey)
@ -38,12 +47,6 @@ from coincurve.ecdsaotves import (
ecdsaotves_dec_sig,
ecdsaotves_rec_enc_key)
from .ecc_util import (
ep,
pointToCPK, CPKToPoint,
getSecretInt,
b2h, i2b, b2i, i2h)
from .contrib.test_framework.messages import (
COIN,
COutPoint,

15
basicswap/interface_part.py

@ -16,17 +16,20 @@ from .contrib.test_framework.script import (
OP_0,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
)
from .ecc_util import i2b
from .util import (
toWIF,
i2b,
ensure,
make_int,
getP2WSH,
TemporaryError,
)
from .util.script import (
getP2WSH,
getCompactSizeLen,
encodeStealthAddress,
getWitnessElementLen)
getWitnessElementLen,
)
from .util.address import (
toWIF,
encodeStealthAddress)
from .chainparams import Coins, chainparams
from .interface_btc import BTCInterface

2
basicswap/interface_xmr.py

@ -31,7 +31,7 @@ from .rpc_xmr import (
make_xmr_rpc_func,
make_xmr_rpc2_func,
make_xmr_wallet_rpc_func)
from .ecc_util import (
from .util import (
b2i, b2h)
from .chainparams import CoinInterface, Coins

350
basicswap/util.py

@ -1,350 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018-2021 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import time
import struct
import decimal
import hashlib
from .script import OpCodes
from .contrib.segwit_addr import bech32_decode, convertbits, bech32_encode
COIN = 100000000
decimal_ctx = decimal.Context()
decimal_ctx.prec = 20
class TemporaryError(ValueError):
pass
def ensure(v, err_string):
if not v:
raise ValueError(err_string)
def toBool(s) -> bool:
return s.lower() in ["1", "true"]
def jsonDecimal(obj):
if isinstance(obj, decimal.Decimal):
return str(obj)
raise TypeError
def dumpj(jin, indent=4):
return json.dumps(jin, indent=indent, default=jsonDecimal)
def dumpje(jin):
return json.dumps(jin, default=jsonDecimal).replace('"', '\\"')
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
def b58decode(v, length=None):
long_value = 0
for (i, c) in enumerate(v[::-1]):
ofs = __b58chars.find(c)
if ofs < 0:
return None
long_value += ofs * (58**i)
result = bytes()
while long_value >= 256:
div, mod = divmod(long_value, 256)
result = bytes((mod,)) + result
long_value = div
result = bytes((long_value,)) + result
nPad = 0
for c in v:
if c == __b58chars[0]:
nPad += 1
else:
break
pad = bytes((0,)) * nPad
result = pad + result
if length is not None and len(result) != length:
return None
return result
def b58encode(v):
long_value = 0
for (i, c) in enumerate(v[::-1]):
long_value += (256**i) * c
result = ''
while long_value >= 58:
div, mod = divmod(long_value, 58)
result = __b58chars[mod] + result
long_value = div
result = __b58chars[long_value] + result
# leading 0-bytes in the input become leading-1s
nPad = 0
for c in v:
if c == 0:
nPad += 1
else:
break
return (__b58chars[0] * nPad) + result
def decodeWif(encoded_key):
key = b58decode(encoded_key)[1:-4]
if len(key) == 33:
return key[:-1]
return key
def toWIF(prefix_byte, b, compressed=True):
b = bytes((prefix_byte,)) + b
if compressed:
b += bytes((0x01,))
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)
def bech32Decode(hrp, addr):
hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return None
decoded = convertbits(data, 5, 8, False)
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
return None
return bytes(decoded)
def bech32Encode(hrp, data):
ret = bech32_encode(hrp, convertbits(data, 8, 5))
if bech32Decode(hrp, ret) is None:
return None
return ret
def decodeAddress(address_str):
b58_addr = b58decode(address_str)
if b58_addr is not None:
address = b58_addr[:-4]
checksum = b58_addr[-4:]
assert(hashlib.sha256(hashlib.sha256(address).digest()).digest()[:4] == checksum), 'Checksum mismatch'
return b58_addr[:-4]
return None
def encodeAddress(address):
checksum = hashlib.sha256(hashlib.sha256(address).digest()).digest()
return b58encode(address + checksum[0:4])
def getKeyID(bytes):
data = hashlib.sha256(bytes).digest()
return hashlib.new("ripemd160", data).digest()
def pubkeyToAddress(prefix, pubkey):
return encodeAddress(bytes((prefix,)) + getKeyID(pubkey))
def SerialiseNum(n):
if n == 0:
return bytes((0x00,))
if n > 0 and n <= 16:
return bytes((0x50 + n,))
rv = bytearray()
neg = n < 0
absvalue = -n if neg else n
while(absvalue):
rv.append(absvalue & 0xff)
absvalue >>= 8
if rv[-1] & 0x80:
rv.append(0x80 if neg else 0)
elif neg:
rv[-1] |= 0x80
return bytes((len(rv),)) + rv
def DeserialiseNum(b, o=0) -> int:
if b[o] == 0:
return 0
if b[o] > 0x50 and b[o] <= 0x50 + 16:
return b[o] - 0x50
v = 0
nb = b[o]
o += 1
for i in range(0, nb):
v |= b[o + i] << (8 * i)
# If the input vector's most significant byte is 0x80, remove it from the result's msb and return a negative.
if b[o + nb - 1] & 0x80:
return -(v & ~(0x80 << (8 * (nb - 1))))
return v
def decodeScriptNum(script_bytes, o):
v = 0
num_len = script_bytes[o]
if num_len >= OpCodes.OP_1 and num_len <= OpCodes.OP_16:
return((num_len - OpCodes.OP_1) + 1, 1)
if num_len > 4:
raise ValueError('Bad scriptnum length') # Max 4 bytes
if num_len + o >= len(script_bytes):
raise ValueError('Bad script length')
o += 1
for i in range(num_len):
b = script_bytes[o + i]
# Negative flag set in last byte, if num is positive and > 0x80 an extra 0x00 byte will be appended
if i == num_len - 1 and b & 0x80:
b &= (~(0x80) & 0xFF)
v += int(b) << 8 * i
v *= -1
else:
v += int(b) << 8 * i
return(v, 1 + num_len)
def getCompactSizeLen(v):
# Compact Size
if v < 253:
return 1
if v <= 0xffff: # USHRT_MAX
return 3
if v <= 0xffffffff: # UINT_MAX
return 5
if v <= 0xffffffffffffffff: # UINT_MAX
return 9
raise ValueError('Value too large')
def getWitnessElementLen(v):
return getCompactSizeLen(v) + v
def SerialiseNumCompact(v):
if v < 253:
return bytes((v,))
if v <= 0xffff: # USHRT_MAX
return struct.pack("<BH", 253, v)
if v <= 0xffffffff: # UINT_MAX
return struct.pack("<BI", 254, v)
if v <= 0xffffffffffffffff: # UINT_MAX
return struct.pack("<BQ", 255, v)
raise ValueError('Value too large')
def float_to_str(f):
# stackoverflow.com/questions/38847690
d1 = decimal_ctx.create_decimal(repr(f))
return format(d1, 'f')
def make_int(v, scale=8, r=0): # r = 0, no rounding, fail, r > 0 round up, r < 0 floor
if type(v) == float:
v = float_to_str(v)
elif type(v) == int:
return v * 10 ** scale
sign = 1
if v[0] == '-':
v = v[1:]
sign = -1
ep = 10 ** scale
have_dp = False
rv = 0
for c in v:
if c == '.':
rv *= ep
have_dp = True
continue
if not c.isdigit():
raise ValueError('Invalid char: ' + c)
if have_dp:
ep //= 10
if ep <= 0:
if r == 0:
raise ValueError('Mantissa too long')
if r > 0:
# Round up
if int(c) > 4:
rv += 1
break
rv += ep * int(c)
else:
rv = rv * 10 + int(c)
if not have_dp:
rv *= ep
return rv * sign
def validate_amount(amount, scale=8) -> bool:
str_amount = float_to_str(amount) if type(amount) == float else str(amount)
has_decimal = False
for c in str_amount:
if c == '.' and not has_decimal:
has_decimal = True
continue
if not c.isdigit():
raise ValueError('Invalid amount')
ar = str_amount.split('.')
if len(ar) > 1 and len(ar[1]) > scale:
raise ValueError('Too many decimal places in amount {}'.format(str_amount))
return True
def format_amount(i, display_scale, scale=None):
if not isinstance(i, int):
raise ValueError('Amount must be an integer.') # Raise error instead of converting as amounts should always be integers
if scale is None:
scale = display_scale
ep = 10 ** scale
n = abs(i)
quotient = n // ep
remainder = n % ep
if display_scale != scale:
remainder %= (10 ** display_scale)
rv = '{}.{:0>{scale}}'.format(quotient, remainder, scale=display_scale)
if i < 0:
rv = '-' + rv
return rv
def format_timestamp(value, with_seconds=False):
str_format = '%Y-%m-%d %H:%M'
if with_seconds:
str_format += ':%S'
str_format += ' %Z'
return time.strftime(str_format, time.localtime(value))
def getP2SHScriptForHash(p2sh):
return bytes((OpCodes.OP_HASH160, 0x14)) \
+ p2sh \
+ bytes((OpCodes.OP_EQUAL,))
def getP2WSH(script):
return bytes((OpCodes.OP_0, 0x20)) + hashlib.sha256(script).digest()
def encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey):
data = bytes((0x00,))
data += scan_pubkey
data += bytes((0x01,))
data += spend_pubkey
data += bytes((0x00,)) # number_signatures - unused
data += bytes((0x00,)) # num prefix bits
b = bytes((prefix_byte,)) + data
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)

187
basicswap/util/__init__.py

@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import time
import decimal
COIN = 100000000
decimal_ctx = decimal.Context()
decimal_ctx.prec = 20
class TemporaryError(ValueError):
pass
def ensure(v, err_string):
if not v:
raise ValueError(err_string)
def toBool(s) -> bool:
return s.lower() in ['1', 'true']
def jsonDecimal(obj):
if isinstance(obj, decimal.Decimal):
return str(obj)
raise TypeError
def dumpj(jin, indent=4):
return json.dumps(jin, indent=indent, default=jsonDecimal)
def dumpje(jin):
return json.dumps(jin, default=jsonDecimal).replace('"', '\\"')
def SerialiseNum(n):
if n == 0:
return bytes((0x00,))
if n > 0 and n <= 16:
return bytes((0x50 + n,))
rv = bytearray()
neg = n < 0
absvalue = -n if neg else n
while(absvalue):
rv.append(absvalue & 0xff)
absvalue >>= 8
if rv[-1] & 0x80:
rv.append(0x80 if neg else 0)
elif neg:
rv[-1] |= 0x80
return bytes((len(rv),)) + rv
def DeserialiseNum(b, o=0) -> int:
if b[o] == 0:
return 0
if b[o] > 0x50 and b[o] <= 0x50 + 16:
return b[o] - 0x50
v = 0
nb = b[o]
o += 1
for i in range(0, nb):
v |= b[o + i] << (8 * i)
# If the input vector's most significant byte is 0x80, remove it from the result's msb and return a negative.
if b[o + nb - 1] & 0x80:
return -(v & ~(0x80 << (8 * (nb - 1))))
return v
def float_to_str(f):
# stackoverflow.com/questions/38847690
d1 = decimal_ctx.create_decimal(repr(f))
return format(d1, 'f')
def make_int(v, scale=8, r=0): # r = 0, no rounding, fail, r > 0 round up, r < 0 floor
if type(v) == float:
v = float_to_str(v)
elif type(v) == int:
return v * 10 ** scale
sign = 1
if v[0] == '-':
v = v[1:]
sign = -1
ep = 10 ** scale
have_dp = False
rv = 0
for c in v:
if c == '.':
rv *= ep
have_dp = True
continue
if not c.isdigit():
raise ValueError('Invalid char: ' + c)
if have_dp:
ep //= 10
if ep <= 0:
if r == 0:
raise ValueError('Mantissa too long')
if r > 0:
# Round up
if int(c) > 4:
rv += 1
break
rv += ep * int(c)
else:
rv = rv * 10 + int(c)
if not have_dp:
rv *= ep
return rv * sign
def validate_amount(amount, scale=8) -> bool:
str_amount = float_to_str(amount) if type(amount) == float else str(amount)
has_decimal = False
for c in str_amount:
if c == '.' and not has_decimal:
has_decimal = True
continue
if not c.isdigit():
raise ValueError('Invalid amount')
ar = str_amount.split('.')
if len(ar) > 1 and len(ar[1]) > scale:
raise ValueError('Too many decimal places in amount {}'.format(str_amount))
return True
def format_amount(i, display_scale, scale=None):
if not isinstance(i, int):
raise ValueError('Amount must be an integer.') # Raise error instead of converting as amounts should always be integers
if scale is None:
scale = display_scale
ep = 10 ** scale
n = abs(i)
quotient = n // ep
remainder = n % ep
if display_scale != scale:
remainder %= (10 ** display_scale)
rv = '{}.{:0>{scale}}'.format(quotient, remainder, scale=display_scale)
if i < 0:
rv = '-' + rv
return rv
def format_timestamp(value, with_seconds=False):
str_format = '%Y-%m-%d %H:%M'
if with_seconds:
str_format += ':%S'
str_format += ' %Z'
return time.strftime(str_format, time.localtime(value))
def b2i(b) -> int:
# bytes32ToInt
return int.from_bytes(b, byteorder='big')
def i2b(i: int) -> bytes:
# intToBytes32
return i.to_bytes(32, byteorder='big')
def b2h(b: bytes) -> str:
return b.hex()
def h2b(h: str) -> bytes:
if h.startswith('0x'):
h = h[2:]
return bytes.fromhex(h)
def i2h(x):
return b2h(i2b(x))

128
basicswap/util/address.py

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib
from basicswap.contrib.segwit_addr import bech32_decode, convertbits, bech32_encode
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
def b58decode(v, length=None):
long_value = 0
for (i, c) in enumerate(v[::-1]):
ofs = __b58chars.find(c)
if ofs < 0:
return None
long_value += ofs * (58**i)
result = bytes()
while long_value >= 256:
div, mod = divmod(long_value, 256)
result = bytes((mod,)) + result
long_value = div
result = bytes((long_value,)) + result
nPad = 0
for c in v:
if c == __b58chars[0]:
nPad += 1
else:
break
pad = bytes((0,)) * nPad
result = pad + result
if length is not None and len(result) != length:
return None
return result
def b58encode(v):
long_value = 0
for (i, c) in enumerate(v[::-1]):
long_value += (256**i) * c
result = ''
while long_value >= 58:
div, mod = divmod(long_value, 58)
result = __b58chars[mod] + result
long_value = div
result = __b58chars[long_value] + result
# leading 0-bytes in the input become leading-1s
nPad = 0
for c in v:
if c == 0:
nPad += 1
else:
break
return (__b58chars[0] * nPad) + result
def encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey):
data = bytes((0x00,))
data += scan_pubkey
data += bytes((0x01,))
data += spend_pubkey
data += bytes((0x00,)) # number_signatures - unused
data += bytes((0x00,)) # num prefix bits
b = bytes((prefix_byte,)) + data
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)
def decodeWif(encoded_key):
key = b58decode(encoded_key)[1:-4]
if len(key) == 33:
return key[:-1]
return key
def toWIF(prefix_byte, b, compressed=True):
b = bytes((prefix_byte,)) + b
if compressed:
b += bytes((0x01,))
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)
def getKeyID(bytes):
data = hashlib.sha256(bytes).digest()
return hashlib.new('ripemd160', data).digest()
def bech32Decode(hrp, addr):
hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return None
decoded = convertbits(data, 5, 8, False)
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
return None
return bytes(decoded)
def bech32Encode(hrp, data):
ret = bech32_encode(hrp, convertbits(data, 8, 5))
if bech32Decode(hrp, ret) is None:
return None
return ret
def decodeAddress(address_str):
b58_addr = b58decode(address_str)
if b58_addr is not None:
address = b58_addr[:-4]
checksum = b58_addr[-4:]
assert(hashlib.sha256(hashlib.sha256(address).digest()).digest()[:4] == checksum), 'Checksum mismatch'
return b58_addr[:-4]
return None
def encodeAddress(address):
checksum = hashlib.sha256(hashlib.sha256(address).digest()).digest()
return b58encode(address + checksum[0:4])
def pubkeyToAddress(prefix, pubkey):
return encodeAddress(bytes((prefix,)) + getKeyID(pubkey))

38
basicswap/ecc_util.py → basicswap/util/ecc.py

@ -2,11 +2,11 @@
# -*- coding: utf-8 -*-
import os
import codecs
import hashlib
import secrets
from .contrib.ellipticcurve import CurveFp, Point, INFINITY, jacobi_symbol
from basicswap.contrib.ellipticcurve import CurveFp, Point, INFINITY, jacobi_symbol
from . import i2b
class ECCParameters():
@ -37,31 +37,9 @@ def ToDER(P) -> bytes:
return bytes((4, )) + int(P.x()).to_bytes(32, byteorder='big') + int(P.y()).to_bytes(32, byteorder='big')
def bytes32ToInt(b) -> int:
return int.from_bytes(b, byteorder='big')
def intToBytes32(i: int) -> bytes:
return i.to_bytes(32, byteorder='big')
def intToBytes32_le(i: int) -> bytes:
return i.to_bytes(32, byteorder='little')
def bytesToHexStr(b: bytes) -> str:
return codecs.encode(b, 'hex').decode('utf-8')
def hexStrToBytes(h: str) -> bytes:
if h.startswith('0x'):
h = h[2:]
return bytes.fromhex(h)
def getSecretBytes() -> bytes:
i = 1 + secrets.randbelow(ep.o - 1)
return intToBytes32(i)
return i2b(i)
def getSecretInt() -> int:
@ -189,16 +167,6 @@ def hash256(inb):
return hashlib.sha256(inb).digest()
i2b = intToBytes32
b2i = bytes32ToInt
b2h = bytesToHexStr
h2b = hexStrToBytes
def i2h(x):
return b2h(i2b(x))
def testEccUtils():
print('testEccUtils()')

31
basicswap/util/rfc2440.py

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
import hashlib
import secrets
def rfc2440_hash_password(password, salt=None):
# Match tor --hash-password
# secret_to_key_rfc2440
EXPBIAS = 6
c = 96
count = (16 + (c & 15)) << ((c >> 4) + EXPBIAS)
if salt is None:
salt = secrets.token_bytes(8)
assert(len(salt) == 8)
hashbytes = salt + password.encode('utf-8')
len_hashbytes = len(hashbytes)
h = hashlib.sha1()
while count > 0:
if count >= len_hashbytes:
h.update(hashbytes)
count -= len_hashbytes
continue
h.update(hashbytes[:count])
break
rv = '16:' + salt.hex() + '60' + h.hexdigest()
return rv.upper()

71
basicswap/util/script.py

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import struct
import hashlib
from basicswap.script import OpCodes
def decodeScriptNum(script_bytes, o):
v = 0
num_len = script_bytes[o]
if num_len >= OpCodes.OP_1 and num_len <= OpCodes.OP_16:
return((num_len - OpCodes.OP_1) + 1, 1)
if num_len > 4:
raise ValueError('Bad scriptnum length') # Max 4 bytes
if num_len + o >= len(script_bytes):
raise ValueError('Bad script length')
o += 1
for i in range(num_len):
b = script_bytes[o + i]
# Negative flag set in last byte, if num is positive and > 0x80 an extra 0x00 byte will be appended
if i == num_len - 1 and b & 0x80:
b &= (~(0x80) & 0xFF)
v += int(b) << 8 * i
v *= -1
else:
v += int(b) << 8 * i
return(v, 1 + num_len)
def getP2SHScriptForHash(p2sh):
return bytes((OpCodes.OP_HASH160, 0x14)) \
+ p2sh \
+ bytes((OpCodes.OP_EQUAL,))
def getP2WSH(script):
return bytes((OpCodes.OP_0, 0x20)) + hashlib.sha256(script).digest()
def SerialiseNumCompact(v):
if v < 253:
return bytes((v,))
if v <= 0xffff: # USHRT_MAX
return struct.pack("<BH", 253, v)
if v <= 0xffffffff: # UINT_MAX
return struct.pack("<BI", 254, v)
if v <= 0xffffffffffffffff: # UINT_MAX
return struct.pack("<BQ", 255, v)
raise ValueError('Value too large')
def getCompactSizeLen(v):
# Compact Size
if v < 253:
return 1
if v <= 0xffff: # USHRT_MAX
return 3
if v <= 0xffffffff: # UINT_MAX
return 5
if v <= 0xffffffffffffffff: # UINT_MAX
return 9
raise ValueError('Value too large')
def getWitnessElementLen(v):
return getCompactSizeLen(v) + v

355
bin/basicswap_prepare.py

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# Copyright (c) 2019-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -11,6 +11,8 @@ import json
import mmap
import stat
import gnupg
import socks
import shutil
import signal
import socket
import hashlib
@ -28,6 +30,8 @@ from basicswap.rpc import (
)
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import Coins
from basicswap.util import toBool
from basicswap.util.rfc2440 import rfc2440_hash_password
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from bin.basicswap_run import startDaemon, startXmrWalletDaemon
@ -79,6 +83,10 @@ LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19795))
BTC_RPC_PORT = int(os.getenv('BTC_RPC_PORT', 19796))
NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19798))
PART_ONION_PORT = int(os.getenv('PART_ONION_PORT', 51734))
LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 19795)) # Still on 0.18 codebase, same port
BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334))
PART_RPC_USER = os.getenv('PART_RPC_USER', '')
PART_RPC_PWD = os.getenv('PART_RPC_PWD', '')
BTC_RPC_USER = os.getenv('BTC_RPC_USER', '')
@ -86,9 +94,21 @@ BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '')
LTC_RPC_USER = os.getenv('LTC_RPC_USER', '')
LTC_RPC_PWD = os.getenv('LTC_RPC_PWD', '')
COINS_BIND_IP = os.getenv('COINS_BIND_IP', '127.0.0.1')
COINS_RPCBIND_IP = os.getenv('COINS_RPCBIND_IP', '127.0.0.1')
TOR_PROXY_HOST = os.getenv('TOR_PROXY_HOST', '127.0.0.1')
TOR_PROXY_PORT = int(os.getenv('TOR_PROXY_PORT', 9050))
TOR_CONTROL_PORT = int(os.getenv('TOR_CONTROL_PORT', 9051))
TOR_DNS_PORT = int(os.getenv('TOR_DNS_PORT', 5353))
TEST_TOR_PROXY = toBool(os.getenv('TEST_TOR_PROXY', 'true')) # Expects a known exit node
TEST_ONION_LINK = toBool(os.getenv('TEST_ONION_LINK', 'false'))
extract_core_overwrite = True
use_tor_proxy = False
default_socket = socket.socket
default_socket_timeout = socket.getdefaulttimeout()
default_socket_getaddrinfo = socket.getaddrinfo
def make_reporthook():
@ -109,18 +129,64 @@ def make_reporthook():
return reporthook
def downloadFile(url, path):
logger.info('Downloading file %s', url)
logger.info('To %s', path)
def getaddrinfo(*args):
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))]
def setConnectionParameters():
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
# Set one second timeout for urlretrieve connections
old_timeout = socket.getdefaulttimeout()
if use_tor_proxy:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, TOR_PROXY_HOST, TOR_PROXY_PORT, rdns=True)
socket.socket = socks.socksocket
socket.getaddrinfo = getaddrinfo # Without this accessing .onion links would fail
# Set low timeout for urlretrieve connections
socket.setdefaulttimeout(5)
urlretrieve(url, path, make_reporthook())
socket.setdefaulttimeout(old_timeout)
def popConnectionParameters():
if use_tor_proxy:
socket.socket = default_socket
socket.getaddrinfo = default_socket_getaddrinfo
socket.setdefaulttimeout(default_socket_timeout)
def downloadFile(url, path):
logger.info('Downloading file %s', url)
logger.info('To %s', path)
try:
setConnectionParameters()
urlretrieve(url, path, make_reporthook())
finally:
popConnectionParameters()
def downloadBytes(url):
try:
setConnectionParameters()
return urllib.request.urlopen(url).read()
finally:
popConnectionParameters()
def testTorConnection():
test_url = 'https://check.torproject.org/'
logger.info('Testing TOR connection at: ' + test_url)
test_response = downloadBytes(test_url).decode('utf-8')
assert('Congratulations. This browser is configured to use Tor.' in test_response)
logger.info('TOR is working.')
def testOnionLink():
test_url = 'http://jqyzxhjk6psc6ul5jnfwloamhtyh7si74b4743k2qgpskwwxrzhsxmad.onion'
logger.info('Testing onion site: ' + test_url)
test_response = downloadBytes(test_url).decode('utf-8')
assert('The Tor Project\'s free software protects your privacy online.' in test_response)
logger.info('Onion links work.')
def extractCore(coin, version_pair, settings, bin_dir, release_path):
@ -289,7 +355,7 @@ def prepareCore(coin, version_pair, settings, data_dir):
pubkeyurl = 'https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc'
logger.info('Importing public key from url: ' + pubkeyurl)
rv = gpg.import_keys(urllib.request.urlopen(pubkeyurl).read())
rv = gpg.import_keys(downloadBytes(pubkeyurl))
print('import_keys', rv)
assert('F0AF4D462A0BDF92' in rv.fingerprints[0])
gpg.trust_keys(rv.fingerprints[0], 'TRUST_FULLY')
@ -304,7 +370,8 @@ def prepareCore(coin, version_pair, settings, data_dir):
pubkeyurl = 'https://raw.githubusercontent.com/tecnovert/basicswap/master/gitianpubkeys/{}_{}.pgp'.format(coin, signing_key_name)
logger.info('Importing public key from url: ' + pubkeyurl)
rv = gpg.import_keys(urllib.request.urlopen(pubkeyurl).read())
rv = gpg.import_keys(downloadBytes(pubkeyurl))
for key in rv.fingerprints:
gpg.trust_keys(key, 'TRUST_FULLY')
@ -319,7 +386,7 @@ def prepareCore(coin, version_pair, settings, data_dir):
extractCore(coin, version_pair, settings, bin_dir, release_path)
def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False):
def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False, tor_control_password=None):
core_settings = settings['chainclients'][coin]
bin_dir = core_settings['bindir']
data_dir = core_settings['datadir']
@ -343,11 +410,16 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
fp.write('testnet=1\n')
fp.write('data-dir={}\n'.format(data_dir))
fp.write('rpc-bind-port={}\n'.format(core_settings['rpcport']))
fp.write('rpc-bind-ip={}\n'.format(COINS_BIND_IP))
fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
fp.write('zmq-rpc-bind-port={}\n'.format(core_settings['zmqport']))
fp.write('zmq-rpc-bind-ip={}\n'.format(COINS_BIND_IP))
fp.write('zmq-rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
fp.write('prune-blockchain=1\n')
if tor_control_password is not None:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
fp.write('proxy-allow-dns-leaks=0\n')
fp.write('no-igd=1\n')
wallets_dir = core_settings.get('walletsdir', data_dir)
if not os.path.exists(wallets_dir):
os.makedirs(wallets_dir)
@ -361,11 +433,15 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
fp.write('untrusted-daemon=1\n')
fp.write('no-dns=1\n')
fp.write('rpc-bind-port={}\n'.format(core_settings['walletrpcport']))
fp.write('rpc-bind-ip={}\n'.format(COINS_BIND_IP))
fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
fp.write('wallet-dir={}\n'.format(os.path.join(data_dir, 'wallets')))
fp.write('log-file={}\n'.format(os.path.join(data_dir, 'wallet.log')))
fp.write('shared-ringdb-dir={}\n'.format(os.path.join(data_dir, 'shared-ringdb')))
fp.write('rpc-login={}:{}\n'.format(core_settings['walletrpcuser'], core_settings['walletrpcpassword']))
if tor_control_password is not None:
if not core_settings['manage_daemon']:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
return
core_conf_path = os.path.join(data_dir, coin + '.conf')
if os.path.exists(core_conf_path):
@ -380,20 +456,28 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
else:
logger.warning('Unknown chain %s', chain)
if COINS_BIND_IP != '127.0.0.1':
if COINS_RPCBIND_IP != '127.0.0.1':
fp.write('rpcallowip=127.0.0.1\n')
fp.write('rpcallowip=172.0.0.0/8\n') # Allow 172.x.x.x, range used by docker
fp.write('rpcbind={}\n'.format(COINS_BIND_IP))
fp.write('rpcbind={}\n'.format(COINS_RPCBIND_IP))
fp.write('rpcport={}\n'.format(core_settings['rpcport']))
fp.write('printtoconsole=0\n')
fp.write('daemon=0\n')
fp.write('wallet=wallet.dat\n')
if tor_control_password is not None:
onionport = core_settings['onionport']
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
fp.write(f'torpassword={tor_control_password}\n')
fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n')
# -listen is automatically set in InitParameterInteraction when bind is set
fp.write(f'bind=0.0.0.0:{onionport}=onion\n')
salt = generate_salt(16)
if coin == 'particl':
fp.write('debugexclude=libevent\n')
fp.write('zmqpubsmsg=tcp://{}:{}\n'.format(COINS_BIND_IP, settings['zmqport']))
fp.write('zmqpubsmsg=tcp://{}:{}\n'.format(COINS_RPCBIND_IP, settings['zmqport']))
fp.write('spentindex=1\n')
fp.write('txindex=1\n')
fp.write('staking=0\n')
@ -421,6 +505,140 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
callrpc_cli(bin_dir, data_dir, chain, '-wallet=wallet.dat create', wallet_util)
def write_torrc(data_dir, tor_control_password):
tor_dir = os.path.join(data_dir, 'tor')
if not os.path.exists(tor_dir):
os.makedirs(tor_dir)
torrc_path = os.path.join(tor_dir, 'torrc')
if os.path.exists(torrc_path):
logger.info(f'torrc file exists at {torrc_path}.')
return
tor_control_hash = rfc2440_hash_password(tor_control_password)
with open(torrc_path, 'w') as fp:
fp.write(f'SocksPort 0.0.0.0:{TOR_PROXY_PORT}\n')
fp.write(f'ControlPort 0.0.0.0:{TOR_CONTROL_PORT}\n')
fp.write(f'DNSPort 0.0.0.0:{TOR_DNS_PORT}\n')
fp.write(f'HashedControlPassword {tor_control_hash}\n')
def addTorSettings(settings, tor_control_password):
settings['tor_control_password'] = tor_control_password
settings['use_tor'] = True
settings['tor_proxy_host'] = TOR_PROXY_HOST
settings['tor_proxy_port'] = TOR_PROXY_PORT
settings['tor_control_port'] = TOR_CONTROL_PORT
def modify_tor_config(settings, coin, tor_control_password=None, enable=False):
coin_settings = settings['chainclients'][coin]
data_dir = coin_settings['datadir']
if coin == 'monero':
core_conf_path = os.path.join(data_dir, coin + 'd.conf')
if not os.path.exists(core_conf_path):
exitWithError('{} does not exist'.format(core_conf_path))
wallets_dir = coin_settings.get('walletsdir', data_dir)
wallet_conf_path = os.path.join(wallets_dir, coin + '_wallet.conf')
if not os.path.exists(wallet_conf_path):
exitWithError('{} does not exist'.format(wallet_conf_path))
# Backup
shutil.copyfile(core_conf_path, core_conf_path + '.last')
shutil.copyfile(wallet_conf_path, wallet_conf_path + '.last')
daemon_tor_settings = ('proxy=', 'proxy-allow-dns-leak=', 'no-igd=')
with open(core_conf_path, 'w') as fp:
with open(core_conf_path + '.last') as fp_in:
# Disable tor first
for line in fp_in:
skip_line = False
for setting in daemon_tor_settings:
if line.startswith(setting):
skip_line = True
break
if not skip_line:
fp.write(line)
if enable:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
fp.write('proxy-allow-dns-leaks=0\n')
fp.write('no-igd=1\n')
wallet_tor_settings = ('proxy=')
with open(wallet_conf_path, 'w') as fp:
with open(wallet_conf_path + '.last') as fp_in:
# Disable tor first
for line in fp_in:
skip_line = False
for setting in wallet_tor_settings:
if line.startswith(setting):
skip_line = True
break
if not skip_line:
fp.write(line)
if enable:
if not coin_settings['manage_daemon']:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
return
config_path = os.path.join(data_dir, coin + '.conf')
if not os.path.exists(config_path):
exitWithError('{} does not exist'.format(config_path))
if 'onionport' not in coin_settings:
default_onionport = 0
if coin == 'bitcoin':
default_onionport = BTC_ONION_PORT
elif coin == 'particl':
default_onionport = PART_ONION_PORT
elif coin == 'litecoin':
default_onionport = LTC_ONION_PORT
else:
exitWithError('Unknown default onion listening port for {}'.format(coin))
coin_settings['onionport'] = default_onionport
# Backup
shutil.copyfile(config_path, config_path + '.last')
tor_settings = ('proxy=', 'torpassword=', 'torcontrol=', 'bind=')
with open(config_path, 'w') as fp:
with open(config_path + '.last') as fp_in:
# Disable tor first
for line in fp_in:
skip_line = False
for setting in tor_settings:
if line.startswith(setting):
skip_line = True
break
if not skip_line:
fp.write(line)
if enable:
onionport = coin_settings['onionport']
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
fp.write(f'torpassword={tor_control_password}\n')
fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n')
fp.write(f'bind=0.0.0.0:{onionport}=onion\n')
def make_rpc_func(bin_dir, data_dir, chain):
bin_dir = bin_dir
data_dir = data_dir
chain = chain
def rpc_func(cmd):
nonlocal bin_dir
nonlocal data_dir
nonlocal chain
return callrpc_cli(bin_dir, data_dir, chain, cmd, cfg.PARTICL_CLI)
return rpc_func
def exitWithError(error_msg):
sys.stderr.write('Error: {}, exiting.\n'.format(error_msg))
sys.exit(1)
def printVersion():
from basicswap import __version__
logger.info('Basicswap version: %s', __version__)
@ -452,31 +670,14 @@ def printHelp():
logger.info('--htmlhost= Interface to host on, default:127.0.0.1.')
logger.info('--xmrrestoreheight=n Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT))
logger.info('--noextractover Prevent extracting cores if files exist. Speeds up tests')
logger.info('--usetorproxy Use TOR proxy. Note that some download links may be inaccessible over TOR.')
logger.info('\n' + 'Known coins: %s', ', '.join(known_coins.keys()))
def make_rpc_func(bin_dir, data_dir, chain):
bin_dir = bin_dir
data_dir = data_dir
chain = chain
def rpc_func(cmd):
nonlocal bin_dir
nonlocal data_dir
nonlocal chain
return callrpc_cli(bin_dir, data_dir, chain, cmd, cfg.PARTICL_CLI)
return rpc_func
def exitWithError(error_msg):
sys.stderr.write('Error: {}, exiting.\n'.format(error_msg))
sys.exit(1)
def main():
global extract_core_overwrite
global use_tor_proxy
data_dir = None
bin_dir = None
port_offset = None
@ -490,6 +691,9 @@ def main():
disable_coin = ''
htmlhost = '127.0.0.1'
xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT
enable_tor = False
disable_tor = False
tor_control_password = None
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != '-':
@ -528,6 +732,15 @@ def main():
if name == 'noextractover':
extract_core_overwrite = False
continue
if name == 'usetorproxy':
use_tor_proxy = True
continue
if name == 'enabletor':
enable_tor = True
continue
if name == 'disabletor':
disable_tor = True
continue
if len(s) == 2:
if name == 'datadir':
data_dir = os.path.expanduser(s[1].strip('"'))
@ -575,6 +788,14 @@ def main():
exitWithError('Unknown argument {}'.format(v))
setConnectionParameters()
if use_tor_proxy and TEST_TOR_PROXY:
testTorConnection()
if use_tor_proxy and TEST_ONION_LINK:
testOnionLink()
if data_dir is None:
data_dir = os.path.join(os.path.expanduser(cfg.DEFAULT_DATADIR))
if bin_dir is None:
@ -598,6 +819,7 @@ def main():
'manage_daemon': True if ('particl' in with_coins and PART_RPC_HOST == '127.0.0.1') else False,
'rpchost': PART_RPC_HOST,
'rpcport': PART_RPC_PORT + port_offset,
'onionport': PART_ONION_PORT + port_offset,
'datadir': os.getenv('PART_DATA_DIR', os.path.join(data_dir, 'particl')),
'bindir': os.path.join(bin_dir, 'particl'),
'blocks_confirmed': 2,
@ -611,6 +833,7 @@ def main():
'manage_daemon': True if ('litecoin' in with_coins and LTC_RPC_HOST == '127.0.0.1') else False,
'rpchost': LTC_RPC_HOST,
'rpcport': LTC_RPC_PORT + port_offset,
'onionport': LTC_ONION_PORT + port_offset,
'datadir': os.getenv('LTC_DATA_DIR', os.path.join(data_dir, 'litecoin')),
'bindir': os.path.join(bin_dir, 'litecoin'),
'use_segwit': True,
@ -624,6 +847,7 @@ def main():
'manage_daemon': True if ('bitcoin' in with_coins and BTC_RPC_HOST == '127.0.0.1') else False,
'rpchost': BTC_RPC_HOST,
'rpcport': BTC_RPC_PORT + port_offset,
'onionport': BTC_ONION_PORT + port_offset,
'datadir': os.getenv('BTC_DATA_DIR', os.path.join(data_dir, 'bitcoin')),
'bindir': os.path.join(bin_dir, 'bitcoin'),
'use_segwit': True,
@ -677,6 +901,48 @@ def main():
chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir'])
if enable_tor:
logger.info('Enabling TOR')
if not os.path.exists(config_path):
exitWithError('{} does not exist'.format(config_path))
with open(config_path) as fs:
settings = json.load(fs)
tor_control_password = settings.get('tor_control_password', None)
if tor_control_password is None:
tor_control_password = generate_salt(24)
settings['tor_control_password'] = tor_control_password
write_torrc(data_dir, tor_control_password)
addTorSettings(settings, tor_control_password)
for coin in settings['chainclients']:
modify_tor_config(settings, coin, tor_control_password, enable=True)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
logger.info('Done.')
return 0
if disable_tor:
logger.info('Disabling TOR')
if not os.path.exists(config_path):
exitWithError('{} does not exist'.format(config_path))
with open(config_path) as fs:
settings = json.load(fs)
settings['use_tor'] = False
for coin in settings['chainclients']:
modify_tor_config(settings, coin, tor_control_password=None, enable=False)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
logger.info('Done.')
return 0
if disable_coin != '':
logger.info('Disabling coin: %s', disable_coin)
if not os.path.exists(config_path):
@ -715,6 +981,7 @@ def main():
exitWithError('{} is already in the settings file'.format(add_coin))
settings['chainclients'][add_coin] = chainclients[add_coin]
settings['use_tor_proxy'] = use_tor_proxy
if not no_cores:
prepareCore(add_coin, known_coins[add_coin], settings, data_dir)
@ -754,6 +1021,10 @@ def main():
'check_expired_seconds': 60
}
if use_tor_proxy:
tor_control_password = generate_salt(24)
addTorSettings(settings, tor_control_password)
if not no_cores:
for c in with_coins:
prepareCore(c, known_coins[c], settings, data_dir)
@ -763,7 +1034,7 @@ def main():
return 0
for c in with_coins:
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, use_containers=use_containers)
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, use_containers=use_containers, tor_control_password=tor_control_password)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
@ -778,7 +1049,11 @@ def main():
partRpc = make_rpc_func(particl_settings['bindir'], particl_settings['datadir'], chain)
daemons = []
daemons.append(startDaemon(particl_settings['datadir'], particl_settings['bindir'], cfg.PARTICLD, ['-noconnect', '-nofindpeers', '-nostaking', '-nodnsseed', '-nolisten']))
daemon_args = ['-noconnect', '-nodnsseed']
if not use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0
daemon_args.append('-nolisten')
daemons.append(startDaemon(particl_settings['datadir'], particl_settings['bindir'], cfg.PARTICLD, daemon_args + ['-nofindpeers', '-nostaking']))
try:
waitForRPC(partRpc)
@ -811,7 +1086,7 @@ def main():
if not coin_settings['manage_daemon']:
continue
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, ['-noconnect', '-nodnsseed', '-nolisten']))
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args))
swap_client.setDaemonPID(c, daemons[-1].pid)
swap_client.setCoinRunParams(c)
swap_client.createCoinInterface(c)

36
doc/tor.md

@ -0,0 +1,36 @@
## Tor
Basicswap can be configured to route all traffic through a tor proxy.
### basicswap-prepare
basicswap-prepare can be configured to download all binaries through tor and to enable or disable tor in all active coin config files.
#### For a new install
Note that some download links, notably for Litecoin, are unreachable when using tor.
If running through docker start the tor container with the following command as the torrc configuration file won't exist yet.
docker compose -f docker-compose_with_tor.yml run --name tor --rm tor \
tor --allow-missing-torrc --SocksPort 0.0.0.0:9050
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=tor --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --withcoins=monero,particl
Start Basicswap with:
docker compose -f docker-compose_with_tor.yml up
#### Enable tor on an existing datadir
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=tor --rm swapclient \
basicswap-prepare --datadir=/coindata --enabletor
#### Disable tor on an existing datadir
docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --datadir=/coindata --disabletor

7
docker/docker-compose.yml

@ -1,6 +1,5 @@
version: '3'
version: '3.4'
services:
swapclient:
image: i_swapclient
stop_grace_period: 5m
@ -19,6 +18,6 @@ services:
max-file: "5"
volumes:
coindata:
driver: local
coindata:
driver: local

44
docker/docker-compose_with_tor.yml

@ -0,0 +1,44 @@
version: '3.4'
services:
swapclient:
image: i_swapclient
container_name: swapclient
stop_grace_period: 5m
build:
context: ../
volumes:
- ${COINDATA_PATH}:/coindata
ports:
- "${HTML_PORT}" # Expose only to localhost, see .env
environment:
- TZ
- TOR_PROXY_HOST
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
tor:
image: i_tor
container_name: tor
build:
context: ./tor
volumes:
- ${COINDATA_PATH}/tor/data:/var/lib/tor/
- ${COINDATA_PATH}/tor/torrc:/etc/tor/torrc
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
volumes:
coindata:
driver: local
networks:
default:
external:
name: coinswap_network

2
docker/production/.env

@ -19,4 +19,4 @@ BTC_DATA_DIR=/data/bitcoin
XMR_DATA_DIR=/data/monero_daemon
XMR_WALLETS_DIR=/data/monero_wallet
COINS_BIND_IP=0.0.0.0
COINS_RPCBIND_IP=0.0.0.0

6
docker/production/docker-compose.yml

@ -18,7 +18,7 @@ services:
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped
#restart: unless-stopped
#bitcoin_core:
#image: i_bitcoin
#build:
@ -50,7 +50,7 @@ services:
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped
#restart: unless-stopped
#monero_daemon:
#image: i_monero_daemon
#build:
@ -147,7 +147,7 @@ services:
- BTC_DATA_DIR
- XMR_DATA_DIR
- XMR_WALLETS_DIR
- COINS_BIND_IP
- COINS_RPCBIND_IP
restart: "no"
networks:
default:

2
docker/production/example.env

@ -18,4 +18,4 @@ BTC_DATA_DIR=/data/bitcoin
XMR_DATA_DIR=/data/monero_daemon
XMR_WALLETS_DIR=/data/monero_wallet
COINS_BIND_IP=0.0.0.0
COINS_RPCBIND_IP=0.0.0.0

8
docker/tor/Dockerfile

@ -0,0 +1,8 @@
FROM alpine:latest
# 9050 SOCKS port
# 9051 control port
# 5353 DNS port
EXPOSE 9050 9051 5353
RUN apk add --no-cache tor
CMD tor -f /etc/tor/torrc

1
requirements.txt

@ -6,3 +6,4 @@ python-gnupg
Jinja2
requests
pycryptodome
PySocks

1
setup.py

@ -39,6 +39,7 @@ setuptools.setup(
"Jinja2",
"requests",
"pycryptodome",
"PySocks",
],
entry_points={
"console_scripts": [

4
tests/basicswap/extended/test_network.py

@ -24,9 +24,11 @@ from basicswap.basicswap import (
)
from basicswap.util import (
COIN,
toWIF,
dumpj,
)
from basicswap.util.address import (
toWIF,
)
from basicswap.rpc import (
callrpc,
callrpc_cli,

2
tests/basicswap/extended/test_nmc.py

@ -33,6 +33,8 @@ from basicswap.basicswap import (
)
from basicswap.util import (
COIN,
)
from basicswap.util.address import (
toWIF,
)
from basicswap.rpc import (

12
tests/basicswap/test_other.py

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# Copyright (c) 2019-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -21,7 +21,8 @@ from coincurve.ecdsaotves import (
from coincurve.keys import (
PrivateKey)
from basicswap.ecc_util import i2b, h2b
from basicswap.util import i2b, h2b
from basicswap.util.rfc2440 import rfc2440_hash_password
from basicswap.interface_btc import BTCInterface
from basicswap.interface_xmr import XMRInterface
@ -279,6 +280,13 @@ class Test(unittest.TestCase):
amount_to_recreate = int((amount_from * rate) // (10 ** scale_from))
assert('10.00000000' == format_amount(amount_to_recreate, scale_to))
def test_rfc2440(self):
password = 'test'
salt = bytes.fromhex('B7A94A7E4988630E')
password_hash = rfc2440_hash_password(password, salt=salt)
assert(password_hash == '16:B7A94A7E4988630E6095334BA67F06FBA509B2A7136A04C9C1B430F539')
if __name__ == '__main__':
unittest.main()

4
tests/basicswap/test_xmr.py

@ -31,10 +31,12 @@ from basicswap.basicswap_util import (
)
from basicswap.util import (
COIN,
toWIF,
make_int,
format_amount,
)
from basicswap.util.address import (
toWIF,
)
from basicswap.rpc import (
callrpc,
callrpc_cli,

Loading…
Cancel
Save