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

Build up to +65% more profitable blocks #7640

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

Conversation

benaadams
Copy link
Member

@benaadams benaadams commented Oct 21, 2024

Current Geth block (with forced local production for PR on Nethermind)

image

0.0145 Eth Fees | 3.41 MGas | 67 txs

PR

0.024 Eth Fees | 4.81 MGas | 69 txs

Current Nethermind block (with forced local production for PR on Nethermind)

image

0.0124 Eth fees | 3.88 MGas | 75 txs

PR

0.014 Eth fees | 5.82 MGas | 94 txs

Note: Doesn't compete with builders as 50% of mainnet orderflow is private and local builder doesn't have access to include in block

Changes

  • For first 65% of slot time build on timer so as not to over build; but also to have a good block ready if requested
  • For remaining slot trigger build every time a new tx arrives that is above base fee and not already building block
  • Cancel optimistic building on GetPayload (if a block is already built) and ForkChoice/NewPayload
  • Sort mempool tx by priority fee of first transaction by sender; maintain nonce order for a sender's txs and priority fee otherwise (e.g. nonce 2 with priority fee 100 doesn't go ahead of nonce 1 with priority fee 50)
  • If one of txs by a sender is invalid, too low basefee or jumps nonce, don't consider any of following txs by same sender with higher nonces; while considering tx replacement (same nonce, higher priority replaces but doesn't blank following)
  • Improve logging when building

Types of changes

What types of changes does your code introduce?

  • Optimization

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes - changed current

Notes on testing

Should also be run on testnets where node is doing a portion of local building

Documentation

Requires documentation update

  • No

Requires explanation in Release Notes

  • Yes

As above

@benaadams benaadams changed the title Build more profitable blocks Build up to +65% more profitable blocks Oct 23, 2024
@benaadams benaadams marked this pull request as ready for review October 23, 2024 15:54
Copy link
Member

@LukaszRozmej LukaszRozmej left a comment

Choose a reason for hiding this comment

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

Extreamly complicated, potentially suboptimal, might have some unforseen issues. Not sure if works at all with parallel block building.

