Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Neo.CLI/config.fs.mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"MaxTraceableBlocks": 2102400,
"InitialGasDistribution": 5200000000000000,
"ValidatorsCount": 7,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512,
"MaxTransactionsPerSender": 100
},
"Hardforks": {
"HF_Aspidochelone": 3000000,
"HF_Basilisk": 4500000,
Expand Down
5 changes: 5 additions & 0 deletions src/Neo.CLI/config.fs.testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"MaxTraceableBlocks": 2102400,
"InitialGasDistribution": 5200000000000000,
"ValidatorsCount": 7,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512,
"MaxTransactionsPerSender": 100
},
"StandbyCommittee": [
"02082828ec6efc92e5e7790da851be72d2091a961c1ac9a1772acbf181ac56b831",
"02b2bcf7e09c0237ab6ef21808e6f7546329823bc6b43488335bd357aea443fabe",
Expand Down
5 changes: 5 additions & 0 deletions src/Neo.CLI/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
"MaxTransactionsPerBlock": 512,
"MemoryPoolMaxTransactions": 50000,
"MaxTraceableBlocks": 2102400,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512,
"MaxTransactionsPerSender": 100
},
"Hardforks": {
"HF_Aspidochelone": 1730000,
"HF_Basilisk": 4120000,
Expand Down
5 changes: 5 additions & 0 deletions src/Neo.CLI/config.json.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ This README provides an explanation for each field in the JSON configuration fil
### MaxTraceableBlocks
- **MaxTraceableBlocks**: Maximum number of blocks that can be traced back. Default is `2102400`.

### MemoryPoolSettings
- **SmartThrottler**: Boolean flag to enable or disable the smart throttler. Default is `true`.
- **MaxTransactionsPerSecond**: Maximum number of transactions can be added to the memory pool per second. Default is `512`. Work only when `SmartThrottler` is `true`.
- **MaxTransactionsPerSender**: Maximum number of transactions that can be added to the memory pool per sender. Default is `100`. Work only when `SmartThrottler` is `true`.

### Hardforks
- **HF_Aspidochelone**: Block height for the Aspidochelone hard fork. MainNet is `1730000`, TestNet is `210000`.
- **HF_Basilisk**: Block height for the Basilisk hard fork. MainNet is `4120000`, TestNet is `2680000`.
Expand Down
5 changes: 5 additions & 0 deletions src/Neo.CLI/config.mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
"MaxTransactionsPerBlock": 512,
"MemoryPoolMaxTransactions": 50000,
"MaxTraceableBlocks": 2102400,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512,
"MaxTransactionsPerSender": 100
},
"Hardforks": {
"HF_Aspidochelone": 1730000,
"HF_Basilisk": 4120000,
Expand Down
5 changes: 5 additions & 0 deletions src/Neo.CLI/config.testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
"MaxTransactionsPerBlock": 5000,
"MemoryPoolMaxTransactions": 50000,
"MaxTraceableBlocks": 2102400,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512,
"MaxTransactionsPerSender": 100
},
"Hardforks": {
"HF_Aspidochelone": 210000,
"HF_Basilisk": 2680000,
Expand Down
14 changes: 14 additions & 0 deletions src/Neo/Ledger/MemoryPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

#nullable enable
using Akka.Util.Internal;
using Neo.Network.P2P;
using Neo.Network.P2P.Payloads;
Expand Down Expand Up @@ -41,6 +42,8 @@ public class MemoryPool : IReadOnlyCollection<Transaction>

private readonly NeoSystem _system;

private readonly SmartThrottler? _throttler;

//
/// <summary>
/// Guarantees consistency of the pool data structures.
Expand Down Expand Up @@ -127,6 +130,8 @@ public MemoryPool(NeoSystem system)
Capacity = system.Settings.MemoryPoolMaxTransactions;
MaxMillisecondsToReverifyTx = (double)system.Settings.MillisecondsPerBlock / 3;
MaxMillisecondsToReverifyTxPerIdle = (double)system.Settings.MillisecondsPerBlock / 15;
if (_system.Settings.MemPoolSettings.EnableSmartThrottler)
_throttler = new SmartThrottler(this, system);
}

/// <summary>
Expand Down Expand Up @@ -291,6 +296,11 @@ internal bool CanTransactionFitInPool(Transaction tx)

