diff --git a/test/functional/feature_fedpeg.py b/test/functional/feature_fedpeg.py index f30ab5f370..ee37e7bb6b 100755 --- a/test/functional/feature_fedpeg.py +++ b/test/functional/feature_fedpeg.py @@ -10,6 +10,7 @@ get_datadir_path, rpc_port, p2p_port, + tor_port, assert_raises_rpc_error, assert_equal, find_vout_for_address, @@ -82,7 +83,16 @@ def setup_network(self, split=False): "-keypool=1", "-listenonion=0", "-addresstype=legacy", # To make sure bitcoind gives back p2pkh no matter version - "-fallbackfee=0.0002" + "-fallbackfee=0.0002", + "-deprecatedrpc=create_bdb", # Required to create legacy (BDB) wallets on newer bitcoind + # bitcoind reads bitcoin.conf, not the elements.conf the test framework + # writes with bind=127.0.0.1, so the framework's collision-avoiding auto + # -bind is skipped. Without an explicit -bind, bitcoind binds P2P on + # 0.0.0.0:port and 127.0.0.1:port+1 (for incoming Tor connections), and + # port+1 collides with the next node's port. Bind explicitly to avoid + # the port+1 default. + "-bind=127.0.0.1:%s" % p2p_port(n), + "-bind=127.0.0.1:%s=onion" % tor_port(n), ]) else: extra_args.extend([ @@ -198,8 +208,11 @@ def run_test(self): WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"] # We just randomize the keys a bit to get another valid fedpegscript tweaked = sidechain.tweakfedpegscript("f00dbabe") - assert sidechain.getaddressinfo(tweaked['p2wsh'])['iswitness'] - assert not sidechain.getaddressinfo(tweaked['p2shwsh'])['iswitness'] + # tweakfedpegscript returns parent-chain-encoded addresses, so decode them + # with the parent node when the parent is bitcoin (bcrt prefix, not ert). + # addr_node = parent if self.options.parent_bitcoin else sidechain + assert parent.getaddressinfo(tweaked['p2wsh'])['iswitness'] + assert not parent.getaddressinfo(tweaked['p2shwsh'])['iswitness'] new_fedpegscript = tweaked["script"] if self.options.post_transition: print("Running test post-transition") @@ -224,7 +237,7 @@ def run_test(self): assert_equal(sidechain.decodescript(addrs["claim_script"])["type"], "witness_v0_keyhash") current_fedpegscript = sidechain.getsidechaininfo()["current_fedpegscripts"][0] tweaked = sidechain.tweakfedpegscript(addrs["claim_script"], current_fedpegscript) - if sidechain.getaddressinfo(addr)['iswitness']: + if parent.getaddressinfo(addr)['iswitness']: assert_equal(tweaked['p2wsh'], addr) else: assert_equal(tweaked['p2shwsh'], addr) diff --git a/test/functional/feature_pegin_subsidy.py b/test/functional/feature_pegin_subsidy.py index 04120d424a..c798bfc14f 100755 --- a/test/functional/feature_pegin_subsidy.py +++ b/test/functional/feature_pegin_subsidy.py @@ -5,6 +5,7 @@ # test/functional/feature_pegin_subsidy.py --parent_bitcoin --parent_binpath="/path/to/bitcoind" --nosandbox from decimal import Decimal +import math from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_raises_rpc_error, @@ -13,6 +14,7 @@ get_datadir_path, rpc_port, p2p_port, + tor_port, assert_equal, ) from test_framework import util @@ -29,6 +31,85 @@ def get_new_unconfidential_address(node, addr_type="bech32"): return val_addr["address"] +COIN_SATS = Decimal(100_000_000) + + +WITNESS_SCALE_FACTOR = 4 +FEDPEG_T = 11 # multisig threshold for self.fedpegscript (11-of-15) + + +def _cfeerate_construct(fee_sats, num_bytes): + """CFeeRate(nFeePaid, num_bytes) -> nSatoshisPerK, per src/policy/feerate.cpp: + nSatoshisPerK = nFeePaid * 1000 / nSize (C++ integer division).""" + if num_bytes > 0: + return (fee_sats * 1000) // num_bytes + return 0 + + +def _cfeerate_get_fee(sat_per_k, num_bytes): + """CFeeRate::GetFee(num_bytes), per src/policy/feerate.cpp: + ceil(nSatoshisPerK * nSize / 1000.0), with a floor of 1 sat if the + naive result rounds to zero and the rate is positive.""" + fee = math.ceil(sat_per_k * num_bytes / 1000.0) + if fee == 0 and num_bytes != 0: + if sat_per_k > 0: + fee = 1 + elif sat_per_k < 0: + fee = -1 + return int(fee) + + +def compute_expected_subsidy(parent, fedpegscript_hex, parent_pegged_asset_hex, pegin_txids, validating=True): + """Replicates CheckPeginSubsidyAndMinimum's expected_subsidy computation + exactly (src/validation.cpp), reading the parent-chain transaction + real fee/vsize.""" + fedpegscript_bytes = len(bytes.fromhex(fedpegscript_hex)) + weight = WITNESS_SCALE_FACTOR * (32 + 4 + 1 + 4) + (FEDPEG_T * 72 + fedpegscript_bytes) + vbytes = (weight + WITNESS_SCALE_FACTOR - 1) // WITNESS_SCALE_FACTOR + + parent_fee_sats = 0 + parent_vsize = 0 + per_tx_debug = [] + if validating: + for txid in pegin_txids: + gt = parent.gettransaction(txid) + blockhash = gt["blockhash"] + result = parent.getrawtransaction(txid, 2, blockhash) + tx_vsize = result["vsize"] + fee_field = result.get("fee", 0) + if isinstance(fee_field, dict): + tx_fee_btc = fee_field.get(parent_pegged_asset_hex, 0) + else: + tx_fee_btc = fee_field + tx_fee_sats = round(Decimal(str(tx_fee_btc)) * COIN_SATS) + parent_vsize += tx_vsize + parent_fee_sats += tx_fee_sats + per_tx_debug.append({"txid": txid, "vsize": tx_vsize, "fee_btc": tx_fee_btc, "fee_sats": tx_fee_sats}) + + sat_per_k = _cfeerate_construct(int(parent_fee_sats), parent_vsize) + sat_per_k = max(sat_per_k, 1000) # std::max(parent_feerate, CFeeRate{1000}) + + expected_subsidy_sats = _cfeerate_get_fee(sat_per_k, len(pegin_txids) * vbytes) + debug = { + "validating": validating, + "fedpegscript_bytes": fedpegscript_bytes, + "weight": weight, + "vbytes": vbytes, + "parent_fee_sats": int(parent_fee_sats), + "parent_vsize": parent_vsize, + "sat_per_k": sat_per_k, + "per_tx": per_tx_debug, + } + return Decimal(expected_subsidy_sats) / COIN_SATS, debug + + +def get_expected_subsidy(self, pegin_txids, validating=True): + expected, _debug = compute_expected_subsidy( + self.nodes[0], self.fedpegscript, self.parent_pegged_asset, pegin_txids, validating=validating + ) + return expected + + class PeginSubsidyTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True @@ -84,11 +165,16 @@ def setup_network(self, split=False): "-addresstype=legacy", # To make sure bitcoind gives back p2pkh no matter version "-fallbackfee=0.0002", "-deprecatedrpc=create_bdb", + # bitcoind reads bitcoin.conf, not the elements.conf the test framework + # writes with bind=127.0.0.1, so the framework's collision-avoiding auto + # -bind is skipped. Without an explicit -bind, bitcoind binds P2P on + # 0.0.0.0:port and 127.0.0.1:port+1 (for incoming Tor connections), and + # port+1 == p2p_port(1) collides with the first sidechain node. Bind + # explicitly to avoid the port+1 default. + "-bind=127.0.0.1:%s" % p2p_port(0), + "-bind=127.0.0.1:%s=onion" % tor_port(0), ] ) - self.expected_stderr = ( - f"Error: Unable to bind to 127.0.0.1:{p2p_port(1)} on this computer. Elements Core is probably already running." - ) else: extra_args.extend( [ @@ -99,7 +185,6 @@ def setup_network(self, split=False): "-dustrelayfee=0.00003000", # use the Bitcoin default dust relay fee rate for the parent nodes ] ) - self.expected_stderr = "" self.add_nodes(1, [extra_args], chain=[parent_chain], binary=parent_binary) self.start_node(0) @@ -113,8 +198,10 @@ def setup_network(self, split=False): ) self.parentgenesisblockhash = self.nodes[0].getblockhash(0) + self.parent_pegged_asset = None if not self.options.parent_bitcoin: parent_pegged_asset = self.nodes[0].getsidechaininfo()["pegged_asset"] + self.parent_pegged_asset = parent_pegged_asset # Setup sidechain nodes # use the current liquidv1 fedpegscript for testing purposes @@ -305,7 +392,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): pegintx = sidechain.createrawpegin(bitcoin_txhex, txoutproof, claim_script) signed = sidechain.signrawtransactionwithwallet(pegintx["hex"]) assert_equal(signed["complete"], True) - pegin_txid = sidechain.sendrawtransaction(signed["hex"]) + pegin_txid = sidechain.sendrawtransaction(signed["hex"], maxburnamount="1.0") pegin_tx = sidechain.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 2) self.generate(sidechain, 1, sync_fun=sync_sidechain) @@ -315,7 +402,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): pegintx = sidechain.createrawpegin(bitcoin_txhex, txoutproof, claim_script) signed = sidechain.signrawtransactionwithwallet(pegintx["hex"]) assert_equal(signed["complete"], True) - pegin_txid = sidechain.sendrawtransaction(signed["hex"]) + pegin_txid = sidechain.sendrawtransaction(signed["hex"], maxburnamount="1.0") pegin_tx = sidechain.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 2) self.generate(sidechain, 1, sync_fun=sync_sidechain) @@ -421,7 +508,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): }, ] fee = Decimal("0.00000363") - subsidy = Decimal("0.00000395") + subsidy = get_expected_subsidy(self, [txid]) - Decimal(1) / COIN_SATS outputs = [ {addr: Decimal("1.0") - fee - subsidy}, {changeaddr: utxo["amount"]}, @@ -487,17 +574,15 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): pegin_txid = sidechain.sendrawtransaction(signed["hex"], maxburnamount="1.0") pegin_tx = sidechain.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 3) - # WSH input 41 bytes * 4 = 164 weight - # Witness (11 * 72 bytes signatures + 626 bytes script size) = 1418 weight - # (164 + 1418 + 3) / 4 = 396 vbytes - assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000396")) + expected_subsidy = get_expected_subsidy(self, [txid]) + assert_equal(pegin_tx["decoded"]["vout"][1]["value"], expected_subsidy) self.generate(sidechain, 1, sync_fun=sync_sidechain) self.log.info("createrawpegin after enforcement, with validatepegin, above threshold") txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=3.0, feerate=2.0) pegintx = sidechain.createrawpegin(bitcoin_txhex, txoutproof, claim_script) signed = sidechain.signrawtransactionwithwallet(pegintx["hex"]) - pegin_txid = sidechain.sendrawtransaction(signed["hex"]) + pegin_txid = sidechain.sendrawtransaction(signed["hex"], maxburnamount="1.0") pegin_tx = sidechain.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 2) self.generate(sidechain, 1, sync_fun=sync_sidechain) @@ -516,11 +601,10 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): pegintx = sidechain2.createrawpegin(bitcoin_txhex, txoutproof, claim_script, feerate) signed = sidechain2.signrawtransactionwithwallet(pegintx["hex"]) - pegin_txid = sidechain.sendrawtransaction(signed["hex"], maxburnamount="1.0") - self.generate(sidechain, 1, sync_fun=sync_sidechain) + pegin_txid = sidechain2.sendrawtransaction(signed["hex"], maxburnamount="1.0") pegin_tx = sidechain2.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 3) - assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000792")) + assert_equal(pegin_tx["decoded"]["vout"][1]["value"], get_expected_subsidy(self, [txid])) self.generate(sidechain2, 1, sync_fun=sync_sidechain) self.log.info("createrawpegin after enforcement, without validatepegin, above threshold") @@ -538,7 +622,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script) pegin_tx = sidechain.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 3) - assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000792")) + assert_equal(pegin_tx["decoded"]["vout"][1]["value"], get_expected_subsidy(self, [txid])) self.generate(sidechain, 1, sync_fun=sync_sidechain) self.log.info("claimpegin after enforcement, with validatepegin, above threshold") @@ -563,7 +647,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): pegin_txid = sidechain2.claimpegin(bitcoin_txhex, txoutproof, claim_script, feerate) pegin_tx = sidechain2.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 3) - assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000792")) + assert_equal(pegin_tx["decoded"]["vout"][1]["value"], get_expected_subsidy(self, [txid])) self.generate(sidechain2, 1, sync_fun=sync_sidechain) self.log.info("claimpegin after enforcement, without validatepegin, above threshold") @@ -630,8 +714,12 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): ] fee = Decimal("0.00000363") addr = get_new_unconfidential_address(sidechain) - # subsidy less than 1 sat/vb - subsidy = Decimal("0.00000395") + sidechain2_threshold = get_expected_subsidy(self, [txid], validating=False) + sidechain_threshold = get_expected_subsidy(self, [txid], validating=True) + assert sidechain2_threshold < sidechain_threshold + + # subsidy one satoshi below sidechain2's (lower) threshold: both reject + subsidy = sidechain2_threshold - Decimal(1) / COIN_SATS outputs = [ {addr: Decimal("1.0") - fee - subsidy}, {"burn": subsidy}, @@ -650,8 +738,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): assert_equal(accept[0]["allowed"], False) assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low") - # subsidy for 1 sat/vb accepted by sidechain2, but rejected by validating node - subsidy = Decimal("0.00000396") + subsidy = sidechain2_threshold outputs = [ {addr: Decimal("1.0") - fee - subsidy}, {"burn": subsidy}, @@ -675,7 +762,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): pegin_txid = sidechain.claimpegin(bitcoin_txhex, txoutproof, claim_script) pegin_tx = sidechain.gettransaction(pegin_txid, True, True) assert_equal(len(pegin_tx["decoded"]["vout"]), 3) - assert_equal(pegin_tx["decoded"]["vout"][1]["value"], Decimal("0.00000396")) + assert_equal(pegin_tx["decoded"]["vout"][1]["value"], get_expected_subsidy(self, [txid])) # check manually constructed peg-in from a sub 1 sat/vb parent txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount=1, feerate=0.1) @@ -690,8 +777,10 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): ] fee = Decimal("0.00000363") addr = get_new_unconfidential_address(sidechain) - # subsidy too low - subsidy = Decimal("0.00000395") + required_subsidy = get_expected_subsidy(self, [txid]) + + # subsidy one satoshi below the real required minimum: too low + subsidy = required_subsidy - Decimal(1) / COIN_SATS outputs = [ {addr: Decimal("1.0") - fee - subsidy}, {"burn": subsidy}, @@ -705,8 +794,8 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): assert_equal(accept[0]["allowed"], False) assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low") - # subsidy accepted - subsidy = Decimal("0.00000396") + # subsidy at the real required minimum: accepted + subsidy = required_subsidy outputs = [ {addr: Decimal("1.0") - fee - subsidy}, {"burn": subsidy}, @@ -747,7 +836,10 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): }, ] fee = Decimal("0.00000363") - subsidy = Decimal("0.00001583") + required_subsidy = get_expected_subsidy(self, [txid1, txid2]) + + # subsidy one satoshi below the real required minimum + subsidy = required_subsidy - Decimal(1) / COIN_SATS outputs = [ {addr1: Decimal("0.5") - fee - subsidy}, {addr2: 1.0}, @@ -761,7 +853,8 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): assert_equal(accept[0]["allowed"], False) assert_equal(accept[0]["reject-reason"], "pegin-subsidy-too-low") - subsidy = Decimal("0.00001584") + # subsidy at the real required minimum + subsidy = required_subsidy outputs = [ {addr1: Decimal("0.5") - fee - subsidy}, {addr2: 1.0}, @@ -808,7 +901,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): signed = sidechain.signrawtransactionwithwallet(raw) accept = sidechain.testmempoolaccept([signed["hex"]]) assert_equal(accept[0]["allowed"], True) - sidechain.sendrawtransaction(signed["hex"]) + sidechain.sendrawtransaction(signed["hex"], maxburnamount="1.0") self.generate(sidechain2, 1, sync_fun=sync_sidechain) # minimum peg-in amount is 1.0 @@ -837,7 +930,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): }, ] fee = Decimal("0.00000363") - subsidy = Decimal("0.00000396") + subsidy = get_expected_subsidy(self, [txid]) + Decimal(10) / COIN_SATS outputs = [ {addr: Decimal("0.99999999") - fee - subsidy}, {"burn": subsidy}, @@ -875,8 +968,12 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): # dust error # restart node1 with no min peg-in amount - self.stop_node(1, expected_stderr=self.expected_stderr) # when running with bitcoind as parent node this stderr can occur + self.stop_node(1) self.start_node(1, extra_args=sidechain.extra_args + ["-peginminamount=0"]) + self.stop_node(2) + self.start_node(2, extra_args=sidechain2.extra_args + ["-peginminamount=0"]) + self.connect_nodes(1, 2) + self.sync_all([sidechain, sidechain2]) self.log.info("claimpegin dust error") amount = Decimal("0.00000546") if self.options.parent_bitcoin else Decimal("0.00000645") txid, vout, txoutproof, bitcoin_txhex, claim_script = parent_pegin(parent, sidechain, amount) @@ -901,9 +998,15 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): }, ] fee = Decimal("0.00000363") - subsidy = Decimal("0.00001194") + required_min_subsidy = get_expected_subsidy(self, [txid]) + target_primary_output_sats = Decimal(13) # dust-sized remainder + subsidy = Decimal("0.00001570") - fee - target_primary_output_sats / COIN_SATS + assert subsidy >= required_min_subsidy, ( + f"subsidy {subsidy} would be below the required minimum {required_min_subsidy} " + "-- dust-test arithmetic needs revisiting" + ) outputs = [ - {addr: Decimal("0.00001570") - fee - subsidy}, # 14 sats is dust at 0.1 sat/vb dustrelayfee + {addr: Decimal("0.00001570") - fee - subsidy}, {"burn": subsidy}, {"fee": fee}, ] @@ -928,7 +1031,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE): # Manually stop sidechains first, then the parent chain. self.stop_node(2) - self.stop_node(1, expected_stderr=self.expected_stderr) # when running with bitcoind as parent node this stderr can occur + self.stop_node(1) self.stop_node(0)