Skip to content

Commit a67cd6b

Browse files
authored
Merge pull request #8 from metaverse-systems/019-perf-dedup-cleanup
019 perf dedup cleanup
2 parents a190054 + d8f2c81 commit a67cd6b

26 files changed

Lines changed: 1722 additions & 977 deletions

.github/copilot-instructions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Auto-generated from all feature plans. Last updated: 2026-04-13
2222
- N/A (build-system-only change) (015-compile-time-optimization)
2323
- C++20 (`-std=c++20`) + Boost (Asio, Serialization), OpenSSL (EVP SHA-256), nlohmann/json (vendored `src/json.hpp`) (016-audit-remediation)
2424
- Boost.Serialization binary chunk files (`chunk_NNNNNN.dat`), keys (`keys.dat`), streams (`streams.dat`, `stream_index.dat`) (016-audit-remediation)
25+
- C++20 (`-std=c++20`) + Boost (Asio, Serialization), OpenSSL (EVP SHA-256), nlohmann/json (vendored `src/json.hpp`), Catch2 (test only) (019-perf-dedup-cleanup)
26+
- Boost.Serialization binary chunk files (`chunk_NNNNNN.dat`), JSON files (`peers.json`, `config.json`) (019-perf-dedup-cleanup)
2527

2628
- C++20 (`-std=c++20`) + Boost (Asio, Serialization), OpenSSL, nlohmann/json (vendored `src/json.hpp`), Catch2 (test only) (001-code-constitution-audit)
2729

@@ -49,9 +51,9 @@ C++20 (`-std=c++20`): Follow standard conventions
4951
- Do not include task numbers (e.g. T001, T010) in code comments. Comments should describe *what* or *why*, not reference planning artifacts.
5052

5153
## Recent Changes
54+
- 019-perf-dedup-cleanup: Added C++20 (`-std=c++20`) + Boost (Asio, Serialization), OpenSSL (EVP SHA-256), nlohmann/json (vendored `src/json.hpp`), Catch2 (test only)
5255
- 018-audit-bug-security-fixes: Added C++20 (`-std=c++20`) + Boost (Asio, Serialization), OpenSSL (EVP SHA-256), nlohmann/json (vendored `src/json.hpp`)
5356
- 017-blockchain-module-split: Added C++20 (`-std=c++20`) + Boost (Asio, Serialization), OpenSSL (EVP SHA-256), nlohmann/json (vendored `src/json.hpp`)
54-
- 016-audit-remediation: Added C++20 (`-std=c++20`) + Boost (Asio, Serialization), OpenSSL (EVP SHA-256), nlohmann/json (vendored `src/json.hpp`)
5557

5658

5759
<!-- MANUAL ADDITIONS START -->

.specify/feature.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"feature_directory": "specs/018-audit-bug-security-fixes"
2+
"feature_directory": "specs/019-perf-dedup-cleanup"
33
}

docs/AUDIT.md

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ This audit found **4 bugs**, **2 security issues**, **5 performance issues**,
3333
test-quality weaknesses** that reduce confidence in the test suite.
3434

3535
All 4 bugs, both security issues, and 1 performance issue were resolved in
36-
**018-audit-bug-security-fixes**. Remaining items are tracked below.
36+
**018-audit-bug-security-fixes**. Three more performance issues, both
37+
duplication clusters, and 1 test-quality issue were resolved in
38+
**019-perf-dedup-cleanup**. Remaining items are tracked below.
3739

3840
| Category | Found | Resolved | Remaining | Highest Open Severity |
3941
|-----------------------|------:|---------:|----------:|----------------------:|
4042
| Bugs | 4 | 4 | 0 ||
4143
| Security issues | 2 | 2 | 0 ||
42-
| Performance issues | 5 | 1 | 4 | Medium |
43-
| Code duplication | 2 | 0 | 2 | Medium |
44+
| Performance issues | 5 | 4 | 1 | Low |
45+
| Code duplication | 2 | 2 | 0 | |
4446
| Architecture concerns | 3 | 0 | 3 | Medium |
45-
| Test quality issues | 6 | 0 | 6 | High |
47+
| Test quality issues | 6 | 1 | 5 | High |
4648

4749
---
4850

@@ -91,39 +93,29 @@ returns JSON-RPC error -32001 "Block not found" for out-of-range indices.
9193

9294
## 4. Performance Issues
9395

94-
### 4.1 O(n) peer lookups — MEDIUM
96+
### 4.1 ~~O(n) peer lookups — MEDIUM~~ ✅ RESOLVED (019)
9597