internal VerifyResult TryAdd(Transaction tx, DataCache snapshot)
{
if (_throttler != null && !_throttler.ShouldAcceptTransaction(tx))
{
return VerifyResult.OutOfMemory;
}

var poolItem = new PoolItem(tx);

if (_unsortedTransactions.ContainsKey(tx.Hash)) return VerifyResult.AlreadyInPool;
Expand Down Expand Up @@ -392,6 +402,7 @@ private List<Transaction> RemoveOverCapacity()
{
PoolItem minItem = GetLowestFeeTransaction(out var unsortedPool, out var sortedPool);

_throttler?.RemoveTransaction(minItem.Tx);
unsortedPool.Remove(minItem.Tx.Hash);
sortedPool.Remove(minItem);
removedTransactions.Add(minItem.Tx);
Expand All @@ -414,6 +425,7 @@ private bool TryRemoveVerified(UInt256 hash, out PoolItem item)

_unsortedTransactions.Remove(hash);
_sortedTransactions.Remove(item);
_throttler?.RemoveTransaction(item.Tx);

RemoveConflictsOfVerified(item);

Expand Down Expand Up @@ -444,6 +456,7 @@ internal bool TryRemoveUnVerified(UInt256 hash, out PoolItem item)

_unverifiedTransactions.Remove(hash);
_unverifiedSortedTransactions.Remove(item);
_throttler?.RemoveTransaction(item.Tx);
return true;
}

Expand Down Expand Up @@ -508,6 +521,7 @@ internal void UpdatePoolForBlockPersisted(Block block, DataCache snapshot)

// Add all the previously verified transactions back to the unverified transactions and clear mempool conflicts list.
InvalidateVerifiedTransactions();
_throttler?.UpdateNetworkState(block);
}
finally
{
Expand Down
218 changes: 218 additions & 0 deletions src/Neo/Ledger/SmartThrottler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright (C) 2015-2024 The Neo Project.
//
// SmartThrottler.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.Network.P2P.Payloads;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Neo.Ledger;

/// <summary>
/// SmartThrottler: Protects Neo blockchain's memory pool from attacks and network congestion
/// </summary>
public class SmartThrottler
{
private readonly MemoryPool _memoryPool;
private readonly NeoSystem _system;
private uint _maxTransactionsPerSecond;
private int _transactionsThisSecond;
private DateTime _lastResetTime;
private readonly object _lock = new();
private readonly ConcurrentDictionary<UInt160, int> _senderTransactionCount = new();
private readonly int _maxTransactionsPerSender;
private long _averageFee;

// Fields for network load estimation
private readonly Queue<ulong> _recentBlockTimes = new();
private const int BlockTimeWindowSize = 20; // Consider last 20 blocks
Copy link
Member

Choose a reason for hiding this comment

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

Move to config

private ulong _lastBlockTimestamp;
private int _unconfirmedTxCount;

/// <summary>
/// Initializes a new instance of the SmartThrottler
/// </summary>
/// <param name="memoryPool">The memory pool this throttler is associated with</param>
/// <param name="system">The Neo system</param>
public SmartThrottler(MemoryPool memoryPool, NeoSystem system)
{
_memoryPool = memoryPool;
_system = system;
_maxTransactionsPerSecond = (uint)system.Settings.MemPoolSettings.MaxTransactionsPerSecond;
_lastResetTime = TimeProvider.Current.UtcNow;
_maxTransactionsPerSender = system.Settings.MemPoolSettings.MaxTransactionsPerSender;
_lastBlockTimestamp = TimeProvider.Current.UtcNow.ToTimestampMS();
_averageFee = CalculateAverageFee(null);
}

/// <summary>
/// Determines whether a new transaction should be accepted
/// </summary>
/// <param name="tx">The transaction to be evaluated</param>
/// <returns>True if the transaction should be accepted, false otherwise</returns>
public bool ShouldAcceptTransaction(Transaction tx)
{
lock (_lock)
{
var now = TimeProvider.Current.UtcNow;
if (now - _lastResetTime >= TimeSpan.FromSeconds(1))
{
_transactionsThisSecond = 0;
_lastResetTime = now;
AdjustThrottling(null);
_averageFee = CalculateAverageFee(null);
}

// Check if we've hit the tx limit and it's not high priority
if (_transactionsThisSecond >= _maxTransactionsPerSecond && !IsHighPriorityTransaction(tx))
return false;

// Check if sender has reached their tx limit
if (!CheckSenderLimit(tx.Sender))
return false;

_transactionsThisSecond++;
_senderTransactionCount.AddOrUpdate(tx.Sender, 1, (_, count) => count + 1);
return true;
}
}

/// <summary>
/// Updates the network state after a new block is added
/// </summary>
/// <param name="block">The newly added block</param>
public void UpdateNetworkState(Block block)
{
lock (_lock)
{
var currentTime = TimeProvider.Current.UtcNow.ToTimestampMS();
var blockTime = currentTime - _lastBlockTimestamp;

_recentBlockTimes.Enqueue(blockTime);
if (_recentBlockTimes.Count > BlockTimeWindowSize)
_recentBlockTimes.Dequeue();

_lastBlockTimestamp = currentTime;
_unconfirmedTxCount = _memoryPool.Count;
_averageFee = CalculateAverageFee(block);

AdjustThrottling(block);

// 不再调用 ResetAfterNewBlock()
}
}

/// <summary>
/// Removes a transaction from throttler tracking
/// </summary>
/// <param name="tx">The transaction being removed</param>
public void RemoveTransaction(Transaction tx)
{
_senderTransactionCount.AddOrUpdate(tx.Sender, 0, (_, count) => Math.Max(0, count - 1));
}

/// <summary>
/// Adjusts throttling parameters based on current network conditions
/// </summary>
private void AdjustThrottling(Block block)
{
var memoryPoolUtilization = (double)_memoryPool.Count / _system.Settings.MemoryPoolMaxTransactions;
var networkLoad = EstimateNetworkLoad(block);

_maxTransactionsPerSecond = CalculateOptimalTps(memoryPoolUtilization, networkLoad, block);
Copy link
Member

Choose a reason for hiding this comment

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

_optimalMaxTransactionsPerSecond instead of _maxTransactionsPerSecond

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no difference to the code.

Copy link
Member

Choose a reason for hiding this comment

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

nomenclature is bad like this, maxTransactionPerSecond for something that is variable does not looks good to me.

}

/// <summary>
/// Estimates current network load
/// </summary>
/// <returns>An integer between 0 and 100 representing the estimated network load</returns>
private int EstimateNetworkLoad(Block block)
{
var load = 0;

// 1. Memory pool utilization (30% weight)
Copy link
Member

Choose a reason for hiding this comment

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

These weights does not look like %, they are more than 100

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how could it be more than 100?

 var memPoolUtilization = (double)_memoryPool.Count / _system.Settings.MemoryPoolMaxTransactions * 100;

Copy link
Member

Choose a reason for hiding this comment

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

Check in the thread below

var memPoolUtilization = (double)_memoryPool.Count / _system.Settings.MemoryPoolMaxTransactions;
Copy link
Member

Choose a reason for hiding this comment

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

get this value from previous line before calling the method, it is called just once as it is right now

load += (int)(memPoolUtilization * 30);

// 2. Recent block times (30% weight)
if (_recentBlockTimes.Count > 0)
{
var avgBlockTime = _recentBlockTimes.Average(t => (double)t);
var blockTimeRatio = avgBlockTime / _system.Settings.MillisecondsPerBlock;
load += (int)(Math.Min(blockTimeRatio, 2) * 30); // Cap at 60 points
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure.

avgBlockTime should usually not be lower than " _system.Settings.MillisecondsPerBlock".
So, usually blockTimeRatio is close to "1".

So, there is an average load from 30 - 60

Copy link
Contributor Author

Choose a reason for hiding this comment

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

its either 0 or 30 actually.

Copy link
Member

Choose a reason for hiding this comment

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

How?

(int)(Math.Min(blockTimeRatio, 2) * 30);

BlockTimeRatio will never/RARELY be less than 1.
So the range is 30 - 60 in this formula

}

// 3. Current block transaction count or unconfirmed transaction growth rate (40% weight)
if (block != null)
{
var blockTxRatio = (double)block.Transactions.Length / _system.Settings.MaxTransactionsPerBlock;
load += (int)(Math.Min(blockTxRatio, 1) * 40);
}
else
{
var txGrowthRate = (double)_unconfirmedTxCount / _system.Settings.MaxTransactionsPerBlock;
load += (int)(Math.Min(txGrowthRate, 1) * 40);
}

return Math.Min(load, 100); // Ensure load doesn't exceed 100
Copy link
Member

@vncoelho vncoelho Jun 28, 2024

Choose a reason for hiding this comment

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

This is bad for any weight metric, insert some cut after weighting.

This part of the code needs enhanced.

If we decide move on with this solution I can later test and analyze.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated, now maximum load is 100.

Copy link
Member

Choose a reason for hiding this comment

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

how could it be more than 100?

The same reason you have this Math.Min(load, 100); here

}

/// <summary>
/// Calculates optimal transactions per second
/// </summary>
private uint CalculateOptimalTps(double memoryPoolUtilization, int networkLoad, Block block)
{
var baseTps = _system.Settings.MaxTransactionsPerBlock * 4;
var utilizationFactor = 1 - memoryPoolUtilization;
var loadFactor = 1 - (networkLoad / 100.0);

// Consider current block's transaction count if available
var blockFactor = 1.0;
if (block != null)
{
blockFactor = Math.Max(0.5, (double)block.Transactions.Length / _system.Settings.MaxTransactionsPerBlock);
}

var optimalTps = (uint)(baseTps * utilizationFactor * loadFactor * blockFactor);
return Math.Max(optimalTps, _system.Settings.MaxTransactionsPerBlock); // Ensure TPS isn't lower than max transactions per block
}

/// <summary>
/// Determines if a transaction is high priority
/// </summary>
private bool IsHighPriorityTransaction(Transaction tx)
{
// High priority: fee > 3x average
Copy link
Member

Choose a reason for hiding this comment

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

committee address?

return tx.NetworkFee + tx.SystemFee > _averageFee * 3;
}

/// <summary>
/// Checks if sender has reached their transaction limit
/// </summary>
private bool CheckSenderLimit(UInt160 sender)
{
return _senderTransactionCount.GetOrAdd(sender, 0) < _maxTransactionsPerSender;
}

/// <summary>
/// Calculates average fee of transactions in memory pool and new block (if provided)
/// </summary>
private long CalculateAverageFee(Block block)
{
var transactions = _memoryPool.GetSortedVerifiedTransactions().ToList();
if (block != null)
{
transactions.AddRange(block.Transactions); // Include transactions from the new block
}
return transactions.Count != 0 ? (long)transactions.Average(tx => tx.NetworkFee + tx.SystemFee) : 0;
}
}
Loading