Hi,
I’ve been thinking about some improvements to transaction relay and wanted some feedback about the design goals.
Motivation
A transaction is only accepted to our mempool and then relayed to our peers if (a) our mempool already has all unconfirmed dependencies of the transaction, and (b) the transaction itself passes a bunch of policy checks (feerate, package size limits, etc).
It is possible for one transaction to fail our feerate policy check, but had it been bundled with another child transaction, that the transactions together should make it in to our mempool (eg because the child’s feerate is sufficiently high). Here are some scenarios where this can come up:
-
A node on the network has a smaller mempool than the default/typical setting. Our default mempool size is 300MB (huge); it seems like it should be the case that someone running a node with, say, a 20MB mempool ought to have all the transactions that will be appearing in the next block. However, nodes running with smaller mempools can have a higher effective minimum feerate required for transaction acceptance (this is a consequence of our mempool limiting/eviction policy), and so high feerate children of a low feerate parent – which might appear in the next block – may never make it in to the mempool of such a node.
-
Matt Corallo recently wrote about an example on the bitcoin-dev mailing list involving lightning transactions, where pre-signed transactions might be broadcast to the blockchain long after they were generated, and thus not have been created with a fee that is sufficient to be confirmed quickly (or even be accepted to node mempools). In such situations, channel participants may need to use chained transactions (CPFP) in order to increase the confirmation speed of such transactions, and that implies we may need to introduce a mechanism for those parent transactions to be relayed along with their higher feerate children, even if the parent transaction would be rejected by itself.
Are there other scenarios that suffer without a package relay solution, which have unique design requirements?
Design questions
Who is responsible for initiating package relay, sender or recipient?
While the sender of a transaction that is chained off a low-fee parent is well-positioned to guess that a transaction might need package relay in order to propagate, expecting relayers to do the same seems onerous and potentially bandwidth wasteful. For example, one way for a sender-initiated package relay system to work might be for a sender to announce via an INV (or similar) all ancestor txid’s along with a child txid. However, what would the protocol look like beyond that first hop – would each node on the network initiate package relay with their peers as well? It seems like this could be used in a way that would cause the same txid’s to be announced repeatedly on the same links in the network, which seems wasteful. Is there a sender-initiated strategy that works better than this naive idea that we should consider?
On the other hand, the recipient of a transaction can tell if a transaction’s parents are missing and can therefore know to initiate package relay from the sending peer (which means that package relay would only occur on links that require it, eg to/from low-memory-mempool nodes).
Should we update the p2p protocol to accommodate package relay, or rely on existing functionality?
In theory, we may be able to shoehorn a recipient-initiated package relay scheme within the existing p2p protocol. A recipient who needs the parents of a transaction could iteratively request them, and then topologically sort the result once all ancestors have arrived, and then proceed with validation.
But in practice, it seems like it might be much simpler to reason about the code and computational complexity if we add some kind of special p2p messages to assist in the process, so that rather than iteratively request parents (for example) we could just ask a peer for all unconfirmed ancestors of a transaction, perhaps even already sorted in topological order. It’s also possible that it’d be helpful to add information or requirements around the package feerate of such packages.
The obvious benefit to not updating the p2p protocol is that we could implement package relay in a new release and the whole network would support providing packages to new software. The downside is potential code complexity and possibly somewhat less efficient processing of transactions due to repeatedly having to check for missing ancestors and doing the topological sort (but perhaps those could be mitigated with the right implementation).
What Denial-of-Service concerns do we need to address?
In addition to the usual things we worry about (bandwidth attacks on the network, CPU or other resource exhaustion attacks), does package relay need to incorporate further anti-DoS measures to be useful?
For example, suppose there is some low feerate transaction A, and it has multiple children: B, C, D, E … One of our peers relays transaction B to us, so we ask for A, but decide that A+B is not good enough for our mempool. How many times will we be willing to re-try A, if C, D, E, etc are also relayed to us?
One approach might be to only attempt the same transaction once over some time period (say by adding it to our reject filter, which gets cleared out every block), so that the same transaction cannot be repeatedly used to CPU-attack us; however, this would seem to conflict with the lightning use-case, where we would not want an adversary to be able to prevent relay of some parent transaction.
Moreover, any situation where we might test a transaction’s signatures prior to a (package-) policy failure that prevents the transaction from being ultimately accepted to the mempool would open the door to CPU-exhaustion attacks. This would imply a design for package processing and acceptance where policy checks on the whole package are performed before signature validation – then, if a signature is found to be invalid, we could ban the peer. Are there any other practical solutions for this? If we take this approach, then it seems we could re-try A over and over with limited downside.
It’s important to note the existing resource usage that transaction relay can already incur – when we receive a new transaction, we generally have to retrieve the inputs from the utxo set, which typically means that an attacker can make us do disk reads costlessly. Given that this seems difficult to avoid, I don’t think package relay needs to limit an attacker’s ability to have us look up inputs. I think this implies that in the previous example, being willing to retry A+C, A+D, etc and looking up A’s inputs over and over shouldn’t be considered an increase in attack surface.
Consistency requirements?
Are there any additional consistency requirements we should (or must) impose on package relay? Currently it is the case that the order that transactions are received by a node does determine which transactions make it into the mempool (some examples that come to mind: transactions that are just at the mempool min fee rate; transactions that depend on common ancestors which are near the descendant chain limit; a transaction that would conflict with another vs descendants of that original transaction; I’m sure there are more examples).
Naively, we might also expect it to be the case that depending on how a package is relayed, it might affect whether a child transaction is accepted. For instance, if we have a parent transaction A, with two children B and C, then it’s possible that B+A has a high enough feerate to be accepted via package relay, but C+A does not – so relaying B first would get all 3 transactions in, while relaying C first would only get A and B in.
This effect could be magnified if there were (for example) anti-DoS rules that would prevent A from being attempted too many times.
Would this kind of behavior significantly harm any use cases that we’re trying to support?