Skip to content

Conversation

darosior
Copy link
Member

@darosior darosior commented Sep 23, 2021

The block policy estimator currently assumes that a miner is incentivized to include a transaction only by its own fee. That is if a tracked transaction A with a feerate of 1sat/vb is confirmed because it was CPFP'd by a child transaction B with a feerate of 20sat/vb it will happily fill its 1ksat/kvb bucket with a success confirmation. Note that B isn't tracked as it has in-mempool ancestors.

This is concerning as it would not only miss part of the data necessary to provide a correct (higher) estimate (as it's the case for RBF, cf #22539) but it would wrongly assume an incorrect (lower) estimate was right. It is even more concerning if we take into account that estimating a too low fee could have security implications for protocols requiring timely confirmation of transactions.

Of course, the estimation would only get worse as CPFP is increasingly used on the network. But it is actively used currently [0] on the network, many applications allow for this functionality directly [1] or indirectly [2]. And with the increasing usage of L2s such as the Lightning Network, we can expect the CPFP usage to increase even more as fee bumping methods get more common. Interestingly, with the current estimator the more anchor output-based fee bumping (shifting the fees from the parent to a child transaction) are used on the network the less the estimation will be reliable for everyone.

This PR proposes a naive implementation for accounting for child transaction(s) feerates when a parent transaction gets confirmed in CBlockPolicyEstimator's processBlock. It would fail to split the feerate if multiple parents share the same child and assign the whole package (but the other parent(s)) feerate to the first seen parent. Accurate tracking would introduce way more complexity.

[0] TODO: Provide some raw data about historical usage of CPFP

[1] For instance Bluewallet, LND, Mycelium and SparrowWallet do. And Lightning implems recommend to do a CPFP if the funding transaction doesn't confirm in a timely manner until we have an upgrade of the funding protocol allow for RBF.

[2] For lots of wallets include the Bitcoin Core one during a fee spike users continue to transact using unconfirmed UTxOs with transactions of increasing feerate (and even sometimes open an issue because they hit the descendants limit).

@darosior
Copy link
Member Author

CI failure seems completely unrelated (feature_asmap failure on a Addrman log line figuring 3 instead of 2). Here is the log trace, "just in case".

Random `feature_asmap` failure logs
2021-09-23T14:53:38.217000Z TestFramework (INFO): Initializing test directory /tmp/cirrus-ci-build/ci/scratch/test_runner/test_runner_₿_🏃_20210923_145026/feature_asmap_26
2021-09-23T14:53:39.000000Z TestFramework (INFO): Test bitcoind with no -asmap arg passed
2021-09-23T14:53:39.442000Z TestFramework (INFO): Test bitcoind -asmap=<absolute path>
2021-09-23T14:53:40.286000Z TestFramework (INFO): Test bitcoind -asmap=<relative path>
2021-09-23T14:53:40.859000Z TestFramework (INFO): Test bitcoind -asmap (using default map file)
2021-09-23T14:53:41.404000Z TestFramework (INFO): Test bitcoind -asmap= (using default map file)
2021-09-23T14:53:42.007000Z TestFramework (INFO): Test bitcoind -asmap restart with addrman containing new and tried entries
2021-09-23T14:55:03.894000Z TestFramework (ERROR): Assertion failed
Traceback (most recent call last):
  File "/tmp/cirrus-ci-build/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/test_framework/test_framework.py", line 131, in main
    self.run_test()
  File "/tmp/cirrus-ci-build/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/feature_asmap.py", line 124, in run_test
    self.test_asmap_interaction_with_addrman_containing_entries()
  File "/tmp/cirrus-ci-build/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/feature_asmap.py", line 96, in test_asmap_interaction_with_addrman_containing_entries
    self.node.getnodeaddresses()  # getnodeaddresses re-runs the addrman checks
  File "/usr/lib/python3.6/contextlib.py", line 88, in __exit__
    next(self.gen)
  File "/tmp/cirrus-ci-build/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/test_framework/test_node.py", line 400, in assert_debug_log
    self._raise_assertion_error('Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(str(expected_msgs), print_log))
  File "/tmp/cirrus-ci-build/ci/scratch/build/bitcoin-x86_64-pc-linux-gnu/test/functional/test_framework/test_node.py", line 166, in _raise_assertion_error
    raise AssertionError(self._node_msg(msg))
AssertionError: [node 0] Expected messages "['Addrman checks started: new 2, tried 2, total 4', 'Addrman checks completed successfully']" does not partially match log:
 - 2021-09-23T14:53:43.886617Z [http] [httpserver.cpp:237] [http_request_cb] Received a POST request for / from 127.0.0.1:52652
 - 2021-09-23T14:53:43.886733Z [httpworker.3] [rpc/request.cpp:174] [parse] ThreadRPCServer method=getnodeaddresses user=__cookie__
 - 2021-09-23T14:53:43.886809Z [httpworker.3] [addrman.cpp:770] [ForceCheckAddrman] Addrman checks started: new 3, tried 1, total 4
 - 2021-09-23T14:53:43.886870Z [httpworker.3] [addrman.cpp:48] [GetTriedBucket] IP 101.0.0.0 mapped to AS1 belongs to tried bucket 113
 - 2021-09-23T14:53:43.886980Z [httpworker.3] [addrman.cpp:850] [ForceCheckAddrman] Addrman checks completed successfully
 - 2021-09-23T14:53:43.886991Z [httpworker.3] [addrman.cpp:770] [ForceCheckAddrman] Addrman checks started: new 3, tried 1, total 4
 - 2021-09-23T14:53:43.887016Z [httpworker.3] [addrman.cpp:48] [GetTriedBucket] IP 101.0.0.0 mapped to AS1 belongs to tried bucket 113
 - 2021-09-23T14:53:43.887102Z [httpworker.3] [addrman.cpp:850] [ForceCheckAddrman] Addrman checks completed successfully
 - 2021-09-23T14:54:28.662861Z [scheduler] [net.h:865] [StartExtraBlockRelayPeers] net: enabling extra block-relay-only peers
 - 2021-09-23T14:54:43.321822Z [scheduler] [random.cpp:523] [SeedPeriodic] Feeding 33048 bytes of dynamic environment data into RNG

@DrahtBot
Copy link
Contributor

DrahtBot commented Sep 23, 2021

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #23075 (refactoring: Fee estimation functional test cleanups by darosior)
  • #22919 (fees: skip pointless fee parameter calculation during IBD by RohitRanjangit)
  • #22722 (rpc: update estimatesmartfee to return max of CBlockPolicyEstimator::estimateSmartFee, mempoolMinFee and minRelayTxFee by pranabp-bit)
  • #22539 (Re-include RBF replacement txs in fee estimation by darosior)
  • #22014 (refactor: Make m_cs_fee_estimator non-recursive by hebasto)
  • #17786 (refactor: Nuke policy/fees->mempool circular dependencies by hebasto)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@instagibbs
Copy link
Member

concept ACK

@darosior
Copy link
Member Author

Removed the commit asserting blocksToConfirm > 0, it would cause more issues with fuzzing targets than i want to deal with in this PR :)

@glozow
Copy link
Member

glozow commented Sep 24, 2021

Concept ACK, planning to review but as I have very little background on the fee estimator it may take me a bit of time :)

