mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-06 10:48:11 +01:00
Add bitcoincash support for prepare and run scripts, add bitcoincash to testing suite, groundwork for bch-xmr atomic swap protocol
This commit is contained in:
@@ -251,6 +251,7 @@ class BasicSwap(BaseApp):
|
||||
protocolInterfaces = {
|
||||
SwapTypes.SELLER_FIRST: atomic_swap_1.AtomicSwapInterface(),
|
||||
SwapTypes.XMR_SWAP: xmr_swap_1.XmrSwapInterface(),
|
||||
SwapTypes.XMR_BCH_SWAP: xmr_swap_1.XmrBchSwapInterface(),
|
||||
}
|
||||
|
||||
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap', transient_instance=False):
|
||||
@@ -688,6 +689,9 @@ class BasicSwap(BaseApp):
|
||||
elif coin == Coins.BTC:
|
||||
from .interface.btc import BTCInterface
|
||||
return BTCInterface(self.coin_clients[coin], self.chain, self)
|
||||
elif coin == Coins.BCH:
|
||||
from .interface.bch import BCHInterface
|
||||
return BCHInterface(self.coin_clients[coin], self.chain, self)
|
||||
elif coin == Coins.LTC:
|
||||
from .interface.ltc import LTCInterface, LTCInterfaceMWEB
|
||||
interface = LTCInterface(self.coin_clients[coin], self.chain, self)
|
||||
|
||||
@@ -64,6 +64,7 @@ class SwapTypes(IntEnum):
|
||||
SELLER_FIRST_2MSG = auto()
|
||||
BUYER_FIRST_2MSG = auto()
|
||||
XMR_SWAP = auto()
|
||||
XMR_BCH_SWAP = auto()
|
||||
|
||||
|
||||
class OfferStates(IntEnum):
|
||||
|
||||
53
basicswap/bin/prepare.py
Executable file → Normal file
53
basicswap/bin/prepare.py
Executable file → Normal file
@@ -49,6 +49,9 @@ LITECOIN_VERSION_TAG = os.getenv('LITECOIN_VERSION_TAG', '')
|
||||
BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '26.0')
|
||||
BITCOIN_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '')
|
||||
|
||||
BITCOINCASH_VERSION = os.getenv('BITCOIN_VERSION', '27.1.0')
|
||||
BITCOINCASH_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '')
|
||||
|
||||
MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.4')
|
||||
MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '')
|
||||
XMR_SITE_COMMIT = '3751c0d7987a9e78324a718c32c008e2ec91b339' # Lock hashes.txt to monero version
|
||||
@@ -84,6 +87,7 @@ SKIP_GPG_VALIDATION = toBool(os.getenv('SKIP_GPG_VALIDATION', 'false'))
|
||||
known_coins = {
|
||||
'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)),
|
||||
'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)),
|
||||
'bitcoincash': (BITCOINCASH_VERSION, BITCOINCASH_VERSION_TAG, ('Calin_Culianu',)),
|
||||
'litecoin': (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ('davidburkett38',)),
|
||||
'decred': (DCR_VERSION, DCR_VERSION_TAG, ('decred_release',)),
|
||||
'namecoin': ('0.18.0', '', ('JeremyRand',)),
|
||||
@@ -113,6 +117,7 @@ expected_key_ids = {
|
||||
'reuben': ('1290A1D0FA7EE109',),
|
||||
'nav_builder': ('2782262BF6E7FADB',),
|
||||
'decred_release': ('6D897EDF518A031D',),
|
||||
'Calin_Culianu': ('21810A542031C02C',),
|
||||
}
|
||||
|
||||
USE_PLATFORM = os.getenv('USE_PLATFORM', platform.system())
|
||||
@@ -186,6 +191,12 @@ BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334))
|
||||
BTC_RPC_USER = os.getenv('BTC_RPC_USER', '')
|
||||
BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '')
|
||||
|
||||
BCH_RPC_HOST = os.getenv('BCH_RPC_HOST', '127.0.0.1')
|
||||
BCH_RPC_PORT = int(os.getenv('BCH_RPC_PORT', 19997))
|
||||
BCH_ONION_PORT = int(os.getenv('BCH_ONION_PORT', 8334))
|
||||
BCH_RPC_USER = os.getenv('BCH_RPC_USER', '')
|
||||
BCH_RPC_PWD = os.getenv('BCH_RPC_PWD', '')
|
||||
|
||||
DCR_RPC_HOST = os.getenv('DCR_RPC_HOST', '127.0.0.1')
|
||||
DCR_RPC_PORT = int(os.getenv('DCR_RPC_PORT', 9109))
|
||||
DCR_WALLET_RPC_HOST = os.getenv('DCR_WALLET_RPC_HOST', '127.0.0.1')
|
||||
@@ -513,8 +524,14 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
|
||||
return
|
||||
|
||||
dir_name = 'dashcore' if coin == 'dash' else coin
|
||||
dir_name = 'bitcoin-cash-node' if coin == 'bitcoincash' else coin
|
||||
if coin == 'decred':
|
||||
bins = ['dcrd', 'dcrwallet']
|
||||
elif coin == 'bitcoincash':
|
||||
bins = ['bitcoind', 'bitcoin-cli', 'bitcoin-tx']
|
||||
versions = version.split('.')
|
||||
if int(versions[0]) >= 22 or int(versions[1]) >= 19:
|
||||
bins.append('bitcoin-wallet')
|
||||
else:
|
||||
bins = [coin + 'd', coin + '-cli', coin + '-tx']
|
||||
versions = version.split('.')
|
||||
@@ -696,6 +713,11 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
assert_url = f'https://raw.githubusercontent.com/bitcoin-core/guix.sigs/main/{version}/{signing_key_name}/all.SHA256SUMS'
|
||||
else:
|
||||
assert_url = 'https://raw.githubusercontent.com/bitcoin-core/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
|
||||
elif coin == 'bitcoincash':
|
||||
release_filename = 'bitcoin-cash-node-{}-{}.{}'.format(version, BIN_ARCH, FILE_EXT)
|
||||
release_url = 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v{}/{}'.format(version, release_filename)
|
||||
assert_filename = 'SHA256SUMS.{}.asc.Calin_Culianu'.format(version)
|
||||
assert_url = 'https://gitlab.com/bitcoin-cash-node/announcements/-/raw/master/release-sigs/%s/%s' % (version, assert_filename)
|
||||
elif coin == 'namecoin':
|
||||
release_url = 'https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}'.format(version, release_filename)
|
||||
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
|
||||
@@ -738,7 +760,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
if not os.path.exists(assert_path):
|
||||
downloadFile(assert_url, assert_path)
|
||||
|
||||
if coin not in ('firo', ):
|
||||
if coin not in ('firo', 'bitcoincash',):
|
||||
assert_sig_url = assert_url + ('.asc' if use_guix else '.sig')
|
||||
if coin not in ('nav', ):
|
||||
assert_sig_filename = '{}-{}-{}-build-{}.assert.sig'.format(coin, os_name, version, signing_key_name)
|
||||
@@ -798,11 +820,13 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
pubkeyurls.append('https://git.wownero.com/wownero/wownero/raw/branch/master/utils/gpg_keys/wowario.asc')
|
||||
if coin == 'firo':
|
||||
pubkeyurls.append('https://firo.org/reuben.asc')
|
||||
if coin == 'bitcoincash':
|
||||
pubkeyurls.append('https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node/-/raw/master/contrib/gitian-signing/pubkeys.txt')
|
||||
|
||||
if ADD_PUBKEY_URL != '':
|
||||
pubkeyurls.append(ADD_PUBKEY_URL + '/' + pubkey_filename)
|
||||
|
||||
if coin in ('monero', 'wownero', 'firo'):
|
||||
if coin in ('monero', 'wownero', 'firo', 'bitcoincash',):
|
||||
with open(assert_path, 'rb') as fp:
|
||||
verified = gpg.verify_file(fp)
|
||||
|
||||
@@ -837,7 +861,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
verified = gpg.verify_file(fp, assert_path)
|
||||
|
||||
ensureValidSignatureBy(verified, signing_key_name)
|
||||
|
||||
extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts)
|
||||
|
||||
|
||||
@@ -1032,6 +1055,10 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
fp.write('fallbackfee=0.0002\n')
|
||||
if BTC_RPC_USER != '':
|
||||
fp.write('rpcauth={}:{}${}\n'.format(BTC_RPC_USER, salt, password_to_hmac(salt, BTC_RPC_PWD)))
|
||||
elif coin == 'bitcoincash':
|
||||
fp.write('prune=2000\n')
|
||||
if BCH_RPC_USER != '':
|
||||
fp.write('rpcauth={}:{}${}\n'.format(BCH_RPC_USER, salt, password_to_hmac(salt, BCH_RPC_PWD)))
|
||||
elif coin == 'namecoin':
|
||||
fp.write('prune=2000\n')
|
||||
elif coin == 'pivx':
|
||||
@@ -1182,6 +1209,8 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
|
||||
default_onionport = 0
|
||||
if coin == 'bitcoin':
|
||||
default_onionport = BTC_ONION_PORT
|
||||
if coin == 'bitcoincash':
|
||||
default_onionport = BCH_ONION_PORT
|
||||
elif coin == 'particl':
|
||||
default_onionport = PART_ONION_PORT
|
||||
elif coin == 'litecoin':
|
||||
@@ -1353,7 +1382,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
|
||||
pass
|
||||
else:
|
||||
if coin_settings['manage_daemon']:
|
||||
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
filename = (coin_name if not coin_name == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
coin_args = ['-nofindpeers', '-nostaking'] if c == Coins.PART else []
|
||||
|
||||
if c == Coins.FIRO:
|
||||
@@ -1755,6 +1784,19 @@ def main():
|
||||
'conf_target': 2,
|
||||
'core_version_group': 22,
|
||||
},
|
||||
'bitcoincash': {
|
||||
'connection_type': 'rpc' if 'bitcoincash' in with_coins else 'none',
|
||||
'manage_daemon': True if ('bitcoincash' in with_coins and BCH_RPC_HOST == '127.0.0.1') else False,
|
||||
'rpchost': BCH_RPC_HOST,
|
||||
'rpcport': BCH_RPC_PORT + port_offset,
|
||||
'onionport': BCH_ONION_PORT + port_offset,
|
||||
'datadir': os.getenv('BCH_DATA_DIR', os.path.join(data_dir, 'bitcoincash')),
|
||||
'bindir': os.path.join(bin_dir, 'bitcoincash'),
|
||||
'use_segwit': False,
|
||||
'blocks_confirmed': 1,
|
||||
'conf_target': 2,
|
||||
'core_version_group': 22,
|
||||
},
|
||||
'litecoin': {
|
||||
'connection_type': 'rpc',
|
||||
'manage_daemon': shouldManageDaemon('LTC'),
|
||||
@@ -1917,6 +1959,9 @@ def main():
|
||||
if BTC_RPC_USER != '':
|
||||
chainclients['bitcoin']['rpcuser'] = BTC_RPC_USER
|
||||
chainclients['bitcoin']['rpcpassword'] = BTC_RPC_PWD
|
||||
if BCH_RPC_USER != '':
|
||||
chainclients['bitcoin']['rpcuser'] = BCH_RPC_USER
|
||||
chainclients['bitcoin']['rpcpassword'] = BCH_RPC_PWD
|
||||
if XMR_RPC_USER != '':
|
||||
chainclients['monero']['rpcuser'] = XMR_RPC_USER
|
||||
chainclients['monero']['rpcpassword'] = XMR_RPC_PWD
|
||||
|
||||
@@ -214,7 +214,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
if c in ('monero', 'wownero'):
|
||||
if v['manage_daemon'] is True:
|
||||
swap_client.log.info(f'Starting {display_name} daemon')
|
||||
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
filename = (c if not c == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info('Started {} {}'.format(filename, pid))
|
||||
@@ -280,7 +280,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
if v['manage_daemon'] is True:
|
||||
swap_client.log.info(f'Starting {display_name} daemon')
|
||||
|
||||
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
filename = (c if not c == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
|
||||
pid = daemons[-1].handle.pid
|
||||
pids.append((c, pid))
|
||||
|
||||
@@ -30,6 +30,7 @@ class Coins(IntEnum):
|
||||
NAV = 14
|
||||
LTC_MWEB = 15
|
||||
# ZANO = 16
|
||||
BCH = 17
|
||||
|
||||
|
||||
chainparams = {
|
||||
@@ -432,7 +433,45 @@ chainparams = {
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
}
|
||||
},
|
||||
Coins.BCH: {
|
||||
'name': 'bitcoincash',
|
||||
'ticker': 'BCH',
|
||||
'message_magic': 'Bitcoin Signed Message:\n',
|
||||
'blocks_target': 60 * 2,
|
||||
'decimal_places': 8,
|
||||
'mainnet': {
|
||||
'rpcport': 8332,
|
||||
'pubkey_address': 0,
|
||||
'script_address': 5,
|
||||
'key_prefix': 128,
|
||||
'hrp': 'bitcoincash',
|
||||
'bip44': 0,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 18332,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'key_prefix': 239,
|
||||
'hrp': 'bchtest',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
'name': 'testnet3',
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 18443,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'key_prefix': 239,
|
||||
'hrp': 'bchreg',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
}
|
||||
ticker_map = {}
|
||||
|
||||
|
||||
@@ -36,3 +36,8 @@ NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix)
|
||||
XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero')))
|
||||
XMRD = os.getenv('XMRD', 'monerod' + bin_suffix)
|
||||
XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix)
|
||||
|
||||
BITCOINCASH_BINDIR = os.path.expanduser(os.getenv('BITCOINCASH_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoincash')))
|
||||
BITCOINCASHD = os.getenv('BITCOINCASHD', 'bitcoind' + bin_suffix)
|
||||
BITCOINCASH_CLI = os.getenv('BITCOINCASH_CLI', 'bitcoin-cli' + bin_suffix)
|
||||
BITCOINCASH_TX = os.getenv('BITCOINCASH_TX', 'bitcoin-tx' + bin_suffix)
|
||||
|
||||
119
basicswap/interface/bch.py
Normal file
119
basicswap/interface/bch.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from typing import Union
|
||||
from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut
|
||||
from basicswap.util import ensure, i2h
|
||||
from .btc import BTCInterface, findOutput
|
||||
from basicswap.rpc import make_rpc_func
|
||||
from basicswap.chainparams import Coins, chainparams
|
||||
from basicswap.interface.contrib.bch_test_framework.cashaddress import Address
|
||||
from basicswap.util.crypto import hash160, sha256
|
||||
from basicswap.interface.contrib.bch_test_framework.script import OP_EQUAL, OP_EQUALVERIFY, OP_HASH256, OP_DUP, OP_HASH160, OP_CHECKSIG
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript, CScriptOp,
|
||||
)
|
||||
|
||||
class BCHInterface(BTCInterface):
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
return Coins.BCH
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super(BCHInterface, self).__init__(coin_settings, network, swap_client)
|
||||
|
||||
|
||||
def decodeAddress(self, address: str) -> bytes:
|
||||
return bytes(Address.from_string(address).payload)
|
||||
|
||||
def pubkey_to_segwit_address(self, pk: bytes) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def pkh_to_address(self, pkh: bytes) -> str:
|
||||
# pkh is ripemd160(sha256(pk))
|
||||
assert (len(pkh) == 20)
|
||||
prefix = self.chainparams_network()['hrp']
|
||||
address = Address("P2PKH", b'\x76\xa9\x14' + pkh + b'\x88\xac')
|
||||
address.prefix = prefix
|
||||
return address.cash_address()
|
||||
|
||||
def getNewAddress(self, use_segwit: bool = False, label: str = 'swap_receive') -> str:
|
||||
args = [label]
|
||||
return self.rpc_wallet('getnewaddress', args)
|
||||
|
||||
def addressToLockingBytecode(self, address: str) -> bytes:
|
||||
return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac'
|
||||
|
||||
def getScriptDest(self, script):
|
||||
return self.scriptToP2SH32LockingBytecode(script)
|
||||
|
||||
def scriptToP2SH32LockingBytecode(self, script: Union[bytes, str]) -> bytes:
|
||||
if isinstance(script, str):
|
||||
script = bytes.fromhex(script)
|
||||
|
||||
return CScript([
|
||||
CScriptOp(OP_HASH256),
|
||||
sha256(sha256(script)),
|
||||
CScriptOp(OP_EQUAL),
|
||||
])
|
||||
|
||||
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||
return tx.serialize_without_witness()
|
||||
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
|
||||
return CScript([
|
||||
CScriptOp(OP_DUP),
|
||||
CScriptOp(OP_HASH160),
|
||||
pkh,
|
||||
CScriptOp(OP_EQUALVERIFY),
|
||||
CScriptOp(OP_CHECKSIG),
|
||||
])
|
||||
|
||||
def getTxSize(self, tx: CTransaction) -> int:
|
||||
return len(tx.serialize_without_witness())
|
||||
|
||||
def getScriptScriptSig(self, script: bytes, ves: bytes) -> bytes:
|
||||
if ves is not None:
|
||||
return CScript([ves, script])
|
||||
else:
|
||||
return CScript([script])
|
||||
|
||||
def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, ves=None, fee_info={}):
|
||||
# tx_fee_rate in this context is equal to `mining_fee` contract param
|
||||
tx_lock = self.loadTx(tx_lock_bytes)
|
||||
output_script = self.getScriptDest(script_lock)
|
||||
locked_n = findOutput(tx_lock, output_script)
|
||||
ensure(locked_n is not None, 'Output not found in tx')
|
||||
locked_coin = tx_lock.vout[locked_n].nValue
|
||||
|
||||
tx_lock.rehash()
|
||||
tx_lock_id_int = tx_lock.sha256
|
||||
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
|
||||
scriptSig=self.getScriptScriptSig(script_lock, ves),
|
||||
nSequence=0))
|
||||
|
||||
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
|
||||
pay_fee = tx_fee_rate
|
||||
tx.vout[0].nValue = locked_coin - pay_fee
|
||||
|
||||
size = self.getTxSize(tx)
|
||||
|
||||
fee_info['fee_paid'] = pay_fee
|
||||
fee_info['rate_used'] = tx_fee_rate
|
||||
fee_info['size'] = size
|
||||
|
||||
tx.rehash()
|
||||
self._log.info('createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.',
|
||||
i2h(tx.sha256), tx_fee_rate, size, pay_fee)
|
||||
|
||||
return tx.serialize_without_witness()
|
||||
247
basicswap/interface/contrib/bch_test_framework/cashaddress.py
Normal file
247
basicswap/interface/contrib/bch_test_framework/cashaddress.py
Normal file
@@ -0,0 +1,247 @@
|
||||
import unittest
|
||||
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
def polymod(values):
|
||||
chk = 1
|
||||
generator = [
|
||||
(0x01, 0x98F2BC8E61),
|
||||
(0x02, 0x79B76D99E2),
|
||||
(0x04, 0xF33E5FB3C4),
|
||||
(0x08, 0xAE2EABE2A8),
|
||||
(0x10, 0x1E4F43E470),
|
||||
]
|
||||
for value in values:
|
||||
top = chk >> 35
|
||||
chk = ((chk & 0x07FFFFFFFF) << 5) ^ value
|
||||
for i in generator:
|
||||
if top & i[0] != 0:
|
||||
chk ^= i[1]
|
||||
return chk ^ 1
|
||||
|
||||
|
||||
def calculate_checksum(prefix, payload):
|
||||
poly = polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0])
|
||||
out = list()
|
||||
for i in range(8):
|
||||
out.append((poly >> 5 * (7 - i)) & 0x1F)
|
||||
return out
|
||||
|
||||
|
||||
def verify_checksum(prefix, payload):
|
||||
return polymod(prefix_expand(prefix) + payload) == 0
|
||||
|
||||
|
||||
def b32decode(inputs):
|
||||
out = list()
|
||||
for letter in inputs:
|
||||
out.append(CHARSET.find(letter))
|
||||
return out
|
||||
|
||||
|
||||
def b32encode(inputs):
|
||||
out = ""
|
||||
for char_code in inputs:
|
||||
out += CHARSET[char_code]
|
||||
return out
|
||||
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
if value < 0 or (value >> frombits):
|
||||
return None
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad:
|
||||
if bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def prefix_expand(prefix):
|
||||
return [ord(x) & 0x1F for x in prefix] + [0]
|
||||
|
||||
|
||||
class Address:
|
||||
"""
|
||||
Class to handle CashAddr.
|
||||
|
||||
:param version: Version of CashAddr
|
||||
:type version: ``str``
|
||||
:param payload: Payload of CashAddr as int list of the bytearray
|
||||
:type payload: ``list`` of ``int``
|
||||
"""
|
||||
|
||||
VERSIONS = {
|
||||
"P2SH20": {"prefix": "bitcoincash", "version_bit": 8, "network": "mainnet"},
|
||||
"P2SH32": {"prefix": "bitcoincash", "version_bit": 11, "network": "mainnet"},
|
||||
"P2PKH": {"prefix": "bitcoincash", "version_bit": 0, "network": "mainnet"},
|
||||
"P2SH20-TESTNET": {"prefix": "bchtest", "version_bit": 8, "network": "testnet"},
|
||||
"P2SH32-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 11,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2PKH-TESTNET": {"prefix": "bchtest", "version_bit": 0, "network": "testnet"},
|
||||
"P2SH20-REGTEST": {"prefix": "bchreg", "version_bit": 8, "network": "regtest"},
|
||||
"P2SH32-REGTEST": {"prefix": "bchreg", "version_bit": 11, "network": "regtest"},
|
||||
"P2PKH-REGTEST": {"prefix": "bchreg", "version_bit": 0, "network": "regtest"},
|
||||
"P2SH20-CATKN": {
|
||||
"prefix": "bitcoincash",
|
||||
"version_bit": 24,
|
||||
"network": "mainnet",
|
||||
},
|
||||
"P2SH32-CATKN": {
|
||||
"prefix": "bitcoincash",
|
||||
"version_bit": 27,
|
||||
"network": "mainnet",
|
||||
},
|
||||
"P2PKH-CATKN": {
|
||||
"prefix": "bitcoincash",
|
||||
"version_bit": 16,
|
||||
"network": "mainnet",
|
||||
},
|
||||
"P2SH20-CATKN-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 24,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2SH32-CATKN-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 27,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2PKH-CATKN-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 16,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2SH20-CATKN-REGTEST": {
|
||||
"prefix": "bchreg",
|
||||
"version_bit": 24,
|
||||
"network": "regtest",
|
||||
},
|
||||
"P2SH32-CATKN-REGTEST": {
|
||||
"prefix": "bchreg",
|
||||
"version_bit": 27,
|
||||
"network": "regtest",
|
||||
},
|
||||
"P2PKH-CATKN-REGTEST": {
|
||||
"prefix": "bchreg",
|
||||
"version_bit": 16,
|
||||
"network": "regtest",
|
||||
},
|
||||
}
|
||||
|
||||
VERSION_SUFFIXES = {"bitcoincash": "", "bchtest": "-TESTNET", "bchreg": "-REGTEST"}
|
||||
|
||||
ADDRESS_TYPES = {
|
||||
0: "P2PKH",
|
||||
8: "P2SH20",
|
||||
11: "P2SH32",
|
||||
16: "P2PKH-CATKN",
|
||||
24: "P2SH20-CATKN",
|
||||
27: "P2SH32-CATKN",
|
||||
}
|
||||
|
||||
def __init__(self, version, payload):
|
||||
if version not in Address.VERSIONS:
|
||||
raise ValueError("Invalid address version provided")
|
||||
|
||||
self.version = version
|
||||
self.payload = payload
|
||||
self.prefix = Address.VERSIONS[self.version]["prefix"]
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"version: {self.version}\npayload: {self.payload}\nprefix: {self.prefix}"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Address('{self.cash_address()}')"
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
return self.cash_address() == other
|
||||
elif isinstance(other, Address):
|
||||
return self.cash_address() == other.cash_address()
|
||||
else:
|
||||
raise ValueError(
|
||||
"Address can be compared to a string address"
|
||||
" or an instance of Address"
|
||||
)
|
||||
|
||||
def cash_address(self):
|
||||
"""
|
||||
Generate CashAddr of the Address
|
||||
|
||||
:rtype: ``str``
|
||||
"""
|
||||
version_bit = Address.VERSIONS[self.version]["version_bit"]
|
||||
payload = [version_bit] + self.payload
|
||||
payload = convertbits(payload, 8, 5)
|
||||
checksum = calculate_checksum(self.prefix, payload)
|
||||
return self.prefix + ":" + b32encode(payload + checksum)
|
||||
|
||||
@staticmethod
|
||||
def from_string(address):
|
||||
"""
|
||||
Generate Address from a cashadress string
|
||||
|
||||
:param scriptcode: The cashaddress string
|
||||
:type scriptcode: ``str``
|
||||
:returns: Instance of :class:~bitcash.cashaddress.Address
|
||||
"""
|
||||
try:
|
||||
address = str(address)
|
||||
except Exception:
|
||||
raise ValueError("Expected string as input")
|
||||
|
||||
if address.upper() != address and address.lower() != address:
|
||||
raise ValueError(
|
||||
"Cash address contains uppercase and lowercase characters"
|
||||
)
|
||||
|
||||
address = address.lower()
|
||||
colon_count = address.count(":")
|
||||
if colon_count == 0:
|
||||
raise ValueError("Cash address is missing prefix")
|
||||
if colon_count > 1:
|
||||
raise ValueError("Cash address contains more than one colon character")
|
||||
|
||||
prefix, base32string = address.split(":")
|
||||
decoded = b32decode(base32string)
|
||||
|
||||
if not verify_checksum(prefix, decoded):
|
||||
raise ValueError(
|
||||
"Bad cash address checksum for address {}".format(address)
|
||||
)
|
||||
converted = convertbits(decoded, 5, 8)
|
||||
|
||||
try:
|
||||
version = Address.ADDRESS_TYPES[converted[0]]
|
||||
except Exception:
|
||||
raise ValueError("Could not determine address version")
|
||||
|
||||
version += Address.VERSION_SUFFIXES[prefix]
|
||||
|
||||
payload = converted[1:-6]
|
||||
return Address(version, payload)
|
||||
|
||||
class TestFrameworkScript(unittest.TestCase):
|
||||
def test_base58encodedecode(self):
|
||||
def check_cashaddress(address: str):
|
||||
self.assertEqual(Address.from_string(address).cash_address(), address)
|
||||
|
||||
check_cashaddress("bitcoincash:qzfyvx77v2pmgc0vulwlfkl3uzjgh5gnmqk5hhyaa6")
|
||||
40
basicswap/interface/contrib/bch_test_framework/script.py
Normal file
40
basicswap/interface/contrib/bch_test_framework/script.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
OP_TXINPUTCOUNT = 0xc3
|
||||
OP_1 = 0x51
|
||||
OP_NUMEQUALVERIFY = 0x9d
|
||||
OP_TXOUTPUTCOUNT = 0xc4
|
||||
OP_0 = 0x00
|
||||
OP_UTXOVALUE = 0xc6
|
||||
OP_OUTPUTVALUE = 0xcc
|
||||
OP_SUB = 0x94
|
||||
OP_UTXOTOKENCATEGORY = 0xce
|
||||
OP_OUTPUTTOKENCATEGORY = 0xd1
|
||||
OP_EQUALVERIFY = 0x88
|
||||
OP_UTXOTOKENCOMMITMENT = 0xcf
|
||||
OP_OUTPUTTOKENCOMMITMENT = 0xd2
|
||||
OP_UTXOTOKENAMOUNT = 0xd0
|
||||
OP_OUTPUTTOKENAMOUNT = 0xd3
|
||||
OP_INPUTSEQUENCENUMBER = 0xcb
|
||||
OP_NOTIF = 0x64
|
||||
OP_OUTPUTBYTECODE = 0xcd
|
||||
OP_OVER = 0x78
|
||||
OP_CHECKDATASIG = 0xba
|
||||
OP_CHECKDATASIGVERIFY = 0xbb
|
||||
OP_ELSE = 0x67
|
||||
OP_CHECKSEQUENCEVERIFY = 0xb2
|
||||
OP_DROP = 0x75
|
||||
OP_EQUAL = 0x87
|
||||
OP_ENDIF = 0x68
|
||||
OP_HASH256 = 0xaa
|
||||
OP_PUSHBYTES_32 = 0x20
|
||||
OP_DUP = 0x76
|
||||
OP_HASH160 = 0xa9
|
||||
OP_CHECKSIG = 0xac
|
||||
OP_SHA256 = 0xa8
|
||||
OP_VERIFY = 0x69
|
||||
@@ -6,6 +6,34 @@
|
||||
|
||||
import traceback
|
||||
|
||||
import unittest
|
||||
from basicswap.interface.contrib.bch_test_framework.script import (
|
||||
OP_TXINPUTCOUNT,
|
||||
OP_1,
|
||||
OP_NUMEQUALVERIFY,
|
||||
OP_TXOUTPUTCOUNT,
|
||||
OP_0,
|
||||
OP_UTXOVALUE,
|
||||
OP_OUTPUTVALUE,
|
||||
OP_SUB,
|
||||
OP_UTXOTOKENCATEGORY,
|
||||
OP_OUTPUTTOKENCATEGORY,
|
||||
OP_EQUALVERIFY,
|
||||
OP_UTXOTOKENCOMMITMENT,
|
||||
OP_OUTPUTTOKENCOMMITMENT,
|
||||
OP_UTXOTOKENAMOUNT,
|
||||
OP_OUTPUTTOKENAMOUNT,
|
||||
OP_INPUTSEQUENCENUMBER,
|
||||
OP_NOTIF,
|
||||
OP_OUTPUTBYTECODE,
|
||||
OP_OVER,
|
||||
OP_CHECKDATASIG,
|
||||
OP_ELSE,
|
||||
OP_CHECKSEQUENCEVERIFY,
|
||||
OP_DROP,
|
||||
OP_EQUAL,
|
||||
OP_ENDIF,
|
||||
)
|
||||
from basicswap.util import (
|
||||
ensure,
|
||||
)
|
||||
@@ -193,3 +221,82 @@ class XmrSwapInterface(ProtocolInterface):
|
||||
ctx.nLockTime = 0
|
||||
|
||||
return ctx.serialize()
|
||||
|
||||
class XmrBchSwapInterface(ProtocolInterface):
|
||||
swap_type = SwapTypes.XMR_BCH_SWAP
|
||||
|
||||
def genScriptLockTxScript(self, mining_fee: int, out_1: bytes, out_2: bytes, public_key: bytes, timelock: int) -> CScript:
|
||||
return CScript([
|
||||
# // v4.1.0-CashTokens-Optimized
|
||||
# // Based on swaplock.cash v4.1.0-CashTokens
|
||||
#
|
||||
# // Alice has XMR, wants BCH and/or CashTokens.
|
||||
# // Bob has BCH and/or CashTokens, wants XMR.
|
||||
#
|
||||
# // Verify 1-in-1-out TX form
|
||||
CScriptOp(OP_TXINPUTCOUNT),
|
||||
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
|
||||
CScriptOp(OP_TXOUTPUTCOUNT),
|
||||
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
|
||||
|
||||
# // int miningFee
|
||||
mining_fee,
|
||||
# // Verify pre-agreed mining fee and that the rest of BCH is forwarded
|
||||
# // to the output.
|
||||
CScriptOp(OP_0), CScriptOp(OP_UTXOVALUE),
|
||||
CScriptOp(OP_0), CScriptOp(OP_OUTPUTVALUE),
|
||||
CScriptOp(OP_SUB), CScriptOp(OP_NUMEQUALVERIFY),
|
||||
|
||||
# # // Verify that any CashTokens are forwarded to the output.
|
||||
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCATEGORY),
|
||||
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCATEGORY),
|
||||
CScriptOp(OP_EQUALVERIFY),
|
||||
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCOMMITMENT),
|
||||
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCOMMITMENT),
|
||||
CScriptOp(OP_EQUALVERIFY),
|
||||
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENAMOUNT),
|
||||
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENAMOUNT),
|
||||
CScriptOp(OP_NUMEQUALVERIFY),
|
||||
|
||||
# // If sequence is not used then it is a regular swap TX.
|
||||
CScriptOp(OP_0), CScriptOp(OP_INPUTSEQUENCENUMBER),
|
||||
CScriptOp(OP_NOTIF),
|
||||
# // bytes aliceOutput
|
||||
out_1,
|
||||
# // Verify that the BCH and/or CashTokens are forwarded to Alice's
|
||||
# // output.
|
||||
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
|
||||
CScriptOp(OP_OVER), CScriptOp(OP_EQUALVERIFY),
|
||||
|
||||
# // pubkey bobPubkeyVES
|
||||
public_key,
|
||||
# // Require Alice to decrypt and publish Bob's VES signature.
|
||||
# // The "message" signed is simply a sha256 hash of Alice's output
|
||||
# // locking bytecode.
|
||||
# // By decrypting Bob's VES and publishing it, Alice reveals her
|
||||
# // XMR key share to Bob.
|
||||
CScriptOp(OP_CHECKDATASIG),
|
||||
|
||||
# // If a TX using this path is mined then Alice gets her BCH.
|
||||
# // Bob uses the revealed XMR key share to collect his XMR.
|
||||
|
||||
# // Refund will become available when timelock expires, and it would
|
||||
# // expire because Alice didn't collect on time, either of her own accord
|
||||
# // or because Bob bailed out and witheld the encrypted signature.
|
||||
CScriptOp(OP_ELSE),
|
||||
# // int timelock_0
|
||||
timelock,
|
||||
# // Verify refund timelock.
|
||||
CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
|
||||
|
||||
# // bytes refundLockingBytecode
|
||||
out_2,
|
||||
|
||||
# // Verify that the BCH and/or CashTokens are forwarded to Refund
|
||||
# // contract.
|
||||
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
|
||||
CScriptOp(OP_EQUAL),
|
||||
|
||||
# // BCH and/or CashTokens are simply forwarded to Refund contract.
|
||||
CScriptOp(OP_ENDIF)
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user