Add Decred transaction to and from bytes.
This commit is contained in:
		
							parent
							
								
									761d0ca505
								
							
						
					
					
						commit
						150caeec40
					
				@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										158
									
								
								basicswap/interface/dcr/messages.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								basicswap/interface/dcr/messages.py
									
									
									
									
									
										Normal file
									
								
							@ -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')
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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=[]):
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user