#!/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. """End-to-end confidential transaction loop on the fork standalone chain. Mints transparent value into CT, pays a stealth (lm2) address non-interactively, and verifies the wallet scans and recovers the payment from on-chain data alone. Flow: - mine mature transparent coins - mintct 10 (transparent -> confidential, kernel balance) - listctreceipts (minted output + change are spendable) - getnewstealthaddress (lm2, view + spend keys) - sendtostealth lm2 4 (non-interactive confidential payment) - mine the payments and verify the wallet recovered the stealth output - kernel outputs carry the "BK" magic in every CT transaction """ import hashlib from test_framework.address import byte_to_base58 from test_framework.descriptors import descsum_create from test_framework.key import ECKey from test_framework.segwit_addr import encode_segwit_address from test_framework.test_framework import LimenkaTestFramework from test_framework.util import ( assert_equal, assert_greater_than, ) GENESIS_TIME = 1231006505 class ForkCTTest(LimenkaTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.chain = 'limenka' self.extra_args = [[ '-forkactivationtime=1', '-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) # The fork chain stamps from 2009 genesis, but a fresh wallet # birthtime is 2026 - blocks older than the birthtime are # skipped. Import a deterministic funding key with an old # timestamp so the wallet tracks the mocktime-mined chain. seed = hashlib.sha256(b'fork-ct-funding').digest() key = ECKey() key.set(seed, compressed=True) priv = key.get_bytes() wif = byte_to_base58(priv + b'\x01', 128) # mainnet WIF, compressed desc = descsum_create(f"wpkh({wif})") import_res = n.importdescriptors([{ "desc": desc, "timestamp": GENESIS_TIME + 600, "active": False, "internal": False, }]) self.log.info(f"importdescriptors result: {import_res}") self.funding_key = priv self.funding_pub = key.get_pubkey().get_bytes() def mine_blocks(self, node, count, addr, start_mt): mt = start_mt hashes = [] for _ in range(count): node.setmocktime(mt) h = self.generatetoaddress(node, 1, addr, sync_fun=self.no_op)[0] hashes.append(h) mt += 600 return hashes def run_test(self): node = self.nodes[0] bi = node.getblockchaininfo() assert_equal(bi['chain'], 'limenka') self.log.info(f"chain {bi['chain']}, delay steps {bi['fork_delay_steps']}") # The fork chain's witness HRP is "bf" (mainnet base58 prefixes). from test_framework.address import hash160 p2wpkh_program = hash160(self.funding_pub) addr = encode_segwit_address("bf", 0, p2wpkh_program) # 110 blocks: coinbase maturity (100) plus margin, at ~600s mocktime. self.log.info(f"addr to mine: {addr}") self.mine_blocks(node, 110, addr, GENESIS_TIME + 600) self.log.info(f"walletinfo: {node.getwalletinfo()}") self.log.info(f"listdescriptors: {node.listdescriptors()}") self.log.info(f"unspent: {node.listunspent()}") bal = node.getbalance() self.log.info(f"transparent balance after mining: {bal}") assert_greater_than(bal, 100) # --- Stealth address --- stealth = node.getnewstealthaddress() self.log.info(f"stealth address: {stealth['address']}") assert stealth['address'].startswith('lm2') assert_equal(len(stealth['view_pubkey']), 66) assert_equal(len(stealth['spend_pubkey']), 66) # --- Mint: transparent value enters the confidential domain --- # Explicit fee: 1000 sats (1e-5 lambda) - comfortably above min relay. # Split the minted value into 3 random confidential outputs: every # output is CT (a transparent change would be rejected and would # break the privacy). mint_txid = node.mintct("10", "0.00001", 3)['txid'] self.log.info(f"mint txid: {mint_txid}") receipts = node.listctreceipts() self.log.info(f"receipts after mint: {receipts}") mint_receipts = [r for r in receipts if r['txid'] == mint_txid] assert_equal(len(mint_receipts), 3) # All three outputs are confidential and positive (no transparent # change leaked). for r in mint_receipts: assert_greater_than(float(r['amount']), 0) # Every CT tx carries the BK kernel. mint_raw = node.getrawtransaction(mint_txid, True) kernel_hexes = [v['scriptPubKey']['hex'] for v in mint_raw['vout']] assert any(h.startswith('6a02424b') for h in kernel_hexes), kernel_hexes # --- Non-interactive stealth payment --- pay_txid = node.sendtostealth(stealth['address'], "4", "0.00001")['txid'] self.log.info(f"stealth payment txid: {pay_txid}") pay_raw = node.getrawtransaction(pay_txid, True) kernel_hexes = [v['scriptPubKey']['hex'] for v in pay_raw['vout']] assert any(h.startswith('6a02424b') for h in kernel_hexes), kernel_hexes # Mine both transactions. self.mine_blocks(node, 1, addr, GENESIS_TIME + 111 * 600) # PSBT round-trip: build an unsigned CT mint, finalize it (sign the # transparent inputs + the kernel), and broadcast the result. psbt = node.createctpsbt("2", "0.00001", 2) self.log.info(f"created CT psbt (len {len(psbt)})") final_tx_hex = node.finalizectpsbt(psbt) self.log.info(f"finalized CT tx hex len {len(final_tx_hex)}") accept = node.testmempoolaccept([final_tx_hex]) assert accept[0]['allowed'], accept node.sendrawtransaction(final_tx_hex) self.mine_blocks(node, 1, addr, GENESIS_TIME + 112 * 600) receipts = node.listctreceipts() self.log.info(f"receipts after psbt mint: {receipts}") # PSBT stealth-spend: build an unsigned CT spend to a fresh stealth # address, finalize it (attach proofs + sign the kernel), and # broadcast. The receiver's blinding rides the kernel offset. stealth2 = node.getnewstealthaddress() stealth_psbt = node.createstealthpsbt(stealth2['address'], "1", "0.00001", 1) self.log.info(f"created stealth CT psbt (len {len(stealth_psbt)})") stealth_hex = node.finalizectpsbt(stealth_psbt) self.log.info(f"finalized stealth CT tx hex len {len(stealth_hex)}") accept2 = node.testmempoolaccept([stealth_hex]) assert accept2[0]['allowed'], accept2 node.sendrawtransaction(stealth_hex) self.mine_blocks(node, 1, addr, GENESIS_TIME + 113 * 600) receipts = node.listctreceipts() self.log.info(f"receipts after psbt stealth: {receipts}") # lm1 (P2SPKH) spend via PSBT on the fork chain: exercises the # forkid Schnorr signing (the marker binds in the hash, not the # BIP341 type byte) and the PSBT signing path forkid. lm1_addr = node.getnewaddress("", "p2spkh") assert lm1_addr.startswith('lm1'), lm1_addr node.sendtoaddress(lm1_addr, "2") self.mine_blocks(node, 1, addr, GENESIS_TIME + 114 * 600) unspent = node.listunspent(0, 9999999, [lm1_addr]) assert_equal(len(unspent), 1) utxo = unspent[0] psbt = node.walletcreatefundedpsbt( [{"txid": utxo["txid"], "vout": utxo["vout"]}], {addr: "1.5"}, 0, {"subtractFeeFromOutputs": [0]})["psbt"] processed = node.walletprocesspsbt(psbt) assert processed["complete"], processed signed_hex = node.finalizepsbt(processed["psbt"])["hex"] accept = node.testmempoolaccept([signed_hex]) assert accept[0]["allowed"], accept node.sendrawtransaction(signed_hex) self.mine_blocks(node, 1, addr, GENESIS_TIME + 115 * 600) self.log.info("lm1 PSBT spend complete") # 128-bit precision: getbalances().mine.precise includes the # confidential outputs (10 - 4 spent = 6 remaining from the mint # change, plus the stealth 4 and its ~6 change) on top of the # transparent balance. balances = node.getbalances() self.log.info(f"precise balance: {balances['mine'].get('precise')}") assert 'precise' in balances['mine'] # The wallet scans on-chain data and recovers the stealth payment: # a spendable receipt for exactly 4 lambda must appear. receipts = node.listctreceipts() self.log.info(f"receipts after payment: {receipts}") amounts = {r['amount'] for r in receipts} assert '4' in amounts, f"stealth payment not recovered: {amounts}" assert '1' in amounts, f"psbt stealth payment not recovered: {amounts}" self.log.info("CT loop complete: mint -> stealth pay -> scan -> spendable") if __name__ == '__main__': ForkCTTest(__file__).main()