96-
`find_peer()`, `add_peer()`, `remove_peer()`, `is_banned()`, and
97-
`get_non_banned_peer_addresses()` all perform linear scans over
98-
`std::vector<PeerEntry>` and `std::vector<BanRecord>`.
98+
`peers_` and `bans_` are now `std::unordered_map<std::string, PeerEntry/BanRecord>`
99+
keyed by `host:port`, giving O(1) lookups in `find_peer()`, `add_peer()`,
100+
`remove_peer()`, `is_banned()`, `ban_peer()`, `unban_peer()`, and
101+
`get_non_banned_peer_addresses()`.
99102

100-
With a configured maximum of 256 stored peers and `is_banned()` called on
101-
every peer exchange, connection attempt, and block reception, switching to
102-
`std::unordered_map<std::string, PeerEntry>` keyed by `host:port` would drop
103-
lookups from O(n) to O(1).
103+
### 4.2 ~~RPC dispatch is a 21-branch `if`/`else` chain — MEDIUM~~ ✅ RESOLVED (019)
104104

105-
### 4.2 RPC dispatch is a 21-branch `if`/`else` chain — MEDIUM
106-
107-
[RpcServer.cpp](../src/network/RpcServer.cpp#L74-L704)
108-
109-
Every request walks up to 21 string comparisons. A
110-
`std::unordered_map<std::string, Handler>` dispatch table would be O(1) and
111-
reduce the 700-line `do_read()` callback into individually testable handler
112-
functions.
105+
`do_read()` now performs a single `dispatch_.find(method)` lookup into an
106+
`std::unordered_map<std::string, RpcHandler>` initialized in `init_dispatch()`.
107+
All 20 handlers are private methods returning `nlohmann::json`.
113108

114109
### 4.3 ~~`recoverChain()` loads each chunk multiple times — MEDIUM~~ ✅ RESOLVED (018)
115110

116111
Resolved together with §2.2. Single-pass recovery loads each chunk once.
117112

118-
### 4.4 String construction in log calls — LOW
119-
120-
Throughout the codebase, `logMessage("INFO", "Block #" + std::to_string(...) + ...)`
121-
constructs the string even when the log level would suppress it. The
122-
`logMessage()` function already filters by level, but the string allocation
123-
happens at the call site.
113+
### 4.4 ~~String construction in log calls — LOW~~ ✅ RESOLVED (019)
124114

125-
**Fix:** A level-check macro or a lazy-evaluation wrapper would eliminate
126-
unnecessary allocations.
115+
Hot-path `logMessage()` calls in `PeerManager.cpp`, `BlockPropagation.cpp`,
116+
`PeerClient.cpp`, and `PeerServer.cpp` have been replaced with lazy
117+
`LOG_INFO`/`LOG_WARN`/`LOG_ERROR`/`LOG_DEBUG` macros that check `getLogLevel()`
118+
before evaluating the message expression.
127119

128120
### 4.5 `replaceChain()` loads entire candidate into memory — LOW
129121

@@ -137,19 +129,18 @@ simultaneously. A streaming/chunked replacement would bound memory usage.
137129

138130
## 5. Code Duplication
139131

140-
### 5.1 Packet serialization in PeerClient and PeerServer — MEDIUM
132+
### 5.1 ~~Packet serialization in PeerClient and PeerServer — MEDIUM~~ ✅ RESOLVED (019)
141133

142-
`PeerClient::send<T>()` ([PeerClient.cpp](../src/network/PeerClient.cpp#L352-L375)) and
143-
`PeerServer::send_packet<T>()` ([PeerServer.cpp](../src/network/PeerServer.cpp#L271-L300))
144-
share the same serialize → `PacketHeader``memcpy``async_write` pattern.
145-
This could live in a shared utility or base class.
134+
Both `PeerClient::send<T>()` and `PeerServer::send_packet<T>()` now call the
135+
shared `serialize_packet<T>()` template from `PacketSerializer.hpp`, which
136+
returns header bytes + serialized payload. Each caller retains its own
137+
`async_write` logic.
146138

147-
### 5.2 Test files still duplicate `mineTestBlock()` / `buildValidChain()` — LOW
139+
### 5.2 ~~Test files still duplicate `mineTestBlock()` / `buildValidChain()` — LOW~~ ✅ RESOLVED (019)
148140

149-
Despite `TestHelpers.hpp` existing, `sync_tests.cpp`,
150-
`block_propagation_tests.cpp`, `consensus_tests.cpp`, and
151-
`chunk_persistence_tests.cpp` still define their own local versions of
152-
`mineTestBlock()`, `buildValidChain()`, and temporary-directory helpers.
141+
Local helper definitions in `sync_tests.cpp`, `consensus_tests.cpp`, and
142+
`chunk_persistence_tests.cpp` have been removed. All test files now use the
143+
shared `TestHelpers::` namespace.
153144

154145
---
155146

@@ -263,13 +254,12 @@ The following behaviors have no test coverage:
263254
| Block propagation relay excludes sender correctly | Medium | Open |
264255
| `recoverChain()` with corrupted index files (fallback to chunk rebuild) | Medium | Open |
265256

266-
### 7.6 Duplicated test setup persists in 4 files — LOW
257+
### 7.6 ~~Duplicated test setup persists in 4 files — LOW~~ ✅ RESOLVED (019)
267258

268-
Despite `TestHelpers.hpp` existing, `sync_tests.cpp`,
269-
`block_propagation_tests.cpp`, `consensus_tests.cpp`, and
270-
`chunk_persistence_tests.cpp` still define local `mineTestBlock()` /
271-
`buildValidChain()` / temp directory helpers instead of using the shared
272-
utilities.
259+
All local test helper definitions have been removed. Test files now use
260+
`TestHelpers::mineTestBlock()`, `TestHelpers::buildValidChain()`,
261+
`TestHelpers::createTestDir()`, `TestHelpers::cleanupTestDir()`, and
262+
`TestHelpers::make_block()` exclusively.
273263

274264
---
275265

@@ -288,8 +278,8 @@ Ordered by impact and effort:
288278
| 5 | Rewrite `rpc_expansion_tests.cpp` to test real RPC handlers (§7.3) | False confidence → real coverage | Medium | Open |
289279
| 6 | Replace trivial assertions with meaningful ones (§7.1, §7.2) | Catches actual regressions | Medium | Open |
290280
| 7 | Cache chunk during `recoverChain()` validation (§2.2, §4.3) | 3× faster startup | Low | ✅ Done (018) |
291-
| 8 | Replace O(n) peer lookups with `unordered_map` (§4.1) | O(1) peer operations | Medium | Open |
292-
| 9 | Extract RPC dispatch table from `do_read()` (§4.2) | Maintainability, testability | Medium | Open |
281+
| 8 | Replace O(n) peer lookups with `unordered_map` (§4.1) | O(1) peer operations | Medium | ✅ Done (019) |
282+
| 9 | Extract RPC dispatch table from `do_read()` (§4.2) | Maintainability, testability | Medium | ✅ Done (019) |
293283
| 10 | Narrow `IBlockchain` into reader/writer interfaces (§6.1) | Reduces coupling | Medium | Open |
294-
| 11 | Remove local test helpers in favor of `TestHelpers.hpp` (§7.6) | Consistency | Low | Open |
284+
| 11 | Remove local test helpers in favor of `TestHelpers.hpp` (§7.6) | Consistency | Low | ✅ Done (019) |
295285
| 12 | Make integration tests deterministic (§7.4) | Reduces CI flakiness | Medium | Open |

docs/ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Last updated: 2026-04-13
2424
| 016 | Code Audit Remediation | Fixed 5 bugs (block count, dirty flag, merkle root, IPv6 parsing, pending pool), cached difficulty per boundary with ChunkRetainGuard, extracted shared utilities (chunkFilename, parsePeerKey, RPC helpers, send_to_peers), added TestHelpers.hpp, single-threaded io_context enforcement |
2525
| 017 | Blockchain Module Split | Split monolithic Blockchain.cpp (1,019 lines) into four focused modules: ChainPersistence (379 lines), DifficultyEngine (95 lines), MerkleProofService (60 lines), and slimmed Blockchain core (624 lines); composition-based ownership; zero API changes; 3 new focused test suites |
2626
| 018 | Audit Bug & Security Fixes | Fixed 4 bugs (sync response block append, RPC getBlockByIndex bounds check, getBlockByIndex resize chunk IDs, recovery triple-load) and 2 security issues (port range validation in parsePeerKey, seed node input validation in main); single-pass recovery optimization |
27+
| 019 | Performance & Deduplication Cleanup | O(1) peer lookups via `unordered_map`, RPC dispatch table replacing 21-branch `if`/`else`, shared `serialize_packet<T>()` template, consolidated `TestHelpers`, lazy `LOG_*` macros |
2728

2829
## Suggested Specs
2930

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Specification Quality Checklist: Performance & Deduplication Cleanup
2+
3+
**Purpose**: Validate specification completeness and quality before proceeding to planning
4+
**Created**: 2026-04-13
5+
**Feature**: [spec.md](../spec.md)
6+
7+
## Content Quality
8+
9+
- [x] No implementation details (languages, frameworks, APIs)
10+
- [x] Focused on user value and business needs
11+
- [x] Written for non-technical stakeholders
12+
- [x] All mandatory sections completed
13+
14+
## Requirement Completeness
15+
16+
- [x] No [NEEDS CLARIFICATION] markers remain
17+
- [x] Requirements are testable and unambiguous
18+
- [x] Success criteria are measurable
19+
- [x] Success criteria are technology-agnostic (no implementation details)
20+
- [x] All acceptance scenarios are defined
21+
- [x] Edge cases are identified
22+
- [x] Scope is clearly bounded
23+
- [x] Dependencies and assumptions identified
24+
25+
## Feature Readiness
26+
27+
- [x] All functional requirements have clear acceptance criteria
28+
- [x] User scenarios cover primary flows
29+
- [x] Feature meets measurable outcomes defined in Success Criteria
30+
- [x] No implementation details leak into specification
31+
32+
## Notes
33+
34+
- All items pass after one revision (removed C++ type names and code-level patterns from FRs and Key Entities).
35+
- Spec is ready for `/speckit.clarify` or `/speckit.plan`.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# JSON-RPC Contract: 019-perf-dedup-cleanup
2+
3+
**Date**: 2026-04-13
4+
5+
## Overview
6+
7+
This feature refactors the RPC dispatch mechanism. The JSON-RPC interface
8+
exposed to external clients does **not change** — all 20 methods retain
9+
identical request/response formats. This contract documents the existing
10+
interface for verification that the refactor is behavior-preserving.
11+
12+
## Method Registry
13+
14+
All methods below must be present in the dispatch table after refactoring.
15+
Each method returns a JSON-RPC 2.0 response.
16+
17+
| Method | Params Required | Success Response | Error Codes |
18+
|--------|----------------|------------------|-------------|
19+
| `publish` | `stream`, `key`; optional: `data`, `keys` | Block JSON | -32602 (invalid params), -32000 (mining timeout), -32001 (syncing), -32003 (stream not permitted) |
20+
| `createStream` | `name` | Stream name | -32602, -32000, -32001 |
21+
| `listStreams` | none | JSON array of streams ||
22+
| `getStreamEntries` | `stream` | JSON array of entries | -32602 |
23+
| `getStreamEntry` | `stream`, `key` | Entry JSON | -32602 |
24+
| `requestSync` | none | `"sync_started"` | -32002 (already syncing), -32003 (no peer) |
25+
| `getBlockByIndex` | `index` | Block JSON | -32602, -32001 (not found) |
26+
| `getBlocksByKeys` | `keys` | JSON array of blocks | -32602 |
27+
| `addPeer` | `host`, `port` | Success message | -32602 |
28+
| `removePeer` | `host`, `port` | Success message | -32602 |
29+
| `listPeers` | none | JSON array of peers ||
30+
| `banPeer` | `host`, `port`; optional: `reason`, `duration` | Success message | -32602 |
31+
| `unbanPeer` | `host`, `port` | Success message | -32602 |
32+
| `getInclusionProof` | `blockIndex`, `entryIndex` | Proof JSON | -32602 |
33+
| `verifyInclusionProof` | `proof` | Boolean result | -32602 |
34+
| `getBlockHeader` | `index` | Header JSON | -32602 |
35+
| `getNodeStatus` | none | Status JSON ||
36+
| `getBlockRange` | `start`, `end` | JSON array of blocks | -32602 |
37+
| `getChainLength` | none | Length string ||
38+
| `getChunkCount` | none | Count string ||
39+
40+
## Error Response Format
41+
42+
All error responses use the existing `errorMessage()` helper:
43+
44+
```json
45+
{
46+
"jsonrpc": "2.0",
47+
"error": {
48+
"code": -32601,
49+
"message": "Invalid method: unknownMethod"
50+
},
51+
"id": 1
52+
}
53+
```
54+
55+
Standard error codes:
56+
- `-32600`: Invalid JSON-RPC message
57+
- `-32601`: Method not found
58+
- `-32602`: Invalid parameters
59+
- `-32000`: Mining timeout / internal error
60+
- `-32001`: Block not found / sync in progress
61+
- `-32002`: Sync already in progress
62+
- `-32003`: No peer connected / stream not permitted
63+
64+
## Verification
65+
66+
After refactoring, every method in this table must produce byte-identical
67+
JSON responses for the same input. The existing `rpc_integration_tests`
68+
exercise the live RPC interface over SSL sockets and serve as the primary
69+
regression gate.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Data Model: Performance & Deduplication Cleanup
2+
3+
**Date**: 2026-04-13
4+
**Feature**: 019-perf-dedup-cleanup
5+
6+
## Modified Entities
7+
8+
### PeerEntry (existing — container change only)
9+
10+
No field changes. The storage container changes from `std::vector<PeerEntry>` to `std::unordered_map<std::string, PeerEntry>` keyed by `peer_key(host, port)`.
11+
12+
| Field | Type | Description |
13+
|-------|------|-------------|
14+
| host | string | Peer hostname/IP (normalized) |
15+
| port | uint16_t | Peer listen port |
16+
| node_uuid | string | Remote node UUID |
17+
| last_seen | uint64_t | Unix timestamp of last contact |
18+
| error_count | uint32_t | Consecutive error count |
19+
20+
**Key**: `peer_key(host, port)``"host:port"` string
21+
**Serialization**: JSON (to/from `peers.json`) — unchanged format
22+
**Validation**: Host is normalized via `normalize_address()` before key construction
23+
24+
### BanRecord (existing — container change only)
25+
26+
No field changes. The storage container changes from `std::vector<BanRecord>` to `std::unordered_map<std::string, BanRecord>` keyed by `peer_key(host, port)`.
27+
28+
| Field | Type | Description |
29+
|-------|------|-------------|
30+
| host | string | Banned peer hostname/IP |
31+
| port | uint16_t | Banned peer port |
32+
| reason | string | Ban reason |
33+
| expires | uint64_t | Unix timestamp; 0 = permanent |
34+
35+
**Key**: `peer_key(host, port)``"host:port"` string
36+
**Serialization**: JSON (to/from `peers.json`) — unchanged format
37+
**State transition**: `expires == 0` → permanent ban; `expires > 0 && expires <= now` → expired (removed by `purge_expired_bans()`)
38+
39+
## New Entities
40+
41+
### RPC Dispatch Table
42+
43+
A mapping from JSON-RPC method names to handler functions, stored as a private member of `RpcServer`.
44+
45+
| Field | Type | Description |
46+
|-------|------|-------------|
47+
| method_name | string (key) | JSON-RPC method name (e.g., "publish") |
48+
| handler | function | Callable returning JSON response |
49+
50+
**Type**: `std::unordered_map<std::string, std::function<nlohmann::json(const nlohmann::json&)>>`
51+
**Lifecycle**: Initialized once in `RpcServer` constructor; immutable thereafter
52+
**Cardinality**: Exactly 20 entries (one per existing RPC method)
53+
54+
### PacketSerializer (utility — no persistent state)
55+
56+
A header-only template function that serializes an object with Boost.Serialization and prepends a `PacketHeader`. No stored state — pure function.
57+
58+
**Input**: Serializable object of type T, packet type enum value
59+
**Output**: Pair of (header bytes, serialized payload string)
60+
**Wire format**: `[PacketHeader: 16 bytes][serialized payload: N bytes]` — identical to current format
61+
62+
## Relationships
63+
64+
```
65+
PeerManager
66+
├── peers_: unordered_map<string, PeerEntry> (was: vector<PeerEntry>)
67+
├── bans_: unordered_map<string, BanRecord> (was: vector<BanRecord>)
68+
└── peer_key(host, port) → string key (existing static helper)
69+
70+
RpcServer
71+
└── dispatch_: unordered_map<string, RpcHandler> (new)
72+
└── 20 handler methods (extracted from do_read)
73+
74+
PeerClient::send<T>() ──uses──▶ serialize_packet<T>() (PacketSerializer.hpp)
75+
PeerServer::send_packet<T>() ──uses──▶ serialize_packet<T>() (PacketSerializer.hpp)
76+
```
77+
78+
## On-Disk Format
79+
80+
No changes. `peers.json` continues to store peers as a JSON array and bans as a JSON array. The internal container type is transparent to the serialized format.

0 commit comments

Comments
 (0)