Merge pull request #10453 from f321x/debug_rbf_fee_calculation

wallet: estimate base tx feerate based on original base tx size
This commit is contained in:
ThomasV
2026-02-24 16:45:07 +01:00
committed by GitHub
3 changed files with 149 additions and 12 deletions
+10 -4
View File
@@ -52,7 +52,7 @@ from .bitcoin import (
from .crypto import sha256d, sha256
from .logging import get_logger
from .util import ShortID, OldTaskGroup
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address, DUMMY_DER_SIG
if TYPE_CHECKING:
from .wallet import Abstract_Wallet
@@ -1008,8 +1008,7 @@ class Transaction:
return construct_witness([])
if estimate_size and hasattr(txin, 'make_witness'):
sig_dummy = b'\x00' * 71 # DER-encoded ECDSA sig, with low S and low R
txin.witness_sizehint = len(txin.make_witness(sig_dummy))
txin.witness_sizehint = len(txin.make_witness(DUMMY_DER_SIG))
if estimate_size and txin.witness_sizehint is not None:
return bytes(txin.witness_sizehint)
@@ -1350,7 +1349,14 @@ class Transaction:
BIP-0141 defines 'Virtual transaction size' to be weight/4 rounded up.
This definition is only for humans, and has little meaning otherwise.
If we wanted sub-byte precision, fee calculation should use transaction
weights, but for simplicity we approximate that with (virtual_size)x4
weights, but for simplicity we approximate that with (virtual_size)x4.
note: while we try to estimate as close to the true value as possible,
whenever that's not possible, we should over-estimate. E.g. ecdsa DER sig
sizes can be 71 or 72 bytes (even 73 though that is non-standard).
Over-estimating is preferred as the typical use-case is the user selecting
a target_feerate, and the code calculating abs fees as target_feerate*est_size.
If we over-estimate est_size there, that means the final true_feerate is going to
be higher than target_feerate, which is desirable especially near the min_relay_fee.
"""
weight = self.estimated_weight()
return self.virtual_size_from_weight(weight)
+7 -6
View File
@@ -2019,16 +2019,17 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# make sure we don't try to spend change from the tx-to-be-replaced:
coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
is_local = self.adb.get_tx_height(base_tx.txid()).height() == TX_HEIGHT_LOCAL
# estimate base tx fee before stripping tx for more accurate estimate
base_tx_fee = base_tx.get_fee()
base_feerate = Decimal(base_tx_fee)/base_tx.estimated_size()
relayfeerate = Decimal(self.relayfee()) / 1000
original_fee_estimator = fee_estimator
if not isinstance(base_tx, PartialTransaction):
base_tx = PartialTransaction.from_tx(base_tx)
base_tx.add_info_from_wallet(self)
else:
# don't cast PartialTransaction, because it removes make_witness
base_tx.remove_signatures()
base_tx_fee = base_tx.get_fee()
base_feerate = Decimal(base_tx_fee)/base_tx.estimated_size()
relayfeerate = Decimal(self.relayfee()) / 1000
original_fee_estimator = fee_estimator
def fee_estimator(size: Union[int, float, Decimal]) -> int:
size = Decimal(size)
lower_bound_relayfee = int(base_tx_fee + round(size * relayfeerate)) if not is_local else 0
@@ -2302,6 +2303,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
Without that, all txins must be ismine.
"""
assert tx
old_tx_size = tx.estimated_size() # estimate before stripping tx for more accurate estimate
if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx)
assert isinstance(tx, PartialTransaction)
@@ -2312,7 +2314,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
tx.add_info_from_wallet(self)
if tx.is_missing_info_from_network():
raise Exception("tx missing info from network")
old_tx_size = tx.estimated_size()
old_fee = tx.get_fee()
assert old_fee is not None
old_fee_rate = old_fee / old_tx_size # sat/vbyte
@@ -2572,6 +2573,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
Without that, all txins must be ismine.
"""
assert tx
old_tx_size = tx.estimated_size() # estimate before stripping tx for more accurate estimate
if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx)
assert isinstance(tx, PartialTransaction)
@@ -2583,7 +2585,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
tx.add_info_from_wallet(self)
if tx.is_missing_info_from_network():
raise Exception("tx missing info from network")
old_tx_size = tx.estimated_size()
old_fee = tx.get_fee()
assert old_fee is not None
old_fee_rate = old_fee / old_tx_size # sat/vbyte
+132 -2
View File
@@ -14,8 +14,9 @@ from electrum import util
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE
from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet,
Abstract_Wallet, CannotBumpFee, BumpFeeStrategy,
TransactionPotentiallyDangerousException, TransactionDangerousException,
TxSighashRiskLevel)
TransactionPotentiallyDangerousException,
TransactionDangerousException,
TxSighashRiskLevel, CannotDoubleSpendTx)
from electrum.util import bfh, NotEnoughFunds, UnrelatedTransactionException, UserFacingException, TxMinedInfo
from electrum.fee_policy import FixedFeePolicy
from electrum.transaction import Transaction, PartialTxOutput, tx_from_any, Sighash
@@ -1262,6 +1263,11 @@ class TestWalletSending(ElectrumTestCase):
await self._rbf_batching(
simulate_moving_txs=simulate_moving_txs,
config=config)
with TmpConfig() as config:
with self.subTest(msg="_rbf_sufficient_fee_increase_adding_outputs_to_base_tx", simulate_moving_txs=simulate_moving_txs):
await self._rbf_sufficient_fee_increase_adding_outputs_to_base_tx(
simulate_moving_txs=simulate_moving_txs,
config=config)
with TmpConfig() as config:
with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all", simulate_moving_txs=simulate_moving_txs):
await self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(
@@ -1288,6 +1294,9 @@ class TestWalletSending(ElectrumTestCase):
with TmpConfig() as config:
with self.subTest(msg="_bump_fee_p2wpkh_csv", simulate_moving_txs=simulate_moving_txs):
await self._bump_fee_p2wpkh_csv(config=config)
with TmpConfig() as config:
with self.subTest(msg="_bump_fee_sufficient_fee_increase", simulate_moving_txs=simulate_moving_txs):
await self._bump_fee_sufficient_fee_increase(config=config, simulate_moving_txs=simulate_moving_txs)
async def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',
@@ -1543,6 +1552,35 @@ class TestWalletSending(ElectrumTestCase):
self.assertEqual(tx.get_block_based_relative_locktime(), 1)
self.assertEqual('9f1842ea9c4d7cf88ac58d55d1b73e6ad7d34693a046d428887ead2c22865483', tx.txid())
async def _bump_fee_sufficient_fee_increase(self, *, config, simulate_moving_txs):
"""
Test that wallet.bump_fee raises if the replacement transaction has an equal feerate than the
transaction it tries to replace.
"""
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config)
# create tx
tx_to_bump_raw = '70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000'
tx_to_bump = tx_from_any(tx_to_bump_raw)
if simulate_moving_txs:
partial_tx = tx_to_bump.serialize_as_bytes().hex()
self.assertEqual(tx_to_bump_raw,
partial_tx)
tx_to_bump = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
tx_to_bump = wallet.sign_transaction(tx_to_bump, password=None)
wallet.adb.receive_tx_callback(tx_to_bump, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertTrue(tx_to_bump.is_complete())
self.assertTrue(tx_to_bump.is_segwit())
self.assertEqual(2, len(tx_to_bump.inputs()))
self.assertEqual(3, len(tx_to_bump.outputs()))
self.assertTrue(wallet.can_rbf_tx(tx_to_bump))
# cancel tx
tx_to_bump_feerate = tx_to_bump.get_fee() / tx_to_bump.estimated_size()
with self.assertRaises(CannotBumpFee):
wallet.bump_fee(tx=tx_to_bump, new_fee_rate=tx_to_bump_feerate)
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_cpfp_p2pkh(self, mock_save_db):
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean')
@@ -2063,6 +2101,64 @@ class TestWalletSending(ElectrumTestCase):
wallet.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertEqual((0, 3_900_000, 0), wallet.get_balance())
async def _rbf_sufficient_fee_increase_adding_outputs_to_base_tx(self, *, simulate_moving_txs, config):
"""
Initial tx1: 2 inputs of our wallet -> 2 external p2wsh outputs + 1 change output
-> let make_unsigned_tx add one external output and another input -> tx2
Compare fee of tx1 and tx2, is the fee increase sufficient to satisfy relay policy?
"""
wallet: Abstract_Wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config, gap_limit=3)
# bootstrap wallet, creates three utxos for wallet
funding_tx = Transaction('020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900')
funding_txid = funding_tx.txid()
self.assertEqual('615080b19e3bd1da18bcdbf74a9bcc94fe4f2348e1115a9028e5667d2c1c2b03', funding_txid)
wallet.adb.receive_tx_callback(funding_tx, tx_height=TX_HEIGHT_UNCONFIRMED)
tx1 = tx_from_any('70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000')
if simulate_moving_txs:
partial_tx = tx1.serialize_as_bytes().hex()
self.assertEqual('70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000',
partial_tx)
tx1 = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
wallet.sign_transaction(tx1, password=None)
wallet.adb.receive_tx_callback(tx1, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertTrue(tx1.is_complete())
self.assertTrue(tx1.is_segwit())
self.assertEqual(2, len(tx1.inputs()))
self.assertEqual(3, len(tx1.outputs()))
tx2_additional_output = [
PartialTxOutput.from_address_and_value("tb1qnp9z33k8dz22dfklvyusv95hp7n3gc0yamzp6mj9y9es636ekrxs5mjmzp", 510_000)
]
tx2 = wallet.make_unsigned_transaction(
base_tx=tx1,
outputs=tx2_additional_output,
fee_policy=FixedFeePolicy(1_000), # lower fee
BIP69_sort=False,
)
if simulate_moving_txs:
partial_tx = tx2.serialize_as_bytes().hex()
self.assertEqual('70736274ff0100fd25010200000003032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610200000000fdffffff0478d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b830c8070000000000220020984a28c6c76894a6a6df61390616970fa71461e4eec41d6e4521730d4759b0cd8d3a070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a903980000008000000000010000000001011f20a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d249002206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c510e8a9039800000080000000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000',
partial_tx)
tx2 = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
wallet.sign_transaction(tx2, password=None)
self.assertTrue(tx2.is_complete())
self.assertTrue(tx2.is_segwit())
self.assertEqual(3, len(tx2.inputs()))
self.assertEqual(4, len(tx2.outputs()))
relayfee_sat_vb = bitcoin.relayfee() / 1000 # assume relayfee is equal to incrementalrelayfee
minimal_accepted_new_fee = int(tx1.get_fee() + tx2.estimated_size() * relayfee_sat_vb)
self.assertGreaterEqual(tx2.get_fee(), minimal_accepted_new_fee)
tx1_feerate_sat_vb = tx1.get_fee() / tx1.estimated_size()
tx2_feerate_sat_vb = tx2.get_fee() / tx2.estimated_size()
self.assertGreater(tx2_feerate_sat_vb, tx1_feerate_sat_vb, msg="tx2 feerate lower than tx1")
def _create_cause_carbon_wallet(self):
data = read_test_vector('cause_carbon_wallet.json')
ks = keystore.from_seed(data['seed'], passphrase='', for_multisig=False)
@@ -2621,6 +2717,10 @@ class TestWalletSending(ElectrumTestCase):
await self._dscancel_when_not_all_inputs_are_ismine(
simulate_moving_txs=simulate_moving_txs,
config=config)
with self.subTest(msg="_dscancel_sufficient_fee_increase", simulate_moving_txs=simulate_moving_txs):
await self._dscancel_sufficient_fee_increase(
simulate_moving_txs=simulate_moving_txs,
config=config)
async def _dscancel_when_all_outputs_are_ismine(self, *, simulate_moving_txs, config):
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',
@@ -2854,6 +2954,36 @@ class TestWalletSending(ElectrumTestCase):
str(tx_copy))
self.assertEqual('3021a4fe24e33af9d0ccdf25c478387c97df671fe1fd8b4db0de4255b3a348c5', tx_copy.txid())
async def _dscancel_sufficient_fee_increase(self, *, simulate_moving_txs, config):
"""
Tries to cancel a tx with a replacement tx of the same feerate as the original tx. This shouldn't
work as the feerate needs to be higher.
"""
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage',
config=config)
# create tx
tx_to_cancel_raw = '70736274ff0100d10200000002032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610000000000fdffffff032b1c2c7d66e528905a11e148234ffe94cc9b4af7dbbc18dad13b9eb18050610100000000fdffffff0378d4030000000000220020f381d0d2c633cdd890015bb438c8e73e9960b21defa126c594e5cf67bd06419f78d40300000000002200204a1c25db1aa7165cb655638fb32319a945262fee7c8ce6f2f17f1223679bb0b84072070000000000160014f0fe5c1867a174a12e70165e728a072619455ed5000000000001011f20a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f80100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220603565a3904c7d2d6a6c2cf3fcdf89d9e5c60b509483104992cfdf85b196665170c10e8a903980000008000000000020000000001011f20a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900100fd1c01020000000001012cdd7dfc38d14f2c95425bb0afc4ee93df4c7b46e9f8bd8d43d845382f88b2b60100000000fdffffff0420a10700000000001600141ab4b6d2f79cb0a1b5be5a2372eb668bc09261f820a107000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9020a1070000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b7303b0c0100000000160014da837c758fa0bce9eb845f240a06484f8dfb862c0247304402201b47c7fe41a9b5b196f1ee628d8e5065d85cba4caa44a0f8199a603cb0ee40a80220159eb66a66a5a3a396922aa3ffd7101224de75b1b57dc0c4732e284cdd228c63012102d63196184adaa312705ec7f0d8c9565261e4c19a6746f5ac3ad30cc0b932501cc7d24900220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f10e8a90398000000800000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea10e8a9039800000080010000000000000000'
tx_to_cancel = tx_from_any(tx_to_cancel_raw)
if simulate_moving_txs:
partial_tx = tx_to_cancel.serialize_as_bytes().hex()
self.assertEqual(tx_to_cancel_raw,
partial_tx)
tx_to_cancel = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
tx_to_cancel = wallet.sign_transaction(tx_to_cancel, password=None)
wallet.adb.receive_tx_callback(tx_to_cancel, tx_height=TX_HEIGHT_UNCONFIRMED)
self.assertTrue(tx_to_cancel.is_complete())
self.assertTrue(tx_to_cancel.is_segwit())
self.assertEqual(2, len(tx_to_cancel.inputs()))
self.assertEqual(3, len(tx_to_cancel.outputs()))
# cancel tx
tx_details = wallet.get_tx_info(tx_to_cancel)
self.assertTrue(tx_details.can_dscancel)
tx_to_cancel_feerate = tx_to_cancel.get_fee() / tx_to_cancel.estimated_size()
with self.assertRaises(CannotDoubleSpendTx):
wallet.dscancel(tx=tx_to_cancel, new_fee_rate=tx_to_cancel_feerate)
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_wallet_history_chain_of_unsigned_transactions(self, mock_save_db):
wallet = self.create_standard_wallet_from_seed('cross end slow expose giraffe fuel track awake turtle capital ranch pulp',