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:
+10
-4
@@ -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
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user