Skip to content

Commit

Permalink
Calculate fees with adjusted vsize
Browse files Browse the repository at this point in the history
  • Loading branch information
Ouziel committed Jan 3, 2025
1 parent 3f56246 commit 9c3fc90
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 142 deletions.
76 changes: 69 additions & 7 deletions counterparty-core/counterpartycore/lib/composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,10 +603,62 @@ def generate_dummy_signed_tx(tx, selected_utxos):
return dummy_signed_tx


def get_needed_fee(tx, satoshis_per_vbyte, selected_utxos):
dummy_signed_tx = generate_dummy_signed_tx(tx, selected_utxos)
virtual_size = dummy_signed_tx.get_vsize()
return satoshis_per_vbyte * virtual_size
def get_output_sigops_count(script_pub_key, is_redeem_script=False, is_segwit=False):
output_type = script.get_output_type(script_pub_key)
multiplicator = 1 if is_segwit else 4
count = 0
if output_type in ["P2PK", "P2PKH"]:
count = 1
elif output_type == "P2MS":
if is_redeem_script:
asm = script.script_to_asm(script_pub_key)
pubkeys_count = int(asm[-2])
if pubkeys_count > 16:
count = 20
else:
count = pubkeys_count
else:
count = 20
elif output_type in ["P2WPKH", "P2WSH", "P2TR"] and is_redeem_script:
return 1
return count * multiplicator


def get_input_sigops_count(script_sig, script_pub_key):
prevout_type = script.get_output_type(script_pub_key)
if prevout_type == "P2SH":
asm = script.script_to_asm(script_sig)
redeem_script = binascii.hexlify(asm[-1]).decode("utf-8")
return get_output_sigops_count(redeem_script, is_redeem_script=True)
if prevout_type == "P2WPKH":
return 1
if prevout_type == "P2WSH":
return get_output_sigops_count(script_pub_key, is_segwit=True)
return 0


# source: https://bitcoin.stackexchange.com/questions/67760/how-are-sigops-calculated
def get_tx_sigops_count(tx, selected_utxos):
sigops_count = 0
for i, utxo in enumerate(selected_utxos):
script_pub_key = utxo["script_pub_key"]
script_sig = tx.inputs[i].script_sig.to_hex()
sigops_count += get_input_sigops_count(script_sig, script_pub_key)
for output in tx.outputs:
sigops_count += get_output_sigops_count(output.script_pubkey.to_hex())
return sigops_count


# source: https://mempool.space/docs/faq#what-is-adjusted-vsize
def get_size_info(tx, selected_utxos, signed=False):
if signed:
signed_tx = tx
else:
signed_tx = generate_dummy_signed_tx(tx, selected_utxos)
sigops_count = get_tx_sigops_count(signed_tx, selected_utxos)
virtual_size = signed_tx.get_vsize()
adjusted_vsize = max(sigops_count * 5, virtual_size)
return adjusted_vsize, virtual_size, sigops_count


