# Neo TaskManager P2P Protocol: Coordinating Block and Transaction Synchronization

> Discover how Neo's TaskManager coordinates P2P block and transaction synchronization, preventing redundant requests and ensuring efficient network-wide data consistency.

- Repository: [The Neo Project/neo](https://github.com/neo-project/neo)
- Tags: internals
- Published: 2026-03-08

---

**The `TaskManager` in Neo’s P2P protocol is an Akka.NET actor that acts as the central coordinator for inventory-level synchronization, maintaining per-peer `TaskSession` state machines and global concurrency counters to prevent redundant block and transaction requests across the network.**

The `TaskManager` class in the neo-project/neo repository orchestrates how a local node fetches block headers, full blocks, and transactions from remote peers. Located in [`src/Neo/Network/P2P/TaskManager.cs`](https://github.com/neo-project/neo/blob/main/src/Neo/Network/P2P/TaskManager.cs), this component ensures reliable synchronization by throttling concurrent requests, deduplicating in-flight inventory, and automatically retrying stalled tasks without flooding the network.

## What Is the TaskManager in Neo's P2P Protocol?

The **TaskManager** is an Akka.NET actor that manages the lifecycle of synchronization tasks between the local node and its connected peers. When a TCP connection is established, the `RemoteNode` registers with the manager via the `Register` message, triggering the creation of a **TaskSession** object that tracks which block headers, block bodies, and transactions are currently being requested or have already been received.

To prevent bandwidth waste, the manager maintains two global counters:

- **`globalInvTasks`** – Tracks how many peers are currently requesting a specific inventory hash (block or transaction).
- **`globalIndexTasks`** – Tracks concurrent requests for specific block indices.

Both counters enforce a hard limit of **`MaxConcurrentTasks = 3`** for any single hash or index. This ensures that no more than three peers simultaneously request the same data, balancing redundancy with network efficiency.

A **timer** fires every 30 seconds to purge stale tasks older than `TaskTimeout = 1` minute and re-issue missing requests, guaranteeing forward progress even when peers become unresponsive【source lines 71-63】.

## How Block Syncing Works in Neo's TaskManager

Block synchronization follows a header-first strategy implemented in the `RequestTasks` method. The manager decides whether to fetch headers or full blocks based on the node's current height relative to the peer's reported `LastBlockIndex`.

### Requesting Block Headers First

If the local node is behind on headers, the manager checks `HasHeaderTask` and `globalInvTasks[HeaderTaskHash]` before issuing a `GetHeaders` command. The code explicitly verifies that the header cache is not full before requesting the next batch:

```csharp
if ((!HasHeaderTask || globalInvTasks[HeaderTaskHash] < MaxConcurrentTasks) &&
    headerHeight < session.LastBlockIndex && !system.HeaderCache.Full)
{
    session.InvTasks[HeaderTaskHash] = DateTime.UtcNow;
    IncrementGlobalTask(HeaderTaskHash);
    remoteNode.Tell(Message.Create(MessageCommand.GetHeaders,
            GetBlockByIndexPayload.Create(headerHeight + 1)));
}

```

This logic appears at lines 90-95 of `RequestTasks` in [`TaskManager.cs`](https://github.com/neo-project/neo/blob/main/TaskManager.cs), ensuring that header requests respect the global concurrency limit.

### Switching to Block-by-Index Requests

Once the local header height catches up to the chain tip, the manager switches to requesting full block bodies by index. It calculates a non-overlapping range of indices that are not already in flight via other sessions, increments `globalIndexTasks` for each slot, and sends a `GetBlockByIndex` message:

```csharp
remoteNode.Tell(Message.Create(MessageCommand.GetBlockByIndex,
        GetBlockByIndexPayload.Create(startHeight, (short)count)));

```

This block-by-index approach (lines 96-104) allows the node to pipeline block downloads across multiple peers while avoiding duplicate requests for the same indices.

### Deduplication and Timeout Handling

Incoming `NewTasks` payloads are filtered against **`_knownHashes`** (already received inventory) and `globalInvTasks` (currently in-flight requests). If a block hash is still pending, it is added to `session.AvailableTasks` and retried during the next timer tick. This deduplication mechanism prevents the node from requesting the same block from multiple peers once the global task limit is reached.

## How Transaction Syncing Works in the TaskManager

Transaction synchronization is gated by header sync progress. Inside the `OnNewTasks` handler, the manager explicitly ignores transaction inventories (`InventoryType.TX`) until the node's `currentHeight` reaches the latest `headerHeight`:

```csharp
if (currentHeight < headerHeight && (payload.Type == InventoryType.TX ||
      (payload.Type == InventoryType.Block && currentHeight < session.LastBlockIndex - InvPayload.MaxHashesCount)))
{
    RequestTasks(Sender, session);
    return;
}

```

This guard (lines 4-8 of `OnNewTasks`) prevents the node from processing transactions that belong to unknown blocks. Once fully synced, transaction hashes are handled like any other inventory: they are added to the `TaskSession`, counted in `globalInvTasks`, and requested via `MessageCommand.GetData`. Unlike blocks, transactions do not enforce a per-peer limit; the global `MaxConcurrentTasks` counter provides sufficient backpressure.

## Handling Task Completion and Invalid Blocks

When an inventory item is successfully received, the `OnTaskCompleted` method (lines 18-27) executes the following cleanup:

1.  Stores the hash in **`_knownHashes`** to prevent future re-requests.
2.  Decrements `globalInvTasks` and `globalIndexTasks` to free concurrency slots.
3.  Removes the hash from the session's pending dictionaries (`InvTasks`, `IndexTasks`).
4.  If the item is a block, stores it in `session.ReceivedBlock` for later verification.

If a block fails validation after relay (handled in `OnInvalidBlock`), the offending remote actor receives an abort signal to terminate the TCP connection, immediately isolating the source of bad data.

## TaskManager Implementation Example

The following pattern demonstrates how the P2P layer initializes synchronization with a new peer:

```csharp
// Inside the P2P node when a TCP connection is established
var remoteNode = Context.ActorOf(Props.Create(() => new RemoteNodeActor()));
var version = new VersionPayload(/* ... */);
remoteNode.Tell(new TaskManager.Register(version));

// After the remote node reports its last block index
remoteNode.Tell(new TaskManager.Update(lastBlockIndex));

// The remote node replies with an inventory payload (e.g., block hashes)
remoteNode.Tell(new TaskManager.NewTasks(invPayload));

```

Upon receiving these messages, the `TaskManager` automatically creates a `TaskSession`, calculates whether to request headers or blocks based on the node's current state, and maintains the session via the 30-second retry timer until synchronization is complete.

## Summary

-   The **`TaskManager`** in [`src/Neo/Network/P2P/TaskManager.cs`](https://github.com/neo-project/neo/blob/main/src/Neo/Network/P2P/TaskManager.cs) is the central actor coordinating block and transaction synchronization in Neo's P2P protocol.
-   It enforces a global limit of **3 concurrent tasks** per hash or index via `globalInvTasks` and `globalIndexTasks` to prevent redundant network traffic.
-   Block syncing uses a **header-first strategy**, switching to block-by-index requests once headers are current.
-   **Transaction requests are deferred** until the node is fully synced on headers, avoiding orphan transaction processing.
-   A **30-second timer** re-issues stale tasks (timeout after 1 minute) and cleans up failed sessions, ensuring robust progress even with unreliable peers.

## Frequently Asked Questions

### What is the maximum number of concurrent tasks per hash in Neo's TaskManager?

The `TaskManager` enforces a default limit of **3 concurrent tasks** per inventory hash or block index through the `MaxConcurrentTasks` constant. This limit is applied via `globalInvTasks` and `globalIndexTasks` counters, ensuring that no more than three peers simultaneously request the same block or transaction.

### How does TaskManager prevent redundant block requests?

The manager maintains a **`_knownHashes`** set containing all previously received inventory and checks **`globalInvTasks`** before issuing new requests. If a hash already exists in either collection, the request is skipped. This deduplication occurs in the `OnNewTasks` handler and ensures that in-flight items are not re-requested from additional peers once the concurrency limit is reached.

### Why does TaskManager ignore transactions before headers are synced?

Inside `OnNewTasks`, the code explicitly checks if `currentHeight < headerHeight` when processing `InventoryType.TX` payloads. This guard prevents the node from downloading transactions that reference blocks it has not yet validated. Processing such transactions would result in orphan transactions or failed validations, so the manager prioritizes header synchronization before enabling transaction fetching.

### How often does TaskManager retry stale synchronization tasks?

The actor initializes a **30-second timer** that iterates through all `TaskSession` objects and purges tasks older than the **`TaskTimeout`** value of 1 minute. Stale tasks are removed from the session's pending dictionaries, and the global counters are decremented, allowing the next timer tick or incoming `NewTasks` message to re-issue the request automatically.