@darosior
Copy link
Member Author

Unrelated CI error: Windows socket error (the use of UNIX-only os.killpg was just a symptom, not the cause)

2021-09-24T10:29:15.111000Z TestFramework (ERROR): Unexpected exception caught during testing 
                                   Traceback (most recent call last):
                                     File "C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\test\functional\test_framework\test_framework.py", line 131, in main
                                       self.run_test()
                                     File "C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\test\functional\feature_taproot.py", line 1465, in run_test
                                       self.test_spenders(self.nodes[1], spenders_taproot_active(), input_counts=[1, 2, 2, 2, 2, 3])
                                     File "C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\test\functional\feature_taproot.py", line 1451, in test_spenders
                                       self.block_submit(node, [tx], msg, witness=True, accept=fail_input is None, cb_pubkey=cb_pubkey, fees=fee, sigops_weight=sigops_weight, err_msg=expected_fail_msg)
                                     File "C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\test\functional\feature_taproot.py", line 1244, in block_submit
                                       block_response = node.submitblock(block.serialize().hex())
                                     File "C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\test\functional\test_framework\coverage.py", line 49, in __call__
                                       return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
                                     File "C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\test\functional\test_framework\authproxy.py", line 144, in __call__
                                       response, status = self._request('POST', self.__url.path, postdata.encode('utf-8'))
                                     File "C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\test\functional\test_framework\authproxy.py", line 107, in _request
                                       self.__conn.request(method, path, postdata, headers)
                                     File "C:\Python39\lib\http\client.py", line 1257, in request
                                       self._send_request(method, url, body, headers, encode_chunked)
                                     File "C:\Python39\lib\http\client.py", line 1303, in _send_request
                                       self.endheaders(body, encode_chunked=encode_chunked)
                                     File "C:\Python39\lib\http\client.py", line 1252, in endheaders
                                       self._send_output(message_body, encode_chunked=encode_chunked)
                                     File "C:\Python39\lib\http\client.py", line 1012, in _send_output
                                       self.send(msg)
                                     File "C:\Python39\lib\http\client.py", line 952, in send
                                       self.connect()
                                     File "C:\Python39\lib\http\client.py", line 923, in connect
                                       self.sock = self._create_connection(
                                     File "C:\Python39\lib\socket.py", line 843, in create_connection
                                       raise err
                                     File "C:\Python39\lib\socket.py", line 831, in create_connection
                                       sock.connect(sa)
                                   OSError: [WinError 10048] Only one usage of each socket address (protocol/network address/port) is normally permitted

We'll need to check if the tx is tracked in the caller, processBlock.

Signed-off-by: Antoine Poinsot <darosior@protonmail.com>
Signed-off-by: Antoine Poinsot <darosior@protonmail.com>
… pointer

We'll need to lookup in the set whether a descendant of an entry was
actually confirmed.

Signed-off-by: Antoine Poinsot <darosior@protonmail.com>
The fee estimation logic would previously assume a miner was
incentivized to include a given transaction only by this transaction's
fee. This isn't true anymore since c82a4e9.

CPFP is actively used on the network today [FIXME provide numbers], and
the usage can be expected to increase a lot with the adoption of
CPFP-based fee bumping for L2 protocols such as the Lightning Network
(see the anchor outputs proposal).

The more CPFP is used, the more the fee estimator will *underestimate*
the fee required to get into a target number of blocks.
This may become at best a nuisance for onchain transaction users and at
worst a security issue for protocol requiring timely confirmation of
pre-sign transactions.

Signed-off-by: Antoine Poinsot <darosior@protonmail.com>
Let's not have run_test get into a giant function as we add more tests

Signed-off-by: Antoine Poinsot <darosior@protonmail.com>
Signed-off-by: Antoine Poinsot <darosior@protonmail.com>
@naumenkogs
Copy link
Member

Concept ACK. Seems like a well-justified improvement.


Do you think this PR could degrade the prediction performance in certain cases? For example, if someone RBFs their package with 1 tx. I actually think we get an improvement here in that case, but yeah, that's just an example.

@darosior
Copy link
Member Author

darosior commented Sep 27, 2021

I don't think this can degrade estimation in any case, it should be a strict improvement (/fix?).

For example, if someone RBFs their package with 1 tx.

For now the replacement transaction would not be considered. Assuming we have both #22539 and this PR this would give a much more correct estimation: taking the entire package feerate instead of ignoring it (#22539), or wrongly assuming the feerate of the parent transaction alone was enough to get it confirmed (this PR).

Copy link
Member

@glozow glozow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, our fee estimator is tracking feerate + time to confirm so that we can estimate the probability of a transaction confirming within n blocks when offered at feerate f. I definitely agree it's incorrect to use base feerates, as CPFP transactions cause us underestimate feerates. So, again, strong concept ACK.

However, I'm not sure about the approach. Looking at a block transaction and its descendants can tell us the feerate at which it was mined, but not the feerate it was offered at. We wouldn't know when it was fee-bumped and how long it took to mine after the high-fee descendant(s) were broadcast (which imo is what we're actually interested in).

In our code, we essentially think of "miner attractiveness" as the ancestor feerate of a transaction (not saying this is the perfect or only way to think about it, just going off of BlockAssembler).

I also don't think it's necessarily bad to include transactions in fee estimation when they have mempool ancestors, as long as everybody's feerates are accurately accounted for.

If I might propose a different approach: for every transaction added to mempool, add it to the fee estimator by ancestor feerate (EDIT: update for the mining score of any transaction each time it changes), and update height and feerate of its ancestors' entries if it improves them. This captures our intent: the transaction is being re-offered at a higher feerate. (Same thing for RBF in #22539, which is already implemented this way - new feerate and new time).

When the transaction is included in a block, we can use blocksToConfirm = nBlockHeight - offerHeight where offerHeight is not the time it first entered the mempool, but the time it was "offered" to a miner at its best feerate.

// How many blocks did it take for miners to include this transaction?
// blocksToConfirm is 1-based, so a transaction included in the earliest
// possible block has confirmation count of 1
int blocksToConfirm = nBlockHeight - entry->GetHeight();
int blocksToConfirm = nBlockHeight - entry.GetHeight();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach may overestimate the number of blocks it takes to a tx at a certain feerate to confirm: if a transaction is first accepted at time t with a feerate of 1sat/vB, sits in the mempool for a long time, and then is fee-bumped to 10sat/vB and included in a block at time t+500, this will record that the transaction had a feerate of 10sat/vB but took 500 blocks to confirm. That would be inaccurate, since it was 1sat/vB for most of that time.

@@ -10,6 +10,7 @@
#include <uint256.h>
#include <random.h>
#include <sync.h>
#include <txmempool.h>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the txmempool forward declarations below if including the header.

@darosior
Copy link
Member Author

However, I'm not sure about the approach. Looking at a block transaction and its descendants can tell us the feerate at which it was mined, but not the feerate it was offered at. We wouldn't know when it was fee-bumped and how long it took to mine after the high-fee descendant(s) were broadcast (which imo is what we're actually interested in).

Right, this would be the accurate approach. I chose to go with the current hack because i found the "right approach" would make it really hard to think about edge cases. Now -and given the overestimation would not be fixed without further hacks- i can't give any good argument for not tracking by ancestor. Will give this approach a go, hopefully in the coming weeks!

@DrahtBot
Copy link
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft".

maflcko pushed a commit that referenced this pull request Jan 6, 2022
60ae116 qa: replace assert with test framework assertion helpers in fee estimation test (Antoine Poinsot)
e502139 qa: fee estimation with RBF test cleanups (Antoine Poinsot)
15f5fd6 qa: don't mine non standard txs in fee estimation test (Antoine Poinsot)
eae52dd qa: pass scriptsig directly to txins constructor in fee estimation test (Antoine Poinsot)
1fc0315 qa: split coins in a single tx in fee estimation test (Antoine Poinsot)
cc204b8 qa: use a single p2sh script in fee estimation test (Antoine Poinsot)
19dd91a qa: remove a redundant condition in fee estimation test (Antoine Poinsot)

Pull request description:

  Some cleanups that i noticed would be desirable while working on  #23074 and #22539, which are intentionally not based on it. Mainly simplifications and a slight speedup.

  - Use a single tx to create the `2**9` coins instead of creating `2**8` 2-outputs transactions
  - Use a single P2SH script
  - Avoid the use of non-standard transactions
  - Misc style nits (happy to take more)

ACKs for top commit:
  pg156:
    I ACK all commits up to 60ae116 (except 1fc0315, where I have a question more for my own learning than actually questioning the PR). I built and ran the test successfully. I agree after the changes, the behavior is kept the same and the code is shorter and easier to reason.
  glozow:
    utACK 60ae116

Tree-SHA512: 57ae2294eb68961ced30f32448c4a530ba1cdee17881594eecb97e1b9ba8927d58c25022b847eb07fb67d676bf436108c416c2f2174864d258fcca5b528b8bbd
@DrahtBot
Copy link
Contributor

There hasn't been much activity lately and the patch still needs rebase. What is the status here?
  • Is it still relevant? ➡️ Please solve the conflicts to make it ready for review and to ensure the CI passes.
  • Is it no longer relevant? ➡️ Please close.
  • Did the author lose interest or time to work on this? ➡️ Please close it and mark it 'Up for grabs' with the label, so that it can be picked up in the future.

@darosior
Copy link
Member Author

darosior commented Feb 22, 2022 via email

@darosior
Copy link
Member Author

Finally got back to give this some thought, and i'm going to close it for now. It sticked around for too long and unfortunately i don't expect to be able to invest the necessary time anytime soon. (It's more complicated than just tracking per ancestor feerate.)

I'm still happy to discuss different approaches if people are still interested. I still believe it's very much needed.

@darosior darosior closed this Jun 10, 2022
@darosior
Copy link
Member Author

One thing we could do at least is to ignore transactions with child. This would reduce our number of data points but better have less rather than wrong ones.

darosior added a commit to darosior/bitcoin that referenced this pull request Jun 15, 2022
The fee estimator would previously assume that a miner was only
incentivized by the fees of a transaction in isolation, completely
disregarding its descendants. For instance, upon confirmation of a
package {A, B} where A is 1sat/vb and B 20sat/vb, the fee estimator
would assume that 1sat/vb was sufficient for getting this transaction
confirmed.

This can lead to significantly underestimated fees as CPFP gets more
usage on the network. It is currently actively used by many wallets, and
will get more adoption along with L2s that rely on it for fee bumping
(for instance the Lightning Network with anchor outputs).

I previously attempted a boutique accounting of child feerates [0], but a
decent tracking of ancestor scores as new child come in and parts of
packages get confirmed will require significantly more work. In the
meantime just detect transactions that were likely confirmed because of
their descendants' feerate and don't take them into account for fee
estimation. It's not perfect but should prevent the mistake in most
cases (and for the very unlikely edge cases the current algorithm is pretty
resistent to outliers anyways).

[0] bitcoin#23074
darosior added a commit to darosior/bitcoin that referenced this pull request Jun 22, 2022
The fee estimator would previously assume that a miner was only
incentivized by the fees of a transaction in isolation, completely
disregarding its descendants. For instance, upon confirmation of a
package {A, B} where A is 1sat/vb and B 20sat/vb, the fee estimator
would assume that 1sat/vb was sufficient for getting this transaction
confirmed.

This can lead to significantly underestimated fees as CPFP gets more
usage on the network. It is currently actively used by many wallets, and
will get more adoption along with L2s that rely on it for fee bumping
(for instance the Lightning Network with anchor outputs).

I previously attempted a boutique accounting of child feerates [0], but a
decent tracking of ancestor scores as new child come in and parts of
packages get confirmed will require significantly more work. In the
meantime just detect transactions that were likely confirmed because of
their descendants' feerate and don't take them into account for fee
estimation. It's not perfect but should prevent the mistake in most
cases (and for the very unlikely edge cases the current algorithm is pretty
resistent to outliers anyways).

[0] bitcoin#23074
darosior added a commit to darosior/bitcoin that referenced this pull request Apr 3, 2023
The fee estimator would previously assume that a miner was only
incentivized by the fees of a transaction in isolation, completely
disregarding its descendants. For instance, upon confirmation of a
package {A, B} where A is 1sat/vb and B 20sat/vb, the fee estimator
would assume that 1sat/vb was sufficient for getting this transaction
confirmed.

This can lead to significantly underestimated fees as CPFP gets more
usage on the network. It is currently actively used by many wallets, and
will get more adoption along with L2s that rely on it for fee bumping
(for instance the Lightning Network with anchor outputs).

I previously attempted a boutique accounting of child feerates [0], but a
decent tracking of ancestor scores as new child come in and parts of
packages get confirmed will require significantly more work. In the
meantime just detect transactions that were likely confirmed because of
their descendants' feerate and don't take them into account for fee
estimation. It's not perfect but should prevent the mistake in most
cases (and for the very unlikely edge cases the current algorithm is pretty
resistent to outliers anyways).

[0] bitcoin#23074
@bitcoin bitcoin locked and limited conversation to collaborators Jun 10, 2023
@fanquake
Copy link
Member

Removing "Up for grabs" given #30079. cc @ismaelsadeeq.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants