Skip to content

perf: consensus, storage, execution, and network hot-path optimizations#74

Open
0xZunia wants to merge 6 commits intomainfrom
perf/consensus-storage-mempool-optimizations
Open

perf: consensus, storage, execution, and network hot-path optimizations#74
0xZunia wants to merge 6 commits intomainfrom
perf/consensus-storage-mempool-optimizations

Conversation

@0xZunia
Copy link
Contributor

@0xZunia 0xZunia commented Mar 15, 2026

Summary

Comprehensive performance optimization pass across 24 files, targeting allocation reduction in consensus, storage, execution, trie, DEX, bridge, and network layers.

Changes (5 commits)

Commit 1 — Core hot paths (8 files):

  • Cache BLS public key byte[] in ValidatorInfo (eliminates 6+ allocs per vote)
  • O(1) vote deduplication via VoteSignatureSet with FNV-1a HashSet
  • Per-address storage slot index in FlatStateDb (O(1) account deletion)
  • Copy-on-write Fork() — share byte[] references instead of deep copy
  • Skip signature re-verification in BlockBuilder for mempool-admitted txs
  • O(n log m) mempool sender selection via SortedSet priority queue
  • ArrayPool clearArray:false in MessageCodec

Commit 2 — Execution caching (4 files):

  • Cache compliance proofs hash in Transaction
  • Direct array construction in PipelinedConsensus aggregation
  • Reuse key buffer in ReceiptStore.PutReceipts
  • Pre-allocate buffers in RocksDbFlatStatePersistence.Flush

Commit 3 — Node/consensus/DEX (3 files):

  • Replace .ToArray().Where().ToList() with direct iteration in NodeCoordinator
  • stackalloc in WeightedLeaderSelector stake conversion
  • Cache DEX event signature BLAKE3 hashes

Commit 4 — Trie/execution/network (10 files):

  • Eliminate MemoryStream in TrieNode.Encode() — pre-computed exact size
  • ReadOnlySpan in NibblePath.FromCompactEncoding (avoid .ToArray() in Decode)
  • Binary uint32 selector comparison in ManagedContractRuntime (zero-alloc)
  • Cache Address.Zero bytes, lazy CallerBytes/ContractAddressBytes
  • Direct array Merkle root construction in BlockBuilder
  • PopCount + pre-sized array in GetValidatorsFromBitmap
  • Loop-based tier rebalance in EpisubService

Commit 5 — DEX/bridge (3 files):

  • In-place RemoveAll() in BatchAuctionSolver deadline filtering
  • Cache relayer list in MultisigRelayer
  • Eliminate intermediate hashes array in BridgeProofVerifier

Estimated impact

Metric Before After
Allocs per block (100 txs) ~1,200-1,500 ~400-600 (~60% reduction)
Vote deduplication O(n) linear scan O(1) HashSet
Mempool tx selection O(n*m) nested loop O(n log m) SortedSet
Fork() storage copy Deep copy all byte[] Share references (COW)
TrieNode.Encode MemoryStream + resize + copy Single exact-size array
Contract dispatch 2 string allocs (hex) 0 allocs (uint compare)

Test plan

  • dotnet build — 0 warnings, 0 errors
  • dotnet test — all 2,905 tests pass, 0 failures
  • Devnet smoke test with 4 validators

0xZunia added 6 commits March 14, 2026 16:46
- Cache BLS public key byte[] in ValidatorInfo to eliminate heap
  allocations in the consensus vote verification loop
- Replace O(n) List.Exists() vote deduplication with O(1) HashSet
  via VoteSignatureSet with FNV-1a ByteArrayKey
- Add per-address storage slot index in FlatStateDb for O(1) account
  deletion instead of scanning the entire storage cache
- Replace deep-copy Fork() with shallow reference sharing (COW-safe
  since SetStorage always replaces the byte[] reference)
- Skip Ed25519 signature re-verification in BlockBuilder for
  transactions already validated at mempool admission
- Replace O(n*m) mempool sender selection with O(n log m) SortedSet
  priority queue
- Avoid zeroing ArrayPool buffers on return in MessageCodec
…on allocs

- Cache compliance proofs hash in Transaction to avoid recomputing
  on every WriteSigningPayload call
- Replace .Select().ToArray() with direct array construction in
  PipelinedConsensus GetAggregateSignature
- Reuse key buffer across iterations in ReceiptStore.PutReceipts
- Pre-allocate account/storage key and state encoding buffers in
  RocksDbFlatStatePersistence.Flush instead of per-iteration allocs
- Apply skipSignature optimization to BuildBlockWithDex path
…on, cache DEX event sigs

- Replace .ToArray().Where().ToList() with direct iteration in
  NodeCoordinator proposal evidence cleanup (2 call sites)
- Use stackalloc for UInt256 stake-to-weight conversion in
  WeightedLeaderSelector instead of heap byte[32]
