diff --git a/electrum/transaction.py b/electrum/transaction.py index 816914fdb..82d11268e 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -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) diff --git a/electrum/wallet.py b/electrum/wallet.py index d7ae3000a..cb8c2db3d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -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 diff --git a/tests/test_wallet_vertical.py b/tests/test_wallet_vertical.py index 7ee10e5d7..ffe3490d6 100644 --- a/tests/test_wallet_vertical.py +++ b/tests/test_wallet_vertical.py @@ -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',