On Mon, Jan 22, 2024 at 01:12:45PM -0500, Murch via bitcoin-dev wrote: > Hi Peter, > > On 1/18/24 13:23, Peter Todd via bitcoin-dev wrote: > > Reposting this blog post here for discussion: > > > > https://petertodd.org/2024/one-shot-replace-by-fee-rate > > I saw your proposal mentioned on Stacker News and read it with interest. In > response, I described a replacement cycle that can be used to broadcast the > same five transactions repeatedly: > > https://stacker.news/items/393182 > > The gist is that by using two confirmed inputs and five transactions, you > can use RBFr to reduce the absolute fee while raising the feerate to top > block levels, then immediately use the current RBF rules to introduce a > high-feerate transaction that beats the RBFr transaction but is hampered by > a low-feerate parent and not attractive for mining, then use RBF to replace > its low-feerate parent, then use the RBFr transaction again to reduce the > absolute feerate. Due to the asymmetric replacements, the same transactions > can replace each other in that order in every cycle. Please refer to the > linked write-up for details, I’ve included weights, fees, and a transaction > graph to make my example comprehensible. > > Among those five transactions, the only transaction attractive for block > inclusion would be the small RBFr transaction with a > bottom-of-the-next-block feerate. Today, if it were mined it would amount to > fees of around 4000 sats every few blocks to make the entire network relay > transactions of more than 205,000 vB every few seconds. Given that my > example is minimal, it should be possible to further increase bandwidth > cost. > > Assuming that I did not make a mistake, i.e. all the replacements are viable > and my scenario is compatible with your proposal, the described One-Shot > Replace-By-Fee-Rate proposal would not be safe for deployment on the > network. I actually tried this attack out, and it fails at step #4 due to the Rule #6, PaysMoreThanConflicts, check. While on stacker.news you stated that: tx_HS has 5000 vB and pays 21 s/vB, but since it spends an output from a low-feerate parent, it’s mining score is only 1.95 s/vB. and You RBF tx_LL and tx_HS with tx_LM that has 100,000 vB and pays 3.05 s/vB (fee: 305,000 s) by spending the outputs C1 and C2. This is permitted, since only tx_LL is a direct conflict, so the feerate of tx_HS does not have to be beat directly. tx_HS _is_ considered to be a direct conflict, and its raw fee-rate _does_ have to be beat directly. While ts_HS does spend an unconfirmed output, it appears that the fee-rate PaysMoreThanConflicts uses to calculate if ts_HS can be beaten is ts_HS's raw fee-rate. So looks like your understanding was incorrect on these two points. FYI here is the actual test script I used to test this attack. You can run it using Bitcoin v26.0 with the -acceptnonstdtxn -mempoolfullrbf=1 command line arguments, with python-bitcoinlib v0.12.2 installed. #!/usr/bin/env python3 import bitcoin bitcoin.SelectParams('regtest') import bitcoin.rpc import sys from bitcoin.core import * from bitcoin.core.script import * from bitcoin.wallet import * proxy = bitcoin.rpc.Proxy() my_addr = proxy.getnewaddress().to_scriptPubKey() coins = proxy.listunspent(1) print(coins[0:2]) txo1 = coins[0]['outpoint'] txo1_amount = coins[0]['amount'] txo2 = coins[1]['outpoint'] txo2_amount = coins[1]['amount'] print(txo1) print(txo2) for i in range(0, 1): # Step 2 tx_ll = CTransaction( [CTxIn(txo1)], [CTxOut(txo1_amount - 100000, my_addr), CTxOut(0, CScript([OP_RETURN, b'x' * 90000]))]) r = proxy.signrawtransactionwithwallet(tx_ll) assert(r['complete']) tx_ll_signed = r['tx'] print('tx_ll = %s' % b2lx(proxy.sendrawtransaction(tx_ll_signed))) tx_ls = CTransaction( [CTxIn(COutPoint(tx_ll.GetTxid(), 0))], [CTxOut(txo1_amount - 100000 - 300, my_addr)]) r = proxy.signrawtransactionwithwallet(tx_ls) assert(r['complete']) tx_ls_signed = r['tx'] print('tx_ls = %s' % b2lx(proxy.sendrawtransaction(tx_ls_signed))) # Step 3 tx_hs = CTransaction( [CTxIn(COutPoint(tx_ll.GetTxid(), 0)), CTxIn(txo2)], [CTxOut((txo1_amount - 100000) + txo2_amount - 4000, my_addr)]) r = proxy.signrawtransactionwithwallet(tx_hs) assert(r['complete']) tx_hs_signed = r['tx'] print('tx_hs = %s ' % b2lx(proxy.sendrawtransaction(tx_hs_signed))) # Step 4 tx_lm = CTransaction( [CTxIn(txo1), CTxIn(txo2)], [CTxOut(txo1_amount + txo2_amount - 300000, my_addr), CTxOut(0, CScript([OP_RETURN, b'x' * 90000]))]) r = proxy.signrawtransactionwithwallet(tx_lm) assert(r['complete']) tx_lm_signed = r['tx'] print('tx_lm = %s' % b2lx(proxy.sendrawtransaction(tx_lm_signed)))