diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index 9e11108..a1652ad 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -20,6 +20,7 @@ from basicswap.util.crypto import ( ) from basicswap.util.extkey import ExtKeyPair from basicswap.interface.dcr.rpc import make_rpc_func +from .messages import CTransaction class DCRInterface(Secp256k1Interface): @@ -119,3 +120,8 @@ class DCRInterface(Secp256k1Interface): ek_account = ek_coin.derive(0 | (1 << 31)) return hash160(ek_account.encode_p()) + + def loadTx(self, tx_bytes: bytes) -> CTransaction: + tx = CTransaction() + tx.deserialize(tx_bytes) + return tx diff --git a/basicswap/interface/dcr/messages.py b/basicswap/interface/dcr/messages.py new file mode 100644 index 0000000..e0d1930 --- /dev/null +++ b/basicswap/interface/dcr/messages.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# -*- 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. + +import copy +from enum import IntEnum +from basicswap.util.crypto import blake256 +from basicswap.util.integer import decode_varint, encode_varint + + +class TxSerializeType(IntEnum): + Full = 0 + NoWitness = 1 + OnlyWitness = 2 + + +class COutpoint: + __slots__ = ('hash', 'n', 'tree') + + +class CTxIn: + __slots__ = ('prevout', 'sequence', + 'value_in', 'block_height', 'block_index', 'signature_script') # Witness + + +class CTxOut: + __slots__ = ('value', 'version', 'script_pubkey') + + +class CTransaction: + __slots__ = ('hash', 'version', 'vin', 'vout', 'locktime', 'expiry') + + def __init__(self, tx=None): + if tx is None: + self.version = 1 + self.vin = [] + self.vout = [] + self.locktime = 0 + self.expiry = 0 + else: + self.version = tx.version + self.vin = copy.deepcopy(tx.vin) + self.vout = copy.deepcopy(tx.vout) + self.locktime = tx.locktime + self.expiry = tx.expiry + + + def deserialize(self, data: bytes) -> None: + + version = int.from_bytes(data[:4], 'little') + self.version = self.version & 0xffff + ser_type: int = version >> 16 + o = 4 + + if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness: + num_txin, nb = decode_varint(data, o) + o += nb + + for i in range(num_txin): + txi = CTxIn() + txi.prevout = COutpoint() + txi.prevout.hash = int.from_bytes(data[o:o + 32], 'little') + o += 32 + txi.prevout.n = int.from_bytes(data[o:o + 4], 'little') + o += 4 + txi.prevout.tree = data[o] + o += 1 + txi.sequence = int.from_bytes(data[o:o + 4], 'little') + o += 4 + self.vin.append(txi) + + num_txout, nb = decode_varint(data, o) + o += nb + + for i in range(num_txout): + txo = CTxOut() + txo.value = int.from_bytes(data[o:o + 8], 'little') + o += 8 + txo.version = int.from_bytes(data[o:o + 2], 'little') + o += 2 + script_bytes, nb = decode_varint(data, o) + o += nb + txo.script_pubkey = data[o:o + script_bytes] + o += script_bytes + self.vout.append(txo) + + self.locktime = int.from_bytes(data[o:o + 4], 'little') + o += 4 + self.expiry = int.from_bytes(data[o:o + 4], 'little') + o += 4 + + num_wit_scripts, nb = decode_varint(data, o) + o += nb + + if ser_type == TxSerializeType.OnlyWitness: + self.vin = [CTxIn() for _ in range(num_wit_scripts)] + else: + if num_wit_scripts != len(self.vin): + raise ValueError('non equal witness and prefix txin quantities') + + for i in range(num_wit_scripts): + txi = self.vin[i] + txi.value_in = int.from_bytes(data[o:o + 8], 'little') + o += 8 + txi.block_height = int.from_bytes(data[o:o + 4], 'little') + o += 4 + txi.block_index = int.from_bytes(data[o:o + 4], 'little') + o += 4 + script_bytes, nb = decode_varint(data, o) + o += nb + txi.signature_script = data[o:o + script_bytes] + o += script_bytes + + def serialize(self, ser_type=TxSerializeType.Full) -> bytes: + data = bytearray() + version = (self.version & 0xffff) | (ser_type << 16) + data += version.to_bytes(4, 'little') + + if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness: + data += encode_varint(len(self.vin)) + for txi in self.vin: + data += txi.prevout.hash.to_bytes(32, 'little') + data += txi.prevout.n.to_bytes(4, 'little') + data += txi.prevout.tree.to_bytes(1) + data += txi.sequence.to_bytes(4, 'little') + + data += encode_varint(len(self.vout)) + for txo in self.vout: + data += txo.value.to_bytes(8, 'little') + data += txo.version.to_bytes(2, 'little') + data += encode_varint(len(txo.script_pubkey)) + data += txo.script_pubkey + + data += self.locktime.to_bytes(4, 'little') + data += self.expiry.to_bytes(4, 'little') + + if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness: + data += encode_varint(len(self.vin)) + for txi in self.vin: + data += txi.value_in.to_bytes(8, 'little') + data += txi.block_height.to_bytes(4, 'little') + data += txi.block_index.to_bytes(4, 'little') + data += encode_varint(len(txi.signature_script)) + data += txi.signature_script + + return data + + def TxHash(self) -> bytes: + return blake256(self.serialize(TxSerializeType.NoWitness))[::-1] + + def TxHashWitness(self) -> bytes: + raise ValueError('todo') + + def TxHashFull(self) -> bytes: + raise ValueError('todo') diff --git a/basicswap/util/integer.py b/basicswap/util/integer.py index 7b001a7..78940b3 100644 --- a/basicswap/util/integer.py +++ b/basicswap/util/integer.py @@ -5,13 +5,18 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -def decode_varint(b: bytes) -> int: - i = 0 - shift = 0 - for c in b: - i += (c & 0x7F) << shift - shift += 7 - return i +def decode_varint(b: bytes, offset: int = 0) -> (int, int): + i: int = 0 + num_bytes: int = 0 + while True: + c = b[offset + num_bytes] + i += (c & 0x7F) << (num_bytes * 7) + num_bytes += 1 + if not c & 0x80: + break + if num_bytes > 8: + raise ValueError('Too many bytes') + return i, num_bytes def encode_varint(i: int) -> bytes: diff --git a/bin/basicswap_run.py b/bin/basicswap_run.py index d5e384a..bf80b07 100755 --- a/bin/basicswap_run.py +++ b/bin/basicswap_run.py @@ -81,17 +81,17 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}): args.append('-datadir=' + datadir_path) args += opts logging.info('Starting node ' + daemon_bin + ' ' + (('-datadir=' + node_dir) if add_datadir else '')) - logging.info('[rm] {}'.format(' '.join(args))) opened_files = [] if extra_config.get('stdout_to_file', False): stdout_dest = open(os.path.join(datadir_path, extra_config.get('stdout_filename', 'core_stdout.log')), 'w') opened_files.append(stdout_dest) + stderr_dest = stdout_dest else: stdout_dest = subprocess.PIPE + stderr_dest = subprocess.PIPE - return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=subprocess.PIPE, cwd=datadir_path), opened_files) ->>>>>>> 676701b (tests: Start dcrd) + return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files) def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]): diff --git a/tests/basicswap/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py index 293ec9e..4f3a38b 100644 --- a/tests/basicswap/extended/test_dcr.py +++ b/tests/basicswap/extended/test_dcr.py @@ -68,7 +68,8 @@ def prepareDCDDataDir(datadir, node_id, conf_file, dir_prefix, num_nodes=3): f'rpclisten=127.0.0.1:{DCR_BASE_RPC_PORT + node_id}\n', f'rpcuser=test{node_id}\n', f'rpcpass=test_pass{node_id}\n', - 'notls=1\n',] + 'notls=1\n', + 'miningaddr=SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH\n',] for i in range(0, num_nodes): if node_id == i: @@ -87,7 +88,8 @@ def prepareDCDDataDir(datadir, node_id, conf_file, dir_prefix, num_nodes=3): f'username=test{node_id}\n', f'password=test_pass{node_id}\n', 'noservertls=1\n', - 'noclienttls=1\n',] + 'noclienttls=1\n', + 'enablevoting=1\n',] wallet_cfg_file_path = os.path.join(node_dir, 'dcrwallet.conf') with open(wallet_cfg_file_path, 'w+') as fp: @@ -101,6 +103,7 @@ class Test(BaseTest): dcr_daemons = [] start_ltc_nodes = False start_xmr_nodes = False + dcr_mining_addr = 'SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH' hex_seeds = [ 'e8574b2a94404ee62d8acc0258cab4c0defcfab8a5dfc2f4954c1f9d7e09d72a', @@ -110,7 +113,13 @@ class Test(BaseTest): @classmethod def prepareExtraCoins(cls): - pass + if not cls.restore_instance: + ci0 = cls.swap_clients[0].ci(cls.test_coin_from) + assert (ci0.rpc_wallet('getnewaddress') == cls.dcr_mining_addr) + cls.dcr_ticket_account = ci0.rpc_wallet('getaccount', [cls.dcr_mining_addr, ]) + ci0.rpc('generate', [110,]) + else: + cls.dcr_ticket_account = ci0.rpc_wallet('getaccount', [cls.dcr_mining_addr, ]) @classmethod def tearDownClass(cls): @@ -123,6 +132,21 @@ class Test(BaseTest): @classmethod def coins_loop(cls): super(Test, cls).coins_loop() + ci0 = cls.swap_clients[0].ci(cls.test_coin_from) + + num_passed: int = 0 + for i in range(5): + try: + ci0.rpc_wallet('purchaseticket', [cls.dcr_ticket_account, 0.1, 0]) + num_passed += 1 + except Exception as e: + logging.warning('coins_loop purchaseticket {}'.format(e)) + + try: + if num_passed >= 5: + ci0.rpc('generate', [1,]) + except Exception as e: + logging.warning('coins_loop generate {}'.format(e)) @classmethod def prepareExtraDataDir(cls, i): @@ -148,6 +172,7 @@ class Test(BaseTest): while p.poll() is None: while len(select.select([pipe_r], [], [], 0)[0]) == 1: buf = os.read(pipe_r, 1024).decode('utf-8') + logging.debug(f'dcrwallet {buf}') response = None if 'Use the existing configured private passphrase' in buf: response = b'y\n' @@ -157,6 +182,8 @@ class Test(BaseTest): response = b'y\n' elif 'Enter existing wallet seed' in buf: response = (cls.hex_seeds[i] + '\n').encode('utf-8') + elif 'Seed input successful' in buf: + pass else: raise ValueError(f'Unexpected output: {buf}') if response is not None: @@ -172,6 +199,8 @@ class Test(BaseTest): os.close(pipe_w) p.stdin.close() + test_delay_event.wait(1.0) + cls.dcr_daemons.append(startDaemon(appdata, DCR_BINDIR, DCR_WALLET, opts=extra_opts, extra_config={'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrwallet_stdout.log'})) logging.info('Started %s %d', DCR_WALLET, cls.dcr_daemons[-1].handle.pid) @@ -211,15 +240,7 @@ class Test(BaseTest): data = ci.decode_address(address) assert (data[2:] == pkh) - def test_001_segwit(self): - logging.info('---------- Test {} segwit'.format(self.test_coin_from.name)) - - swap_clients = self.swap_clients - - ci = swap_clients[0].ci(self.test_coin_from) - assert (ci.using_segwit() is True) - - for i, sc in enumerate(swap_clients): + for i, sc in enumerate(self.swap_clients): loop_ci = sc.ci(self.test_coin_from) root_key = sc.getWalletKey(Coins.DCR, 1) masterpubkey = loop_ci.rpc_wallet('getmasterpubkey') @@ -231,6 +252,39 @@ class Test(BaseTest): if i < 2: assert (seed_hash == hash160(masterpubkey_data)) + def test_001_segwit(self): + logging.info('---------- Test {} segwit'.format(self.test_coin_from.name)) + + swap_clients = self.swap_clients + + ci0 = swap_clients[0].ci(self.test_coin_from) + assert (ci0.using_segwit() is True) + + addr_out = ci0.rpc_wallet('getnewaddress') + addr_info = ci0.rpc_wallet('validateaddress', [addr_out,]) + assert (addr_info['isvalid'] is True) + assert (addr_info['ismine'] is True) + + rtx = ci0.rpc_wallet('createrawtransaction', [[], {addr_out: 2.0}]) + + account_from = ci0.rpc_wallet('getaccount', [self.dcr_mining_addr, ]) + frtx = ci0.rpc_wallet('fundrawtransaction', [rtx, account_from]) + + f_decoded = ci0.rpc_wallet('decoderawtransaction', [frtx['hex'], ]) + + sfrtx = ci0.rpc_wallet('signrawtransaction', [frtx['hex']]) + s_decoded = ci0.rpc_wallet('decoderawtransaction', [sfrtx['hex'], ]) + sent_txid = ci0.rpc_wallet('sendrawtransaction', [sfrtx['hex'], ]) + + assert (f_decoded['txid'] == sent_txid) + assert (f_decoded['txid'] == s_decoded['txid']) + assert (f_decoded['txid'] == s_decoded['txid']) + + ctx = ci0.loadTx(bytes.fromhex(sfrtx['hex'])) + ser_out = ctx.serialize() + assert (ser_out.hex() == sfrtx['hex']) + assert (f_decoded['txid'] == ctx.TxHash().hex()) + if __name__ == '__main__': unittest.main() diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index 087f3d7..ccac3f8 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -460,7 +460,7 @@ class Test(unittest.TestCase): for i, expect_length in test_vectors: b = encode_varint(i) assert (len(b) == expect_length) - assert (decode_varint(b) == i) + assert (decode_varint(b) == (i, expect_length)) def test_base58(self): kv = edu.get_secret()