#!/usr/bin/env python3 # Copyright (c) 2026 The Limenka developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Fork-chain height-based locktime binds to the parent chain's cadence. HTLCs (and any contract using CLTV with a height-based nLockTime) must resolve at the same real time on the fork chain and on the parent (spamchain) chain, even though the two chains diverge in height after the fork point. The fork chain therefore interprets a height-based locktime L against an effective height that advances one per ~600s of the fork chain's own median time past since the fork point - i.e. when a spamchain block at height L appears - rather than the fork chain's literal height. The fork point is height 50. The fork chain mines at 300s/block (twice the parent cadence), so after 110 blocks the literal height is 160 but only 110 * 300s = 55 * 600s of real time has elapsed: the effective height is 105. A transaction with nLockTime = 110 must therefore be NON-FINAL at literal height 160 (a literal-height rule would wrongly accept it), and become final only once the effective height reaches 110. """ from decimal import Decimal from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import LimenkaTestFramework BASE_TIME = 1700000000 FORK_POINT = 50 class ForkHtlcLocktimeTest(LimenkaTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.chain = 'limenka' self.extra_args = [[ '-forkactivationtime=1', '-forkactivationheight=%d' % FORK_POINT, '-forkdelaysteps=1024', '-forkmineondemand', '-forkstandalone', '-fallbackfee=0.0002', '-addresstype=bech32', ]] self.rpc_timeout = 240 def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): self.add_wallet_options(parser, descriptors=True, legacy=False) def init_wallet(self, *, node): wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[node] if node < len(self.wallet_names) else False if wallet_name is not False: n = self.nodes[node] if wallet_name is not None: n.createwallet(wallet_name=wallet_name, descriptors=True, load_on_startup=True) import hashlib from test_framework.address import byte_to_base58 from test_framework.descriptors import descsum_create from test_framework.key import ECKey key = ECKey() key.set(hashlib.sha256(b'fork-htlc-funding').digest(), compressed=True) priv = key.get_bytes() wif = byte_to_base58(priv + b'\x01', 128) desc = descsum_create(f"wpkh({wif})") import_res = n.importdescriptors([{ "desc": desc, "timestamp": 1231006506, "active": False, "internal": False, }]) assert all(r['success'] for r in import_res) def mine_cadence(self, node, addr, n, step, start_index=0): for i in range(n): node.setmocktime(BASE_TIME + (start_index + i) * step) self.generatetoaddress(node, 1, addr, sync_fun=self.no_op) def run_test(self): node = self.nodes[0] mine_addr = node.getnewaddress() # Mine to literal height 160 at a 300s cadence. Effective height at # the tip: 50 + (155*300 - 45*300) / 600 = 105. self.mine_cadence(node, mine_addr, 160, 300) assert node.getblockcount() == 160 # A raw contract tx with nLockTime = 110, between the effective # height (105) and the literal height (160). utxo = node.listunspent()[0] dest = node.getnewaddress() raw = node.createrawtransaction( [{"txid": utxo["txid"], "vout": utxo["vout"]}], [{dest: utxo["amount"] - Decimal("0.001")}], 110, ) signed = node.signrawtransactionwithwallet(raw) assert signed["complete"] # A literal-height rule would accept locktime 110 at height 160; the # cadence-bound rule must reject it (effective height is 105). rejected = False try: node.sendrawtransaction(signed["hex"]) except Exception as e: assert "non-final" in str(e), str(e) rejected = True assert rejected, "locktime 110 accepted at literal height 160 (effective 105)" # Mine to literal height 270 at 300s cadence: effective height = # 50 + (265*300 - 45*300) / 600 = 160, which is >= 110. The tx is # now final even though it was created 110 blocks ago. self.mine_cadence(node, mine_addr, 110, 300, start_index=160) assert node.getblockcount() == 270 txid = node.sendrawtransaction(signed["hex"]) assert txid in node.getrawmempool() self.log.info("height-based locktime resolved at the parent cadence (effective height reached 110)") if __name__ == '__main__': ForkHtlcLocktimeTest(__file__).main()