def prepare_fee_parameters(construct_params):
Expand Down Expand Up @@ -640,6 +692,7 @@ def prepare_inputs_and_change(db, source, outputs, unspent_list, construct_param
change_outputs = []
btc_in = 0
needed_fee = 0
size_info = (0, 0, 0)
# try with one input and increase until the change is enough for the fee
use_all_inputs_set = construct_params.get("use_all_inputs_set", False)
input_count = len(unspent_list) if use_all_inputs_set else 1
Expand Down Expand Up @@ -682,7 +735,10 @@ def prepare_inputs_and_change(db, source, outputs, unspent_list, construct_param
+ [create_tx_output(change_amount, change_address, unspent_list, construct_params)],
has_segwit=has_segwit,
)
needed_fee = get_needed_fee(tx, sat_per_vbyte, selected_utxos)

size_info = get_size_info(tx, selected_utxos)
adjusted_vsize = size_info[0]
needed_fee = sat_per_vbyte * adjusted_vsize
if max_fee is not None:
needed_fee = min(needed_fee, max_fee)
# if change is enough for needed fee, add change output and break
Expand All @@ -696,7 +752,7 @@ def prepare_inputs_and_change(db, source, outputs, unspent_list, construct_param
# else try with more inputs
input_count += 1

return selected_utxos, btc_in, change_outputs
return selected_utxos, btc_in, change_outputs, size_info


def get_default_args(func):
Expand Down Expand Up @@ -742,7 +798,7 @@ def construct(db, tx_info, construct_params):
outputs = prepare_outputs(source, destinations, data, unspent_list, construct_params)

# prepare inputs and change
selected_utxos, btc_in, change_outputs = prepare_inputs_and_change(
selected_utxos, btc_in, change_outputs, size_info = prepare_inputs_and_change(
db, source, outputs, unspent_list, construct_params
)
inputs = utxos_to_txins(selected_utxos)
Expand All @@ -756,6 +812,7 @@ def construct(db, tx_info, construct_params):
lock_scripts = [utxo["script_pub_key"] for utxo in selected_utxos]
tx = Transaction(inputs, outputs + change_outputs)
unsigned_tx_hex = tx.serialize()
adjusted_vsize, virtual_size, sigops_count = size_info

return {
"rawtransaction": unsigned_tx_hex,
Expand All @@ -765,6 +822,11 @@ def construct(db, tx_info, construct_params):
"btc_fee": btc_in - btc_out - btc_change,
"data": config.PREFIX + data if data else None,
"lock_scripts": lock_scripts,
"signed_tx_estimated_size": {
"vsize": virtual_size,
"adjusted_vsize": adjusted_vsize,
"sigops_count": sigops_count,
},
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from bitcoinutils.keys import P2pkhAddress, P2wpkhAddress
from bitcoinutils.script import Script, b_to_h
from bitcoinutils.transactions import Transaction, TxInput, TxOutput
from bitcoinutils.transactions import TxInput, TxOutput

from counterpartycore.lib import config, exceptions

Expand Down Expand Up @@ -875,118 +875,6 @@
],
}
],
"get_needed_fee": [
{
"in": (
Transaction(
[
TxInput(UTXO_1.split(":")[0], int(UTXO_1.split(":")[1])),
TxInput(UTXO_2.split(":")[0], int(UTXO_2.split(":")[1])),
TxInput(UTXO_3.split(":")[0], int(UTXO_3.split(":")[1])),
],
[
TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()),
TxOutput(
config.DEFAULT_MULTISIG_DUST_SIZE,
Script(
[
1,
MULTISIG_PAIRS[0][0],
MULTISIG_PAIRS[0][1],
DEFAULT_PARAMS["pubkey"][ADDR[0]],
3,
"OP_CHECKMULTISIG",
]
),
),
TxOutput(
config.DEFAULT_MULTISIG_DUST_SIZE,
Script(
[
1,
MULTISIG_PAIRS[1][0],
MULTISIG_PAIRS[1][1],
DEFAULT_PARAMS["pubkey"][ADDR[0]],
3,
"OP_CHECKMULTISIG",
]
),
),
TxOutput(
config.DEFAULT_MULTISIG_DUST_SIZE,
Script(
[
1,
MULTISIG_PAIRS[2][0],
MULTISIG_PAIRS[2][1],
DEFAULT_PARAMS["pubkey"][ADDR[0]],
3,
"OP_CHECKMULTISIG",
]
),
),
],
),
3,
),
"out": 1527,
},
{
"in": (
Transaction(
[
TxInput(UTXO_1.split(":")[0], int(UTXO_1.split(":")[1])),
TxInput(UTXO_2.split(":")[0], int(UTXO_2.split(":")[1])),
TxInput(UTXO_3.split(":")[0], int(UTXO_3.split(":")[1])),
],
[
TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()),
TxOutput(
config.DEFAULT_MULTISIG_DUST_SIZE,
Script(
[
1,
MULTISIG_PAIRS[0][0],
MULTISIG_PAIRS[0][1],
DEFAULT_PARAMS["pubkey"][ADDR[0]],
3,
"OP_CHECKMULTISIG",
]
),
),
TxOutput(
config.DEFAULT_MULTISIG_DUST_SIZE,
Script(
[
1,
MULTISIG_PAIRS[1][0],
MULTISIG_PAIRS[1][1],
DEFAULT_PARAMS["pubkey"][ADDR[0]],
3,
"OP_CHECKMULTISIG",
]
),
),
TxOutput(
config.DEFAULT_MULTISIG_DUST_SIZE,
Script(
[
1,
MULTISIG_PAIRS[2][0],
MULTISIG_PAIRS[2][1],
DEFAULT_PARAMS["pubkey"][ADDR[0]],
3,
"OP_CHECKMULTISIG",
]
),
),
],
),
6,
),
"out": 3054,
},
],
"prepare_fee_parameters": [
{"in": ({"exact_fee": 1000},), "out": (1000, None, None)},
{"in": ({"exact_fee": 666, "max_fee": 1000},), "out": (666, None, None)},
Expand Down
4 changes: 2 additions & 2 deletions counterparty-core/counterpartycore/test/fixtures/vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
DEFAULT_PARAMS as DP,
)

