diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index 41e3527..b20c656 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -21,8 +21,8 @@ from basicswap.util.crypto import ( from basicswap.util.extkey import ExtKeyPair from basicswap.util.integer import encode_varint from basicswap.interface.dcr.rpc import make_rpc_func -from .messages import CTransaction, SigHashType, TxSerializeType -from .script import push_script_data +from .messages import CTransaction, CTxOut, SigHashType, TxSerializeType +from .script import push_script_data, OP_HASH160, OP_EQUAL, OP_DUP, OP_EQUALVERIFY, OP_CHECKSIG from coincurve.keys import ( PrivateKey @@ -128,6 +128,14 @@ class DCRInterface(Secp256k1Interface): def nbK() -> int: # No. of bytes requires to encode a public key return 33 + @staticmethod + def txVersion() -> int: + return 2 + + @staticmethod + def txoType(): + return CTxOut + def __init__(self, coin_settings, network, swap_client=None): super().__init__(network) self._rpc_host = coin_settings.get('rpchost', '127.0.0.1') @@ -169,6 +177,9 @@ class DCRInterface(Secp256k1Interface): else: self.rpc('getblockchaininfo') + def getChainHeight(self) -> int: + return self.rpc('getblockcount') + def checkWallets(self) -> int: # Only one wallet possible? return 1 @@ -239,3 +250,16 @@ class DCRInterface(Secp256k1Interface): def stripTxSignature(self, tx_bytes) -> bytes: tx = self.loadTx(tx_bytes) return tx.serialize(TxSerializeType.NoWitness) + + def getScriptDest(self, script: bytes) -> bytes: + # P2SH + script_hash = self.pkh(script) + assert len(script_hash) == 20 + + return OP_HASH160.to_bytes(1) + len(script_hash).to_bytes(1) + script_hash + OP_EQUAL.to_bytes(1) + + def getPubkeyHashDest(self, pkh: bytes) -> bytes: + # P2PKH + + assert len(pkh) == 20 + return OP_DUP.to_bytes(1) + OP_HASH160.to_bytes(1) + len(pkh).to_bytes(1) + pkh + OP_EQUALVERIFY.to_bytes(1) + OP_CHECKSIG.to_bytes(1) diff --git a/basicswap/interface/dcr/messages.py b/basicswap/interface/dcr/messages.py index f83d3e2..723e0c2 100644 --- a/basicswap/interface/dcr/messages.py +++ b/basicswap/interface/dcr/messages.py @@ -32,9 +32,14 @@ class SignatureType(IntEnum): STSchnorrSecp256k1 = 2 -class COutpoint: +class COutPoint: __slots__ = ('hash', 'n', 'tree') + def __init__(self, hash=0, n=0, tree=0): + self.hash = hash + self.n = n + self.tree = tree + def get_hash(self) -> bytes: return self.hash.to_bytes(32, 'big') @@ -43,15 +48,23 @@ class CTxIn: __slots__ = ('prevout', 'sequence', 'value_in', 'block_height', 'block_index', 'signature_script') # Witness - def __init__(self, tx=None): + def __init__(self, prevout=COutPoint(), sequence=0): + self.prevout = prevout + self.sequence = sequence self.value_in = -1 self.block_height = 0 self.block_index = 0xffffffff + self.signature_script = bytes() class CTxOut: __slots__ = ('value', 'version', 'script_pubkey') + def __init__(self, value=0, script_pubkey=bytes()): + self.value = value + self.version = 0 + self.script_pubkey = script_pubkey + class CTransaction: __slots__ = ('hash', 'version', 'vin', 'vout', 'locktime', 'expiry') @@ -83,7 +96,7 @@ class CTransaction: for i in range(num_txin): txi = CTxIn() - txi.prevout = COutpoint() + 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') diff --git a/basicswap/interface/dcr/script.py b/basicswap/interface/dcr/script.py index adef809..78bb6f5 100644 --- a/basicswap/interface/dcr/script.py +++ b/basicswap/interface/dcr/script.py @@ -9,9 +9,15 @@ OP_0 = 0x00 OP_DATA_1 = 0x01 OP_1NEGATE = 0x4f OP_1 = 0x51 +OP_EQUAL = 0x87 OP_PUSHDATA1 = 0x4c OP_PUSHDATA2 = 0x4d OP_PUSHDATA4 = 0x4e +OP_DUP = 0x76 +OP_EQUALVERIFY = 0x88 +OP_HASH160 = 0xa9 +OP_CHECKSIG = 0xac +OP_CHECKSEQUENCEVERIFY = 0xb2 def push_script_data(data_array: bytearray, data: bytes) -> None: @@ -28,7 +34,7 @@ def push_script_data(data_array: bytearray, data: bytes) -> None: return if len_data < OP_PUSHDATA1: - data_array += bytes(((OP_DATA_1 - 1) + len_data,)) + data_array += len_data.to_bytes(1) elif len_data <= 0xff: data_array += bytes((OP_PUSHDATA1, len_data)) elif len_data <= 0xffff: @@ -36,4 +42,5 @@ def push_script_data(data_array: bytearray, data: bytes) -> None: else: data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, 'little') + print('[rm] data_array', (data_array + data).hex()) data_array += data diff --git a/basicswap/interface/nav.py b/basicswap/interface/nav.py index 39de503..53c7717 100644 --- a/basicswap/interface/nav.py +++ b/basicswap/interface/nav.py @@ -488,7 +488,7 @@ class NAVInterface(BTCInterface): return block_rv - def getScriptScriptSig(self, script: bytes) -> bytearray: + def getScriptScriptSig(self, script: bytes) -> bytes: return self.getP2SHP2WSHScriptSig(script) def getScriptDest(self, script): diff --git a/tests/basicswap/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py index d8b20fe..689752c 100644 --- a/tests/basicswap/extended/test_dcr.py +++ b/tests/basicswap/extended/test_dcr.py @@ -36,6 +36,8 @@ from tests.basicswap.util import ( from tests.basicswap.test_xmr import BaseTest, test_delay_event from basicswap.interface.dcr import DCRInterface +from basicswap.interface.dcr.messages import CTransaction, CTxIn, COutPoint +from basicswap.interface.dcr.script import OP_CHECKSEQUENCEVERIFY, push_script_data from bin.basicswap_run import startDaemon logger = logging.getLogger() @@ -276,6 +278,7 @@ class Test(BaseTest): frtx = ci0.rpc_wallet('fundrawtransaction', [rtx, account_from]) f_decoded = ci0.rpc_wallet('decoderawtransaction', [frtx['hex'], ]) + assert (f_decoded['version'] == 1) sfrtx = ci0.rpc_wallet('signrawtransaction', [frtx['hex']]) s_decoded = ci0.rpc_wallet('decoderawtransaction', [sfrtx['hex'], ]) @@ -330,6 +333,7 @@ class Test(BaseTest): # Set prevout value ctx = ci0.loadTx(tx_bytes_signed) + assert (ctx.vout[0].version == 0) ctx.vin[0].value_in = ci0.make_int(prevout['amount']) tx_bytes_signed = ctx.serialize() assert (tx_bytes_signed.hex() == sfrtx['hex']) @@ -337,6 +341,80 @@ class Test(BaseTest): sent_txid = ci0.rpc_wallet('sendrawtransaction', [tx_bytes_signed.hex(), ]) assert (len(sent_txid) == 64) + def test_004_csv(self): + logging.info('---------- Test {} csv'.format(self.test_coin_from.name)) + swap_clients = self.swap_clients + ci0 = swap_clients[0].ci(self.test_coin_from) + + script = bytearray() + push_script_data(script, bytes((3,))) + script += OP_CHECKSEQUENCEVERIFY.to_bytes(1) + + script_dest = ci0.getScriptDest(script) + + prevout_amount: int = ci0.make_int(1.1) + tx = CTransaction() + tx.version = ci0.txVersion() + tx.vout.append(ci0.txoType()(prevout_amount, script_dest)) + tx_hex = tx.serialize().hex() + tx_decoded = ci0.rpc_wallet('decoderawtransaction', [tx_hex, ]) + + utxo_pos = None + script_address = None + for i, txo in enumerate(tx_decoded['vout']): + script_address = tx_decoded['vout'][0]['scriptPubKey']['addresses'][0] + addr_info = ci0.rpc_wallet('validateaddress', [script_address,]) + if addr_info['isscript'] is True: + utxo_pos = i + break + assert (utxo_pos is not None) + + accounts = ci0.rpc_wallet('listaccounts') + for account_from in accounts: + try: + frtx = ci0.rpc_wallet('fundrawtransaction', [tx_hex, account_from]) + break + except Exception as e: + logging.warning('fundrawtransaction failed {}'.format(e)) + sfrtx = ci0.rpc_wallet('signrawtransaction', [frtx['hex']]) + sent_txid = ci0.rpc_wallet('sendrawtransaction', [sfrtx['hex'], ]) + + tx_spend = CTransaction() + tx_spend.version = ci0.txVersion() + + tx_spend.vin.append(CTxIn(COutPoint(int(sent_txid, 16), utxo_pos), sequence=3)) + tx_spend.vin[0].value_in = prevout_amount + signature_script = bytearray() + push_script_data(signature_script, script) + tx_spend.vin[0].signature_script = signature_script + + addr_out = ci0.rpc_wallet('getnewaddress') + pkh = ci0.decode_address(addr_out)[2:] + + tx_spend.vout.append(ci0.txoType()()) + tx_spend.vout[0].value = ci0.make_int(1.09) + tx_spend.vout[0].script_pubkey = ci0.getPubkeyHashDest(pkh) + + tx_spend_hex = tx_spend.serialize().hex() + + try: + sent_spend_txid = ci0.rpc_wallet('sendrawtransaction', [tx_spend_hex, ]) + except Exception as e: + assert ('transaction sequence locks on inputs not met' in str(e)) + else: + assert False, 'Should fail' + + sent_spend_txid = None + for i in range(20): + try: + sent_spend_txid = ci0.rpc_wallet('sendrawtransaction', [tx_spend_hex, ]) + break + except Exception as e: + logging.info('sendrawtransaction failed {}, height {}'.format(e, ci0.getChainHeight())) + test_delay_event.wait(1) + + assert (sent_spend_txid is not None) + if __name__ == '__main__': unittest.main() diff --git a/tests/basicswap/extended/test_nav.py b/tests/basicswap/extended/test_nav.py index 4019207..4bdb431 100644 --- a/tests/basicswap/extended/test_nav.py +++ b/tests/basicswap/extended/test_nav.py @@ -425,7 +425,7 @@ class Test(TestFunctions): tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest)) tx_hex = ToHex(tx) tx_funded = self.callnoderpc('fundrawtransaction', [tx_hex]) - utxo_pos = 0 if tx_funded['changepos'] == 1 else 1 + utxo_pos: int = 0 if tx_funded['changepos'] == 1 else 1 tx_signed = self.callnoderpc('signrawtransaction', [tx_funded['hex'], ])['hex'] self.sync_blocks() txid = self.callnoderpc('sendrawtransaction', [tx_signed, ])