- Cache BLAKE3 hashes of DEX event signatures in static dictionary
  to avoid per-operation string concat + hash computation
…ath optimizations

Trie layer:
- Replace MemoryStream in TrieNode.Encode() with pre-computed exact
  size and direct byte[] write — eliminates ~3 allocs per trie node
- Accept ReadOnlySpan<byte> in NibblePath.FromCompactEncoding() to
  avoid .ToArray() copies during TrieNode.Decode()

Execution layer:
- Replace hex string selector comparison with uint32 binary compare
  in ManagedContractRuntime — eliminates 2 string allocs per call
- Cache Address.Zero.ToArray() as static field in TransactionExecutor
- Add lazy-cached CallerBytes/ContractAddressBytes on VmExecutionContext
  so ContractBridge.Setup() avoids .ToArray() per contract call
- Replace .Select().ToList() with direct array construction for Merkle
  root computation in BlockBuilder
- Replace LINQ .Take().Select().ToList() with direct loop in Mempool
  fast path (no-stateDb case)

Consensus layer:
- Use PopCount + pre-sized array in ValidatorSet.GetValidatorsFromBitmap
  instead of dynamically-resized List

Network layer:
- Replace LINQ chains (Select→Where→OrderBy→FirstOrDefault) with
  direct min/max loops in EpisubService.RebalanceTiers()
- Replace .Where().ToList() with in-place RemoveAll() in
  BatchAuctionSolver deadline filtering to avoid List allocation
- Replace .Select().ToList() with direct loop for order extraction
- Cache relayer list in MultisigRelayer, invalidate on add/remove
- Eliminate intermediate hashes array in BridgeProofVerifier by
  computing leaf hashes directly into the padded buffer
@0xZunia 0xZunia requested a review from Copilot March 19, 2026 08:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Performance-focused refactor across consensus, execution, storage/trie, DEX, bridge, and network code paths to reduce allocations and improve asymptotic behavior in several hot loops.

