Skip to content
Open
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
139 changes: 117 additions & 22 deletions test/functional/feature_pegin_subsidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,83 @@ def get_new_unconfidential_address(node, addr_type="bech32"):
return val_addr["address"]


COIN_SATS = Decimal(100_000_000)

import math as _math

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_obj = result.get("fee", {})
tx_fee_btc = fee_obj.get(parent_pegged_asset_hex, 0)
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 @@ -114,6 +191,7 @@ def setup_network(self, split=False):
self.parentgenesisblockhash = self.nodes[0].getblockhash(0)
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 +498,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 +563,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 +593,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 +611,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 +636,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 +703,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 +727,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 +751,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 +766,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 +783,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 +825,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 +842,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 +919,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 @@ -873,6 +958,10 @@ def parent_pegin(parent, node, amount=1.0, feerate=DEFAULT_FEERATE):
# 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.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 +986,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 Down
Loading