@@ -48,7 +50,7 @@ Block[] Process(

public interface IBlockTransactionsExecutor
{
TxReceipt[] ProcessTransactions(Block block, ProcessingOptions processingOptions, BlockReceiptsTracer receiptsTracer, IReleaseSpec spec);
TxReceipt[] ProcessTransactions(Block block, ProcessingOptions processingOptions, BlockReceiptsTracer receiptsTracer, IReleaseSpec spec, CancellationToken token);
Copy link
Member

Choose a reason for hiding this comment

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

Maybe to reduce boilerplate changes:

Suggested change
TxReceipt[] ProcessTransactions(Block block, ProcessingOptions processingOptions, BlockReceiptsTracer receiptsTracer, IReleaseSpec spec, CancellationToken token);
TxReceipt[] ProcessTransactions(Block block, ProcessingOptions processingOptions, BlockReceiptsTracer receiptsTracer, IReleaseSpec spec, CancellationToken token = default);

Comment on lines +42 to +44
if (token.IsCancellationRequested) return null;

return _processor.Process(block, options, tracer, token);
Copy link
Member

Choose a reason for hiding this comment

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

sounds like : ?

@@ -123,6 +118,7 @@ public enum TxAction
{
Add,
Skip,
Invalid,
Copy link
Member

Choose a reason for hiding this comment

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

please double check if we cover all the usages of results of this enum now

Copy link
Member

Choose a reason for hiding this comment

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

Looks like we potentially need change SimulateBridgeHelper

Comment on lines +45 to +48
catch
{
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain?

Comment on lines +283 to +288
chain.AddTransactions(BuildTransactions(chain, startingHead, TestItem.PrivateKeyC, TestItem.AddressA, 3, 10, out _, out _));
await Task.Delay(timePerSlot / 4);
chain.AddTransactions(BuildTransactions(chain, startingHead, TestItem.PrivateKeyC, TestItem.AddressA, 3, 10, out _, out _));
await Task.Delay(timePerSlot / 4);
chain.AddTransactions(BuildTransactions(chain, startingHead, TestItem.PrivateKeyC, TestItem.AddressA, 3, 10, out _, out _));
await Task.Delay(timePerSlot / 4);
Copy link
Member

Choose a reason for hiding this comment

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

Sounds like a loop, and maybe TestCases with different loop count?

@@ -207,15 +299,32 @@ private void CleanupOldPayloads(object? sender, EventArgs e)
bool currentBestBlockIsEmpty = blockContext.CurrentBestBlock?.Transactions.Length == 0;
if (currentBestBlockIsEmpty && !blockContext.ImprovementTask.IsCompleted)
{
await Task.WhenAny(blockContext.ImprovementTask, Task.Delay(GetPayloadWaitForFullBlockMillisecondsDelay));
await Task.WhenAny(blockContext.ImprovementTask, Task.Delay(GetPayloadWaitForFullBlockMillisecondsDelay, _tokenSource.Token));
Copy link
Member

Choose a reason for hiding this comment

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

Can throw now?

int checkedTransactions = 0;
int selectedTransactions = 0;
using ArrayPoolList<Transaction> selectedBlobTxs = new(_eip4844Config.GetMaxBlobsPerBlock());
using ArrayPoolList<Transaction> selectedTxs = new(transactionSnapshot.Length + blobTransactionSnapshot.Length);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
using ArrayPoolList<Transaction> selectedTxs = new(transactionSnapshot.Length + blobTransactionSnapshot.Length);
using ArrayPoolList<Transaction> selectedTxs = new(gasLimit/Transaction.BaseTxGasCost + _eip4844Config.GetMaxBlobsPerBlock());

.Where(tx =>
tx.SenderAddress is not null && // Ensure the transaction has a sender address
_txFilterPipeline.Execute(tx, parent) && // Pass the transaction through the filter pipeline
!tx.IsAboveInitCode(spec)) // Exclude transactions that exceed the init code size limit
Copy link
Member

Choose a reason for hiding this comment

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

why is it needed here?

tx.SenderAddress is not null && // Ensure the transaction has a sender address
_txFilterPipeline.Execute(tx, parent) && // Pass the transaction through the filter pipeline
!tx.IsAboveInitCode(spec)) // Exclude transactions that exceed the init code size limit
.DistinctBy((tx) => tx, ByHashTxComparer.Instance)); // Remove duplicate transactions based on their hash
Copy link
Member

Choose a reason for hiding this comment

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

why would there be duplicates in tx pool?

Comment on lines +102 to +127
// Sort transactions from the same sender by their nonce in ascending order
// Otherwise txs will be invalidated by not being in nonce order
SortMultiSendersByNonce(selectedTxs, parent, spec, token);
// Remove any transactions that have been invalidated or are no longer valid after sorting
RemoveBlankedTransactions(selectedTxs);

if (_logger.IsTrace) _logger.Trace($"Selected {tx.ToShortString()} to be potentially included in block.");
// Re-sort transactions to maintain the correct order, ensuring transactions from the same sender remain in nonce order
// As sorting by nonce may have put txs with lower priorty earlier
selectedTxs.AsSpan().Sort((tx0, tx1) =>
tx0.SenderAddress == tx1.SenderAddress ?
0 : // If transactions are from the same sender, keep their current order
comparer.Compare(tx0, tx1)); // Otherwise, sort based on the main comparer

selectedTransactions++;
yield return tx;
}
// Remove blob transactions that cannot be included in the block (e.g. due to block limit constraints)
RemoveBlobTransactions(selectedTxs.AsSpan(), parent, spec);
// Remove any additional invalidated transactions after blob transaction removal
RemoveBlankedTransactions(selectedTxs);

if (selectedBlobTxs.Count > 0)
// Convert the list of selected transactions to an array for inclusion in the block
Transaction[] potentialTxs = selectedTxs.ToArray();

if (_logger.IsDebug) _logger.Debug($"Potentially selected {potentialTxs.Length} out of {checkedTransactions} pending transactions checked.");

// Return the selected transactions unless the operation was cancelled
return token.IsCancellationRequested ? Array.Empty<Transaction>() : potentialTxs;
}
Copy link
Member

Choose a reason for hiding this comment

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

Really dislike the solution:

  1. Sort things multiple times
  2. Allocating memory while previous solution was lazy.
  3. Prioritizing GasPrice sorting over Nonce sorting first.

GetOrderedTransactions where close to optimal, lazy and more readable solution and I would prefer to keep it.

@marcindsobczak
Copy link
Contributor

We have a gasBottleneck, which is 0 for non-ready txs and positive for ready ones. When you want to build an optimal block, you can just sort txs by gas bottleneck (it is actually default sorting) and just pick one by one up to the block gas limit (but only positive values)

@marcindsobczak
Copy link
Contributor

From PR description:
Sort mempool tx by priority fee of first transaction by sender; maintain nonce order for a sender's txs and priority fee otherwise (e.g. nonce 2 with priority fee 100 doesn't go ahead of nonce 1 with priority fee 50)
It is already managed by gas bottleneck, you can just reuse it.
To be clear - if sender has tx with nonce N and price 10 gwei and tx with nonce N+1 and price 100 gwei, gas bottleneck of both is equal 10 gwei and both will be included before e.g. tx from other sender, with nonce first to be executed, but with price 5 gwei

@LukaszRozmej
Copy link
Member

From PR description: Sort mempool tx by priority fee of first transaction by sender; maintain nonce order for a sender's txs and priority fee otherwise (e.g. nonce 2 with priority fee 100 doesn't go ahead of nonce 1 with priority fee 50) It is already managed by gas bottleneck, you can just reuse it. To be clear - if sender has tx with nonce N and price 10 gwei and tx with nonce N+1 and price 100 gwei, gas bottleneck of both is equal 10 gwei and both will be included before e.g. tx from other sender, with nonce first to be executed, but with price 5 gwei

I think this won't build optimal blocks, so for example it is better to include transactions with priority fee 10 and next 100, than the one from other sender with fee 20, but that 2nd one will win on gasBottleneck

@marcindsobczak
Copy link
Contributor

From PR description: Sort mempool tx by priority fee of first transaction by sender; maintain nonce order for a sender's txs and priority fee otherwise (e.g. nonce 2 with priority fee 100 doesn't go ahead of nonce 1 with priority fee 50) It is already managed by gas bottleneck, you can just reuse it. To be clear - if sender has tx with nonce N and price 10 gwei and tx with nonce N+1 and price 100 gwei, gas bottleneck of both is equal 10 gwei and both will be included before e.g. tx from other sender, with nonce first to be executed, but with price 5 gwei

I think this won't build optimal blocks, so for example it is better to include transactions with priority fee 10 and next 100, than the one from other sender with fee 20, but that 2nd one will win on gasBottleneck

Right, as long as all of them are able to pay for base fee. In this example - tx with fee 10 must be able to pay base fee by itself to make it possible to process tx with fee 100

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.

3 participants