# UNITTEST_VECTOR = COMPOSER_VECTOR
UNITTEST_VECTOR = COMPOSER_VECTOR

UNITTEST_VECTOR = (
UNITTEST_VECTOR_ = (
FAIRMINTER_VECTOR
| FAIRMINT_VECTOR
| LEDGER_VECTOR
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
**Notes about the Optional `encoding` Parameter.**
**Notes about the Optional `sat_per_vbyte` Parameter.**

By default the default value of the `encoding` parameter detailed above is `auto`, which means that `counterparty-server` automatically determines the best way to encode the Counterparty protocol data into a new transaction. If you know what you are doing and would like to explicitly specify an encoding:
To calculate the fees required for a transaction, we do not know the final size of the transaction before signing it.
So the composer injects fake script_sig and witnesses into the transaction before calculating the vsize.
Two remarks:

- To return the transaction as an **OP_RETURN** transaction, specify `opreturn` for the `encoding` parameter.
- **OP_RETURN** transactions cannot have more than 80 bytes of data. If you force `OP_RETURN` encoding and your transaction would have more than this amount, an exception will be generated.
1. this only works for standard scripts

- To return the transaction as a **multisig** transaction, specify `multisig` for the `encoding` parameter.
- `pubkey` should be set to the hex-encoded public key of the source address.
- Note that with the newest versions of Bitcoin (0.12.1 onward), bare multisig encoding does not reliably propagate. More information on this is documented [here](https://github.com/rubensayshi/counterparty-core/pull/9).

- To return the transaction as a **pubkeyhash** transaction, specify `pubkeyhash` for the `encoding` parameter.
- `pubkey` should be set to the hex-encoded public key of the source address.

- To return the transaction as a 2 part **P2SH** transaction, specify `P2SH` for the encoding parameter.
- First call the `create_` method with the `encoding` set to `P2SH`.
- Sign the transaction as usual and broadcast it. It's recommended but not required to wait for the transaction to confirm as malleability is an issue here (P2SH isn't yet supported on segwit addresses).
- The resulting `txid` must be passed again on an identic call to the `create_` method, but now passing an additional parameter `p2sh_pretx_txid` with the value of the previous transaction's id.
- The resulting transaction is a `P2SH` encoded message, using the redeem script on the transaction inputs as data carrying mechanism.
- Sign the transaction following the `Bitcoinjs-lib on javascript, signing a P2SH redeeming transaction` section
- **NOTE**: Don't leave pretxs hanging without transmitting the second transaction as this pollutes the UTXO set and risks making bitcoin harder to run on low spec nodes.
1. the size of DER signatures can vary by a few bytes and it is impossible to predict it. The composer uses a fixed size of 70 bytes so there may be a discrepancy of a few satoshis with the fees requested with `sat_per_vbyte` (for example if a DER signature is 72 bytes with `sat_per_vbyte=2` there will be an error of 4 sats in the calculated fees).
Loading

0 comments on commit 9c3fc90

Please sign in to comment.