Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions test/functional/feature_fedpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
get_datadir_path,
rpc_port,
p2p_port,
tor_port,
assert_raises_rpc_error,
assert_equal,
find_vout_for_address,
Expand Down Expand Up @@ -81,7 +82,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([
Expand Down Expand Up @@ -197,8 +207,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")
Expand All @@ -223,7 +236,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)
Expand Down
160 changes: 132 additions & 28 deletions test/functional/feature_pegin_subsidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,6 +14,7 @@
get_datadir_path,
rpc_port,
p2p_port,
tor_port,
assert_equal,
)
from test_framework import util
Expand All @@ -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
Expand Down Expand Up @@ -83,11 +164,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(
[
Expand All @@ -98,7 +184,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)
Expand All @@ -112,8 +197,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
Expand Down Expand Up @@ -420,7 +507,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"]},
Expand Down Expand Up @@ -485,10 +572,8 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE):
pegin_txid = sidechain.sendrawtransaction(signed["hex"])
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")
Expand Down Expand Up @@ -517,7 +602,7 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE):
pegin_txid = sidechain2.sendrawtransaction(signed["hex"])
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")
Expand All @@ -535,7 +620,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")
Expand All @@ -560,7 +645,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")
Expand Down Expand Up @@ -627,8 +712,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},
Expand All @@ -647,8 +736,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},
Expand All @@ -672,7 +760,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)
Expand All @@ -687,8 +775,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},
Expand All @@ -702,8 +792,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},
Expand Down Expand Up @@ -744,7 +834,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},
Expand All @@ -758,7 +851,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},
Expand Down Expand Up @@ -834,7 +928,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},
Expand Down Expand Up @@ -871,8 +965,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)
Expand All @@ -897,9 +995,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},
]
Expand All @@ -923,7 +1027,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)


Expand Down
4 changes: 4 additions & 0 deletions test/functional/test_framework/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@ def rpc_port(n):
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)


def tor_port(n):
return PORT_MIN + PORT_RANGE * 2 + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)


def rpc_url(datadir, i, chain, rpchost):
rpc_u, rpc_p = get_auth_cookie(datadir, chain)
host = '127.0.0.1'
Expand Down
Loading