perf: consensus, storage, execution, and network hot-path optimizations#74
Open
perf: consensus, storage, execution, and network hot-path optimizations#74
Conversation
- 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
There was a problem hiding this comment.
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
Commit 2 — Execution caching (4 files):
Commit 3 — Node/consensus/DEX (3 files):
Commit 4 — Trie/execution/network (10 files):
Commit 5 — DEX/bridge (3 files):
Estimated impact
Test plan
dotnet build— 0 warnings, 0 errorsdotnet test— all 2,905 tests pass, 0 failures