From 74c7072926ba33da7652c12f2785caa1619bd660 Mon Sep 17 00:00:00 2001
From: tecnovert <tecnovert@tecnovert.net>
Date: Mon, 29 Apr 2024 00:40:14 +0200
Subject: [PATCH] Decred CSV test.

---
 basicswap/interface/dcr/dcr.py       | 28 +++++++++-
 basicswap/interface/dcr/messages.py  | 19 +++++--
 basicswap/interface/dcr/script.py    |  9 +++-
 basicswap/interface/nav.py           |  2 +-
 tests/basicswap/extended/test_dcr.py | 78 ++++++++++++++++++++++++++++
 tests/basicswap/extended/test_nav.py |  2 +-
 6 files changed, 130 insertions(+), 8 deletions(-)

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, ])