Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mempool: handle covenant conflicts during reorg and save TXs for later if possible #773

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

pinheadmz
Copy link
Member

This PR should improve UX and increase miner fee profits after reorgs and in some edge cases involving covenant conflicts.

There are some covenants that can not coexist in a block, for example UPDATE can only happen once per name per block. That also means that at any time in the mempool, there can only be one UPDATE per name. After a reorginzation when transactions get removed from blocks and re-enter the mempool, we need to track these conflicts carefully and make sure that "the next valid transaction" in the covenant chain is the only covenant for a given name in the mempool.

By storing the conflicting TXs outside the mempool in a new data structure, a miner can still earn fees LATER when the parent TXs confirm and the child TXs are once again valid.

@pinheadmz pinheadmz added this to the hsd 5.0.0 milestone Oct 8, 2022
@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 68.056% when pulling 0c7d0a8 on pinheadmz:handle-reorg-mempool into 6f11212 on handshake-org:master.

@pinheadmz pinheadmz modified the milestones: hsd 5.0.0, hsd 5.0.1 Nov 29, 2022
Copy link
Contributor

@nodech nodech left a comment

Choose a reason for hiding this comment

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

I think we can also add new check to the exists(hash) and has(hash) methods, to just skip this tx if it's already in the disconnected list.

// covenants prevent them from joining in a block or mempool.
// Will be valid again once the parent is re-confirmed.
this.disconnected =
new BufferMap(); // parent hash -> BufferSet[child hashes]
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Would it be better to have the mapping description above the definition, so we don't have this kinda weird lines?

    // Will be valid again once the parent is re-confirmed.

    // parent hash -> BufferSet[child hashes]
    this.disconnected = new BufferMap();
    // child tx hash -> TX (not MempoolEntry)
    this.children = new BufferMap();

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like children should be called else: disconnectedTXMap or something.


// This child has to be removed from the mempool
// but it will be valid again once its parent confirms.
this.disconnectSpenders(tx);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a case where this will do anything ?

If spender was in the mempool it should not have disconnectable spenders?

Copy link
Member Author

Choose a reason for hiding this comment

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

good point about this, I need to go back down this rabbit hole. it's possible i was clearing out ANY children of the disconnected tx, which would apply to like, if a child spent the change output of a name-linked tx

// We can hang on to those evicted child TXs until they are valid again.
if (this.contracts.hasNames(tx))
this.disconnectSpenders(tx);

Copy link
Contributor

Choose a reason for hiding this comment

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

In the catch of the following block, we need to try and reconnect those disconnected names. Otherwise they will get stuck in the disconnected until node is restarted, if unconfirmed transaction can't be inserted into the mempool.

if (missing)
throw new Error('spender is orphan.');
// Recurse only on success
await this.reconnectSpenders(tx);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should reconnect spenders here again.. For example, if we have update -> update -> update chain in there, we only need to move into a single layer only, not in depth. Otherwise we will encounter unique name issues?

'Could not reconnect spender %x: %s.',
tx.hash(), err.message);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should remove parent->[child hashes] from the disconnected list and the single layer of children from the children.

@@ -209,18 +219,27 @@ class Mempool extends EventEmitter {
const entry = this.getEntry(hash);

if (!entry) {
// Confirmed TX was not in mempool
// maybe clear other references and conflicts
this.removeOrphan(hash);
this.removeDoubleSpends(tx);
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to removeDoubleSpends:

There needs to be additional check when the transaction that we have not seen is removed by removeDoubleSpends. I think evictEntry is a good place to clean up disconnects, because evicting the original transaction that we are waiting to reconnect will leave them there w/o any way to clean up.

Same as removeDoubleSpends it could happen on covenant invalidation, e.g. unconfirmed disconnector transaction, that we are waiting for got invalidated in the mempool (e.g. expire). This also leads to evictEntry.

I believe all evictEntry callers are included here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another thing that we need to track is double spends to the .children transactions. Any of the children can get double spent in the block, but since they are not part of the normal transaction map and are not tracked via getSpent we will need to track them separately.


for (const hash of hashes) {
const tx = this.children.get(hash);
assert(tx);
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: this assert will no longer hold true if we track double spends for children.

@@ -193,13 +210,6 @@ class Mempool extends EventEmitter {
*/

async _addBlock(block, txs, view) {
if (this.map.size === 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Why remove this ?

Copy link
Member Author

Choose a reason for hiding this comment

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

See #319

I don't think there was any reason for this to be here besides early-bcoin-style laziness and maybe even an accidental copy-paste from _removeBlock()where it used to make more sense, to just avoid complicated mempool issues.

What the function does is resolve orphans whose missing parents were just confirmed. There is no reason to NOT do that just because the current mempool is empty.

@pinheadmz pinheadmz mentioned this pull request Jan 5, 2023
@nodech nodech modified the milestones: hsd 5.0.1, hsd 6.0.0 May 30, 2023
@nodech nodech removed this from the hsd 6.0.0 milestone Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants