Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 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,10 @@
"MaxTraceableBlocks": 2102400,
"InitialGasDistribution": 5200000000000000,
"ValidatorsCount": 7,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512
},
"Hardforks": {
"HF_Aspidochelone": 3000000,
"HF_Basilisk": 4500000,
Expand Down
4 changes: 4 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,10 @@
"MaxTraceableBlocks": 2102400,
"InitialGasDistribution": 5200000000000000,
"ValidatorsCount": 7,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512
},
"StandbyCommittee": [
"02082828ec6efc92e5e7790da851be72d2091a961c1ac9a1772acbf181ac56b831",
"02b2bcf7e09c0237ab6ef21808e6f7546329823bc6b43488335bd357aea443fabe",
Expand Down
4 changes: 4 additions & 0 deletions src/Neo.CLI/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
"MaxTransactionsPerBlock": 512,
"MemoryPoolMaxTransactions": 50000,
"MaxTraceableBlocks": 2102400,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512
},
"Hardforks": {
"HF_Aspidochelone": 1730000,
"HF_Basilisk": 4120000,
Expand Down
4 changes: 4 additions & 0 deletions src/Neo.CLI/config.json.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ 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`.

### 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
4 changes: 4 additions & 0 deletions src/Neo.CLI/config.mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
"MaxTransactionsPerBlock": 512,
"MemoryPoolMaxTransactions": 50000,
"MaxTraceableBlocks": 2102400,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512
},
"Hardforks": {
"HF_Aspidochelone": 1730000,
"HF_Basilisk": 4120000,
Expand Down
4 changes: 4 additions & 0 deletions src/Neo.CLI/config.testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
"MaxTransactionsPerBlock": 5000,
"MemoryPoolMaxTransactions": 50000,
"MaxTraceableBlocks": 2102400,
"MemoryPool": {
"EnableSmartThrottler": true,
"MaxTransactionsPerSecond": 512
},
"Hardforks": {
"HF_Aspidochelone": 210000,
"HF_Basilisk": 2680000,
Expand Down
11 changes: 11 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 @@ -508,6 +518,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
184 changes: 184 additions & 0 deletions src/Neo/Ledger/SmartThrottler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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.

#nullable enable

using Neo.Network.P2P.Payloads;
using System;
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 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;
_lastBlockTimestamp = _lastResetTime.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)
{
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;

_transactionsThisSecond++;
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)
{
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);
}

/// <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)
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 double as well and base %, already divided by 100.

The only use of it is aligned with memPool Use

{
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); // Cap at 30 points

// 2. Recent block times (30% weight)
if (_recentBlockTimes.Count > 0)
{
var avgBlockTime = _recentBlockTimes.Average(t => (double)t);
load += (avgBlockTime < _system.Settings.MillisecondsPerBlock ? 1 : 0) * 30; // Cap at 30 points
}

// 3. Current block transaction count or unconfirmed transaction growth rate (40% weight)
if (block != null) // Cap at 40 points
{
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 load;
}

/// <summary>
/// Calculates optimal transactions per second
/// </summary>
private uint CalculateOptimalTps(double memoryPoolUtilization, int networkLoad, Block? block)
{
var baseTps = _system.Settings.MemPoolSettings.MaxTransactionsPerSecond;
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 || tx.GetAttribute<HighPriorityAttribute>() != null;
}

/// <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;
}
}
73 changes: 73 additions & 0 deletions src/Neo/Ledger/SmartThrottler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# SmartThrottler

## 1. Introduction

The SmartThrottler is designed to protect the blockchain from potential attacks and network congestion by intelligently controlling transaction flow. This document outlines its core features and implementation details. Issue ref. https://github.com/neo-project/neo/issues/2862.

## 2. Key Features

- Dynamic adjustment of transaction acceptance rate
- Priority handling for high-fee transactions
- Multi-factor network load estimation
- Adaptive response to new block additions

## 3. Core Components

### 3.1 Transaction Acceptance Control

The `ShouldAcceptTransaction` method is the gatekeeper for new transactions. It resets the per-second transaction counter every second and checks against sender limits. High-fee transactions get preferential treatment.

### 3.2 Network Load Estimation

Network load is calculated based on three factors:

1. Memory Pool Usage (30% weight)
- Ratio of current transactions to pool capacity
- Indicates short-term transaction backlog

2. Recent Block Times (30% weight)
- Average time for the last 20 blocks vs. expected time
- Reflects medium-term network performance

3. Transaction Growth or Block Fullness (40% weight)
- Either current block transaction count or unconfirmed transaction growth
- Shows immediate transaction processing pressure

The final load score is capped at 100 to maintain consistency.

### 3.3 Optimal TPS Calculation

The `CalculateOptimalTps` method determines the best transactions-per-second rate. It factors in memory pool usage, network load, and current block details to adapt to changing conditions.

### 3.4 High-Priority Transaction Identification

Transactions with fees exceeding 3 times the average are flagged as high-priority. This allows important transactions to bypass normal throttling limits.

### 3.5 Sender Limit Enforcement

Each sender is capped at 10 transactions in the memory pool. This prevents any single entity from flooding the network.

## 4. Workflow

1. Initialization: Set up initial parameters.
2. Transaction Acceptance:
- Evaluate network conditions
- Apply throttling rules
- Update counters for accepted transactions
3. Network State Updates:
- Recalculate average fees
- Adjust throttling parameters

## 5. Key Algorithms

### 5.1 Network Load Calculation

```
load = (pool_usage * 30) + (block_time_factor * 30) + (tx_growth_or_block_fullness * 40)
```

### 5.2 Optimal TPS Calculation

```
optimal_tps = base_tps * (1 - pool_usage) * (1 - network_load/100) * block_factor
```
Loading