Changes:

  • Reworked several serialization/encoding paths to write directly into exact-sized buffers (trie nodes, network message buffers, Merkle roots).
  • Added caching and indexing structures to avoid repeated allocations/scans (BLS pubkey bytes, compliance proofs hash, per-address storage slot index, event signature hashes).
  • Improved selection/dedup algorithms in mempool/consensus/network services (SortedSet-based sender interleaving, O(1) vote signature dedup, loop-based tier rebalance).

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/storage/Basalt.Storage/Trie/TrieNode.cs Replaces MemoryStream-based node encoding with exact-size buffer writes; span-based compact path decode.
src/storage/Basalt.Storage/Trie/NibblePath.cs Changes compact path decode API to accept ReadOnlySpan to avoid intermediate allocations.
src/storage/Basalt.Storage/RocksDb/RocksDbFlatStatePersistence.cs Reuses key/value buffers during flush to reduce per-iteration allocations.
src/storage/Basalt.Storage/RocksDb/ReceiptStore.cs Reuses receipt key buffer inside batched writes.
src/storage/Basalt.Storage/FlatStateDb.cs Adds per-address storage-slot index; switches Fork() to shallow-copy storage values (COW).
src/node/Basalt.Node/NodeCoordinator.cs Removes LINQ materialization when pruning old proposal keys.
src/network/Basalt.Network/MessageCodec.cs Returns pooled buffers without clearing to reduce overhead.
src/network/Basalt.Network/Gossip/EpisubService.cs Replaces LINQ sorting with single-pass best/worst selection loops.
src/execution/Basalt.Execution/VM/ManagedContractRuntime.cs Switches selector dispatch from hex strings to uint32 comparisons (zero-alloc fast path).
src/execution/Basalt.Execution/VM/ExecutionContext.cs Adds cached Caller/ContractAddress byte[] accessors to avoid repeated ToArray().
src/execution/Basalt.Execution/VM/ContractBridge.cs Uses cached address bytes from VmExecutionContext to reduce allocations.
src/execution/Basalt.Execution/TransactionValidator.cs Adds skipSignature option to validation for performance-sensitive paths.
src/execution/Basalt.Execution/TransactionExecutor.cs Caches Address.Zero bytes to avoid repeated allocations.
src/execution/Basalt.Execution/Transaction.cs Caches computed compliance proofs hash for reuse in signing payload.
src/execution/Basalt.Execution/Mempool.cs Avoids LINQ allocations; implements SortedSet “priority queue” sender interleaving.
src/execution/Basalt.Execution/Dex/DexEngine.cs Adds static cache for event signature hashes to avoid recomputation.
src/execution/Basalt.Execution/Dex/BatchAuctionSolver.cs Uses in-place filtering and loop-based list extraction to reduce allocations.
src/execution/Basalt.Execution/BlockBuilder.cs Skips signature re-verification in block building; avoids LINQ for Merkle leaf collection.
src/consensus/Basalt.Consensus/WeightedLeaderSelector.cs Uses stackalloc for stake-to-weight conversion buffer.
src/consensus/Basalt.Consensus/ValidatorSet.cs Adds cached BLS pubkey bytes; changes bitmap extraction to pre-sized array.
src/consensus/Basalt.Consensus/PipelinedConsensus.cs Uses cached BLS pubkey bytes and avoids LINQ allocations for signature aggregation.
src/consensus/Basalt.Consensus/BasaltBft.cs Replaces linear vote signature dedup with VoteSignatureSet and cached BLS pubkey bytes usage.
src/bridge/Basalt.Bridge/MultisigRelayer.cs Adds cached relayer list to avoid rebuilding list repeatedly.
src/bridge/Basalt.Bridge/BridgeProofVerifier.cs Removes intermediate hashes array and refactors Merkle proof building allocations; updates Caddyfile CORS note.
deploy/testnet/Caddyfile Removes duplicated CORS headers (comment-only guidance).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +141 to 151
// Remove storage entries for this address using O(1) index lookup
if (_storageSlotsIndex.TryGetValue(address, out var slots))
{
if (key.Item1 == address)
keysToRemove.Add(key);
}
foreach (var key in keysToRemove)
{
_storageCache.Remove(key);
_deletedStorage.Add(key);
foreach (var slot in slots)
{
var cacheKey = (address, slot);
_storageCache.Remove(cacheKey);
_deletedStorage.Add(cacheKey);
}
_storageSlotsIndex.Remove(address);
}
Comment on lines +144 to 149
int count = System.Numerics.BitOperations.PopCount(bitmap);
var result = new ValidatorInfo[count];
int idx = 0;
for (int i = 0; i < _validators.Count && i < 64; i++)
{
if ((bitmap & (1UL << i)) != 0)
Comment on lines +789 to +797
private static Hash256 GetEventSignature(string eventName)
{
if (!EventSigCache.TryGetValue(eventName, out var sig))
{
sig = Blake3Hasher.Hash(System.Text.Encoding.UTF8.GetBytes("Dex." + eventName));
EventSigCache[eventName] = sig;
}
return sig;
}
{
lock (_lock)
return _relayers.Values.ToList();
return _cachedRelayerList ??= _relayers.Values.ToList();
Comment on lines 184 to 189
if (BranchValue != null)
{
ms.WriteByte(1);
WriteLength(ms, BranchValue.Length);
ms.Write(BranchValue);
buffer[pos++] = 1;
WriteVarInt(buffer, ref pos, BranchValue.Length);
BranchValue.CopyTo(buffer.AsSpan(pos));
}
Comment on lines +30 to 43
// Pre-allocate reusable key buffers to avoid per-iteration allocations
var accountKey = new byte[1 + Address.Size];
accountKey[0] = AccountPrefix;
var storageKey = new byte[1 + Address.Size + Hash256.Size];
storageKey[0] = StoragePrefix;
var stateBuffer = new byte[137]; // nonce(8)+balance(32)+storageRoot(32)+codeHash(32)+type(1)+compliance(32)

// Write (upsert) live account and storage entries
foreach (var (address, state) in accounts)
{
var key = MakeAccountKey(address);
var value = EncodeAccountState(state);
batch.Put(RocksDbStore.CF.State, key, value);
address.WriteTo(accountKey.AsSpan(1));
EncodeAccountStateInto(state, stateBuffer);
batch.Put(RocksDbStore.CF.State, accountKey, stateBuffer);
}
Comment on lines +34 to 36
var key = new byte[Hash256.Size]; // Reuse across iterations
foreach (var receipt in receipts)
{
Comment on lines +78 to +99
// Use two alternating buffers instead of allocating per level
var current = new byte[size][];
for (int i = 0; i < size; i++)
padded[i] = i < hashes.Length ? hashes[i] : new byte[32];
current[i] = i < leaves.Length ? HashLeaf(leaves[i]) : new byte[32];

var proof = new List<byte[]>();
var idx = leafIndex;

while (padded.Length > 1)
while (current.Length > 1)
{
// Record sibling
var siblingIdx = idx ^ 1;
if (siblingIdx < padded.Length)
proof.Add(padded[siblingIdx]);
if (siblingIdx < current.Length)
proof.Add(current[siblingIdx]);

// Compute next level
var nextLevel = new byte[padded.Length / 2][];
for (int i = 0; i < nextLevel.Length; i++)
nextLevel[i] = HashPair(padded[2 * i], padded[2 * i + 1]);
// Compute next level into half-size array (reuse previous if big enough)
var halfLen = current.Length / 2;
var next = new byte[halfLen][];
for (int i = 0; i < halfLen; i++)
next[i] = HashPair(current[2 * i], current[2 * i + 1]);

padded = nextLevel;
current = next;
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.

2 participants