You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Clarify dependencies between framework source components #3517
At f0ff3f7, the source-folder include graph has 25 components and 91 direct internal edges: one 7-component cyclic core (common, consensus, endpoints, js, kv, node, service) and 18 acyclic components, all 18 with explicit outgoing dependency policies. #8398 took indexing out of the core and completed the planned internal_tables_access.h relocation; the production graph has not changed since that merge.
Suggested next cuts:
Finish common through or immediately after Unify operator and node startup configuration #8309 by removing the four node -> common witnesses, then add its policy. Projected 90 edges, 6 cyclic, 19 acyclic.
Coordinate with [WIP] Refactor Consensus API and KV Interaction #8190 to remove kv -> {node, service} by moving node/config integration and service-table-aware deserialisation out of KV core. This frees both kv and service. Projected, after the common cut: 88 edges, 4 cyclic, 21 acyclic.
Put the constitution runtime behind a node-facing interface to remove node -> js. Projected 87 edges, 3 cyclic, 22 acyclic, leaving the explicit node <-> consensus and node <-> endpoints seams.
The build-time dependency graph between components in CCF has grown messier over time. We originally had README.mds in some src/ folders indicating their intended dependencies, but these have rotted over time.
We should re-evaluate what dependencies we have and can enforce, to keep the code base navigable.
3rd party/STL libs are generally useable anywhere/visible everywhere (for instance nlohmann/json is used throughout the codebase, including in core containers), with a few exceptions: We should limit visibility of anything platform-specific (including SGX and host-only libraries).
ds/ should not depend on anything else. This is a place for generic containers and shims that may be used by multiple other components.
We should aim to have a set of pure components which depend only on ds/. For instance crypto/, tls/, and http/ could be written like this - generic protocol implementations and helpers, stripped of platform-integration features.
Where we have tightly-coupled systems like kv/ and consensus/, we should try to document this coupling to ensure it is required.
The repo currently contains 3 READMEs which claim the following dependencies:
ds/README.md
# Data Structures
This directory contains data structures and utilities used by the rest of the code. No files
in this directory should include files from any other directory.
kv/README.md
# Transactional Key Value Store
This directory implements a transactional key value store. The only directory
it should depend on is `ds`.
crypto/README.md
# Crypto
This directory implements several cryptography helper types. The only directory
it should depend on is `ds`.
None of these dependencies are currently enforced. The actual dependencies between the folders under src/ look like this:
Dotted lines show mutual/circular dependencies. Initial thoughts:
We're aware of the mutual dependency between kv/ and consensus/, and believe it's extremely hard to break
We should try to break the dependency from ds/ to crypto/
A surprising number of things rely directly on crypto/, I wonder if it's worth splitting this
This shows we do already have a few 'clean' components at the bottom of the tree, which are only consuming other libraries (clients/, host/, apps/), we should enforce that distinction while we have it
This is the same source-folder view as the original diagram: each node is a top-level directory under src/ containing C/C++ source or headers, and A -> B means that a non-test source/header in src/A directly includes a header owned by B (through src/B, include/ccf/B, or a relative path). Tests, benchmarks, fuzzers, and third-party/STL dependencies are omitted. Dashed arrows participate in a dependency cycle; solid arrows do not. The subgraph groupings are only for readability, not dependency layers.
flowchart TB
subgraph consumers["Acyclic consumers"]
direction LR
apps["apps/"]
clients["clients/"]
end
subgraph runtime["Node and execution"]
direction LR
host["host/"]
enclave["enclave/"]
node["node/"]
endpoints["endpoints/"]
js["js/"]
indexing["indexing/"]
snapshots["snapshots/"]
end
subgraph state["Replicated state and service"]
direction LR
consensus["consensus/"]
kv["kv/"]
service["service/"]
end
subgraph protocols["Protocols and transport"]
direction LR
http["http/"]
tls["tls/"]
quic["quic/"]
tcp["tcp/"]
udp["udp/"]
end
subgraph support["Foundation and support"]
direction LR
common["common/"]
crypto["crypto/"]
ds["ds/"]
pal["pal/"]
tasks["tasks/"]
cose["cose/"]
end
apps --> js
clients --> crypto
clients --> ds
clients --> http
clients --> tls
common -.-> consensus
common -.-> crypto
common -.-> ds
common -.-> node
common -.-> pal
common -.-> service
consensus -.-> crypto
consensus -.-> ds
consensus -.-> enclave
consensus -.-> kv
consensus -.-> node
consensus -.-> pal
consensus -.-> service
crypto --> cose
crypto -.-> ds
ds -.-> crypto
ds -.-> pal
ds -.-> service
enclave -.-> common
enclave -.-> crypto
enclave -.-> ds
enclave -.-> host
enclave -.-> http
enclave -.-> indexing
enclave -.-> js
enclave -.-> kv
enclave -.-> node
enclave -.-> pal
enclave -.-> quic
enclave -.-> service
enclave -.-> tasks
enclave -.-> tcp
enclave -.-> tls
enclave -.-> udp
endpoints -.-> crypto
endpoints -.-> ds
endpoints -.-> http
endpoints -.-> kv
endpoints -.-> node
endpoints -.-> pal
endpoints -.-> service
host -.-> common
host -.-> consensus
host -.-> crypto
host -.-> ds
host -.-> enclave
host -.-> http
host -.-> indexing
host -.-> kv
host -.-> node
host -.-> pal
host -.-> service
host -.-> snapshots
host -.-> tcp
host -.-> udp
http -.-> crypto
http -.-> ds
http -.-> enclave
http -.-> host
http -.-> node
indexing -.-> crypto
indexing -.-> ds
indexing -.-> kv
indexing -.-> node
indexing -.-> pal
js -.-> crypto
js -.-> ds
js -.-> endpoints
js -.-> kv
js -.-> node
js -.-> pal
js -.-> service
js -.-> tls
kv -.-> crypto
kv -.-> ds
kv -.-> node
kv -.-> pal
kv -.-> service
node -.-> common
node -.-> consensus
node --> cose
node -.-> crypto
node -.-> ds
node -.-> enclave
node -.-> endpoints
node -.-> host
node -.-> http
node -.-> indexing
node -.-> js
node -.-> kv
node -.-> pal
node -.-> service
node -.-> snapshots
node -.-> tasks
pal -.-> crypto
pal -.-> ds
quic -.-> ds
quic -.-> enclave
quic -.-> udp
service -.-> consensus
service --> cose
service -.-> crypto
service -.-> ds
service -.-> kv
service -.-> node
service -.-> pal
snapshots -.-> consensus
snapshots -.-> ds
snapshots -.-> host
snapshots -.-> http
tasks -.-> ds
tcp -.-> ds
tls -.-> crypto
tls -.-> ds
udp -.-> ds
Loading
At this revision there are 23 components and 120 direct internal dependency edges. apps/ and clients/ are the only acyclic consumers, cose/ is an acyclic sink, and the other 20 components form one strongly connected component. In particular, the original intended boundaries still do not hold: ds/ directly depends on crypto/, pal/, and service/; kv/ directly depends on crypto/, ds/, node/, pal/, and service/; and crypto/ directly depends on ds/ and cose/. Unlike the 2022 graph, host/ is no longer a clean outer consumer; it is now part of the cyclic core.
Rerun against main at 09568fb (2026-09-04), using the deterministic include analysis now implemented by scripts/check-source-dependencies.py and the same production-source exclusions as the previous updates.
There are now 23 components and 104 direct internal edges. The remaining cyclic core is a single 12-component SCC:
The other 11 components are acyclic: apps, cose, crypto, ds, msgpack, pal, tasks, tcp, threading, tls, and udp.
Compared with the 2026-09-02 baseline at 9751a52:
Break the crypto, ds, and pal dependency cycle #8265 removed eight edges into pal, including the last outgoing edge from ds. This eliminated the separate crypto -> ds -> pal -> crypto cycle. The resulting enforced foundation is ds -> {}, crypto -> {cose, ds}, and pal -> {crypto, ds}.
Break the crypto, ds, and pal dependency cycle #8265 also added the dependency checker to scripts/includes-checks.sh. It currently enforces declared dependencies for crypto, ds, and pal; it does not yet prove that the full repository is a DAG.
Remove experimental QUIC support #8271 removed the experimental quic component and its four graph edges, reducing the main SCC from 13 components to 12.
Overall, the graph moved from 24 components / 116 edges / 16 cyclic components to 23 components / 104 edges / 12 cyclic components.
Dashed arrows below participate in the remaining cycle; solid arrows do not.
flowchart TB
subgraph consumers["Acyclic consumers"]
apps["apps/"]
end
subgraph runtime["Node and execution"]
host["host/"]
enclave["enclave/"]
node["node/"]
endpoints["endpoints/"]
js["js/"]
indexing["indexing/"]
snapshots["snapshots/"]
end
subgraph state["Replicated state and service"]
consensus["consensus/"]
kv["kv/"]
service["service/"]
end
subgraph protocols["Protocols and transport"]
http["http/"]
tls["tls/"]
tcp["tcp/"]
udp["udp/"]
end
subgraph support["Foundation and support"]
common["common/"]
crypto["crypto/"]
ds["ds/"]
pal["pal/"]
tasks["tasks/"]
cose["cose/"]
msgpack["msgpack/"]
threading["threading/"]
end
apps --> js
common -.-> consensus
common --> crypto
common --> ds
common -.-> node
common --> pal
common -.-> service
consensus --> crypto
consensus --> ds
consensus -.-> enclave
consensus -.-> kv
consensus -.-> node
consensus -.-> service
crypto --> cose
crypto --> ds
enclave -.-> common
enclave --> crypto
enclave --> ds
enclave -.-> host
enclave -.-> http
enclave -.-> indexing
enclave -.-> js
enclave -.-> kv
enclave -.-> node
enclave -.-> service
enclave --> tasks
enclave --> tcp
enclave --> tls
enclave --> udp
endpoints --> crypto
endpoints --> ds
endpoints -.-> http
endpoints -.-> kv
endpoints -.-> node
endpoints -.-> service
host -.-> common
host -.-> consensus
host --> crypto
host --> ds
host -.-> enclave
host -.-> http
host -.-> indexing
host -.-> kv
host -.-> node
host --> pal
host -.-> service
host -.-> snapshots
host --> tcp
host --> udp
http --> crypto
http --> ds
http -.-> enclave
http -.-> host
http -.-> node
indexing --> crypto
indexing --> ds
indexing -.-> kv
indexing -.-> node
js --> crypto
js --> ds
js -.-> endpoints
js -.-> kv
js -.-> node
js --> pal
js -.-> service
js --> tls
kv --> crypto
kv --> ds
kv -.-> node
kv -.-> service
node -.-> common
node -.-> consensus
node --> cose
node --> crypto
node --> ds
node -.-> enclave
node -.-> endpoints
node -.-> host
node -.-> http
node -.-> indexing
node -.-> js
node -.-> kv
node --> pal
node -.-> service
node -.-> snapshots
node --> tasks
pal --> crypto
pal --> ds
service -.-> consensus
service --> cose
service --> crypto
service --> ds
service -.-> kv
service -.-> node
service --> pal
snapshots -.-> consensus
snapshots --> ds
snapshots -.-> host
snapshots -.-> http
tasks --> ds
tcp --> ds
tls --> crypto
tls --> ds
udp --> ds
2026-09-09: HTTP is acyclic; snapshots should be next
Current main is ab11900. The source dependency inputs are unchanged from the locally analysed 7b31f9e: the intervening #8277 adds the Lean model and associated metadata, not C/C++ dependencies.
Current state
Revision / proposed step
Source components
Direct source-component edges
Components in cycles
Acyclic components
Components with an outgoing dependency policy
2026-09-04, 09568fb
23
104
12
11
3
2026-09-09, ab11900
25
106
11
14
14
Proposed snapshots cut, against this baseline
25
104
10
15
15
There is now one 11-component strongly connected component (SCC):
All 14 currently acyclic components now have explicit outgoing dependency policies. This is progress toward a component DAG, not a claim that the repository is already a DAG.
Compared with September 4, the four removed source-component edges are host -> http and http -> {enclave, host, node}. The six added edges are host -> {http_client, uv}, http_client -> {ds, uv}, node -> http_client, and snapshots -> http_client. The net increase of two edges reflects the new reusable components, not a new cycle.
consensus::Index in the two persist_snapshot index parameters; no consensus operations
Do both cuts together. Removing either edge alone leaves all 11 components cyclic. No single-edge deletion in the current graph reduces the number of cyclic components. Among the two-edge cuts that do, this pair has the fewest direct include witnesses: three. That is an ownership signal, not an estimate that the complete patch is only three lines.
Move TimeBoundLogger from src/host/time_bound_logger.h to src/ds/time_bound_logger.h. Give the generic helper an appropriate ccf::ds namespace and update all host and snapshot consumers. Its dependencies are already just ds, the standard library, and fmt. Preserve the single shared default_max_time, including the slow_io_logging_threshold assignment in host/run.cpp, and preserve formatting and logging thresholds. Do not duplicate the helper or introduce a ds -> host forwarding include.
Replace the writer's consensus::Index parameters with the existing ccf::SeqNo from ccf/tx_id.h. Both are exactly uint64_t today: Index, SeqNo. Remove the ledger ringbuffer header from snapshot_writer.h; do not move consensus machinery into ds, create another sequence-number type, or change snapshot bytes/filenames.
Enforce the resulting boundary in scripts/source-dependencies.json:"snapshots": ["ccf-api", "ds", "http", "http_client"]. Keep the existing foundation policies unchanged. Resolve any missing direct includes exposed by the moves, rather than relying on unrelated headers to make them compile.
Keep the PR structural and demonstrate the intended boundary. Add standalone-header compilation coverage using the existing unit-target pattern, plus focused coverage for the relocated timing helper. Build the host/runtime consumers and the affected logger_test, ledger_test, files_cleanup_test, indexing_test, and snapshotter_test targets. Run those unit tests and the existing recovery_test e2e path. Preserve the writer's exclusive creation, complete writes, fsync/rename sequence, and failure handling, and retain the snapshotter's commit/rollback behaviour. Run the include/dependency and applicable formatting/ASCII checks before pushing.
Acceptance criteria: no production include from snapshots into host or consensus; snapshots has only the declared outgoing dependencies; the foundation policies still hold; and the recomputed source graph has 15 acyclic components and one 10-component SCC. At this baseline the source edge count should fall from 106 to 104. These are projections for the proposed change, not landed results.
This is preferable to another isolated one-include cleanup such as moving HistoricalTransactionFetcher: removing indexing -> node alone still leaves indexing -> kv -> node -> indexing, so it does not shrink the SCC. It also avoids bundling the broader configuration, RPC, and KV/consensus work already in flight in #8309, #8117, and #8190.
Full refreshed source-component graph (25 components, 106 edges)
As in the previous updates, A -> B means a production source/header in src/A directly includes a header owned by B. Dashed arrows participate in the remaining SCC; solid arrows do not. Groupings are only for readability.
flowchart TB
subgraph consumers["Acyclic consumers"]
apps["apps/"]
end
subgraph runtime["Node and execution"]
host["host/"]
enclave["enclave/"]
node["node/"]
endpoints["endpoints/"]
js["js/"]
indexing["indexing/"]
snapshots["snapshots/"]
end
subgraph state["Replicated state and service"]
consensus["consensus/"]
kv["kv/"]
service["service/"]
end
subgraph protocols["Protocols and transport"]
http["http/"]
http_client["http_client/"]
tls["tls/"]
tcp["tcp/"]
udp["udp/"]
end
subgraph support["Foundation and support"]
common["common/"]
crypto["crypto/"]
ds["ds/"]
pal["pal/"]
tasks["tasks/"]
cose["cose/"]
msgpack["msgpack/"]
threading["threading/"]
uv["uv/"]
end
apps --> js
common -.-> consensus
common --> crypto
common --> ds
common -.-> node
common --> pal
common -.-> service
consensus --> crypto
consensus --> ds
consensus -.-> enclave
consensus -.-> kv
consensus -.-> node
consensus -.-> service
crypto --> cose
crypto --> ds
enclave -.-> common
enclave --> crypto
enclave --> ds
enclave -.-> host
enclave --> http
enclave -.-> indexing
enclave -.-> js
enclave -.-> kv
enclave -.-> node
enclave -.-> service
enclave --> tasks
enclave --> tcp
enclave --> tls
enclave --> udp
endpoints --> crypto
endpoints --> ds
endpoints --> http
endpoints -.-> kv
endpoints -.-> node
endpoints -.-> service
host -.-> common
host -.-> consensus
host --> crypto
host --> ds
host -.-> enclave
host --> http_client
host -.-> indexing
host -.-> kv
host -.-> node
host --> pal
host -.-> service
host -.-> snapshots
host --> tcp
host --> udp
host --> uv
http --> crypto
http --> ds
http_client --> ds
http_client --> uv
indexing --> crypto
indexing --> ds
indexing -.-> kv
indexing -.-> node
js --> crypto
js --> ds
js -.-> endpoints
js -.-> kv
js -.-> node
js --> pal
js -.-> service
js --> tls
kv --> crypto
kv --> ds
kv -.-> node
kv -.-> service
node -.-> common
node -.-> consensus
node --> cose
node --> crypto
node --> ds
node -.-> enclave
node -.-> endpoints
node -.-> host
node --> http
node --> http_client
node -.-> indexing
node -.-> js
node -.-> kv
node --> pal
node -.-> service
node -.-> snapshots
node --> tasks
pal --> crypto
pal --> ds
service -.-> consensus
service --> cose
service --> crypto
service --> ds
service -.-> kv
service -.-> node
service --> pal
snapshots -.-> consensus
snapshots --> ds
snapshots -.-> host
snapshots --> http
snapshots --> http_client
tasks --> ds
tcp --> ds
tls --> crypto
tls --> ds
udp --> ds
Loading
Measurement scope
This uses the scanner functions in scripts/check-source-dependencies.py and the exclusions/generated-header list in scripts/source-dependencies.json, followed by SCC analysis. It retains the previous updates' source-folder projection: nodes are top-level src components containing production C/C++ sources/headers; tests, benchmarks, and fuzz sources are excluded; third-party/STL dependencies and public-only component targets are omitted from the displayed graph.
Public headers under include/ccf are resolved as include targets, but their own contents are not separately scanned as source vertices. In particular, ccf-api appears in policies but is not a node in this source-folder projection; the other public-only target encountered here is research. This is not a linker/build-target or complete transitive public-header graph.
The existing checker enforces configured outgoing dependency allowlists through scripts/includes-checks.sh; it does not currently emit this graph or prove global acyclicity. The full DAG goal therefore remains open.
Assuming #8338 merges, the graph below is measured on GitHub's synthetic merge 5e32dfb: main at 31f5f9f plus PR head cb954b1. This is a projection, not a claim that the PR has landed.
The only removed source-component edges are snapshots -> host and snapshots -> consensus; none are added. snapshots leaves the cyclic core, with the enforced boundary snapshots -> {ccf-api, ds, http, http_client}. All 15 acyclic components have outgoing dependency policies.
Legend: arrows run from an includer to its dependency. Dashed arrows participate in the remaining cycle; solid arrows do not. Red nodes and red arrows mark the next candidate ownership boundaries to address, not additional changes assumed in #8338. Green marks snapshots, acyclic under the merge assumption. Groupings are for readability, not dependency layers.
flowchart LR
apps["apps/"]
snapshots["snapshots/ (post-8338)"]
subgraph core["Remaining cyclic core (10)"]
direction LR
common["common/"]
consensus["consensus/"]
enclave["enclave/"]
endpoints["endpoints/"]
host["host/"]
indexing["indexing/"]
js["js/"]
kv["kv/"]
node["node/"]
service["service/"]
end
subgraph protocols["Acyclic protocols and transport"]
direction LR
http["http/"]
http_client["http_client/"]
tls["tls/"]
tcp["tcp/"]
udp["udp/"]
end
subgraph support["Acyclic foundation and support"]
direction LR
crypto["crypto/"]
ds["ds/"]
pal["pal/"]
tasks["tasks/"]
cose["cose/"]
msgpack["msgpack/"]
threading["threading/"]
uv["uv/"]
end
apps --> js
common -.-> consensus
common --> crypto
common --> ds
common -.-> node
common --> pal
common -.-> service
consensus --> crypto
consensus --> ds
consensus -.-> enclave
consensus -.-> kv
consensus -.-> node
consensus -.-> service
crypto --> cose
crypto --> ds
enclave -.-> common
enclave --> crypto
enclave --> ds
enclave -.-> host
enclave --> http
enclave -.-> indexing
enclave -.-> js
enclave -.-> kv
enclave -.-> node
enclave -.-> service
enclave --> tasks
enclave --> tcp
enclave --> tls
enclave --> udp
endpoints --> crypto
endpoints --> ds
endpoints --> http
endpoints -.-> kv
endpoints -.-> node
endpoints -.-> service
host -.-> common
host -.-> consensus
host --> crypto
host --> ds
host -.-> enclave
host --> http_client
host -.-> indexing
host -.-> kv
host -.-> node
host --> pal
host -.-> service
host --> snapshots
host --> tcp
host --> udp
host --> uv
http --> crypto
http --> ds
http_client --> ds
http_client --> uv
indexing --> crypto
indexing --> ds
indexing -.-> kv
indexing -.-> node
js --> crypto
js --> ds
js -.-> endpoints
js -.-> kv
js -.-> node
js --> pal
js -.-> service
js --> tls
kv --> crypto
kv --> ds
kv -.-> node
kv -.-> service
node -.-> common
node -.-> consensus
node --> cose
node --> crypto
node --> ds
node -.-> enclave
node -.-> endpoints
node -.-> host
node --> http
node --> http_client
node -.-> indexing
node -.-> js
node -.-> kv
node --> pal
node -.-> service
node --> snapshots
node --> tasks
pal --> crypto
pal --> ds
service -.-> consensus
service --> cose
service --> crypto
service --> ds
service -.-> kv
service -.-> node
service --> pal
snapshots --> ds
snapshots --> http
snapshots --> http_client
tasks --> ds
tcp --> ds
tls --> crypto
tls --> ds
udp --> ds
classDef opportunity fill:#ffebe9,stroke:#cf222e,stroke-width:3px,color:#82071e
classDef extracted fill:#dafbe1,stroke:#1a7f37,stroke-width:2px,color:#116329
class host,indexing opportunity
class snapshots extracted
linkStyle 18,56,57,77 stroke:#cf222e,stroke-width:3px
Loading
Next targets in red
Priority
Target
Cyclic edges to address together
Direct include witnesses
1
host: restore an outer consumer
enclave -> host, node -> host
5
2
indexing: separate runtime adapters and the KV implementation
indexing -> node, indexing -> kv
5
Start with host. Its remaining incoming dependencies from the cyclic core are all ledger-related: three includes of host/ledger.h in the enclave entry/construction path, plus ReadLedgerSubsystem and recovery's ledger filename helpers in node. Inject the existing AbstractReadLedgerSubsystemInterface instead of the concrete ledger at the enclave entry point; keep the concrete adapter beside the host-owned ledger, and give the shared ledger filename helpers a lower-level, cycle-free ledger-format owner. Remove both back-edges without introducing replacement cycles, preserving ledger lifetime and recovery behaviour. This would take host out of the SCC; the existing host -> node/enclave/... consumer dependencies can remain.
Then investigate indexing's boundary. Move HistoricalTransactionFetcher beside its node-owned historical cache, and replace unnecessarily broad private KV includes in indexer.h, seqnos_by_key_bucketed.cpp, and visit_each_entry_in_map.cpp with the narrow interfaces they actually use. This is not an automatic SCC win: direct includes of public ccf/kv/... headers still count as kv in this graph. A genuine acyclic boundary requires either a separately owned lower-level KV API or first removing kv's own back-edges to node and service. Do not hide the dependency through transitive includes. Moving the historical fetcher alone leaves indexing -> kv -> node -> indexing.
No single-edge deletion shrinks the remaining 10-component SCC. The two highlighted pairs are the lowest-include-count two-edge cuts that do so in the measured graph, but include count is not implementation cost. Keep the broader configuration and KV/consensus work coordinated with #8309 and #8190; neither, nor #8117, is assumed merged here. Recompute the graph and enforce the resulting boundary after each real extraction.
Measurement scope
Same source-folder projection as the preceding updates, using check-source-dependencies.py and its policy/exclusion file, followed by strongly connected component analysis. Nodes are top-level src components with production C/C++ files. Public headers are resolved as include targets, but their bodies are not scanned as source vertices; public-only ccf-api and research targets, third-party/STL dependencies, tests, benchmarks, and fuzzers are omitted. This is not a complete transitive public-header or build-target graph, and the full DAG goal remains open.
The remaining 8-component SCC is common, consensus, endpoints, indexing, js, kv, node, service.
The 17 acyclic components are apps, cose, crypto, ds, enclave, host, http, http_client, ledger, msgpack, pal, snapshots, tasks, tcp, threading, tls, uv. All 17 have explicit outgoing policies in scripts/source-dependencies.json. The full DAG goal remains open.
A note on numbers: the descriptions of #8389 and #8396 report 26 components and 114 -> 110 edges because they also count the 14 include edges into the public-only ccf-api and research targets. This update keeps the issue's projection, in which those targets are resolved but not displayed: 96 edges (110 if they are included). The two views agree on the SCC.
What changed structurally
With enclave gone, node is the only core component that still depends on common, indexing, or js. In the previous updates no single-edge deletion shrank the SCC; now there are three:
Each of these turns the freed component into an outer consumer of the core, exactly as #8389 and #8396 did for host and enclave. It does not make them lower-level "pure" components; that remains the goal for kv and service, see below.
Remaining core edges, with direct include witness counts (27 edges):
Move 1 (now): take indexing out through node -> indexing
The single witness is node_state.h:34 including indexing/indexer.h. NodeState uses the Indexer for exactly one thing: tick() calls indexer->update_strategies(elapsed, committed_txid) while the node is partOfNetwork. The Indexer is constructed, installed as a subsystem, and owned by the composition root in enclave.h, whose tick handler already ticks the sibling historical_state_cache immediately after node->tick(elapsed_ms).
Drive Indexer::update_strategies from the composition root. Remove the indexer_ parameter from NodeState::initialize and the indexer member. In the enclave tick handler, directly after node->tick(elapsed_ms) (so still after consensus->periodic), call indexer->update_strategies(elapsed_ms, committed) gated on the existing node->is_part_of_network(). Expose the committed TxID from NodeState with a small accessor rather than reaching into consensus from enclave.h. Do not introduce a node-owned observer interface that Indexer implements: that would keep the pair coupled in the other direction and complicate a later lower-level indexing. The ordering relative to n2n_channels->tick changes, which does not affect the committed index.
Enforce the boundary:"indexing": ["ccf-api", "crypto", "ds", "kv", "node", "tasks"], which is exactly the measured outgoing set. indexing -> node (the historical fetcher adapter) and indexing -> kv (transaction deserialisation in the strategies) remain as ordinary consumer dependencies. The 09-13 idea of making indexing lower than kv stays blocked on kv's own back-edges and is not needed for this cut.
Validation: build the enclave/node targets and indexing_test; run indexing_test and the e2e_logging.py indexing paths (test_committed_index, test_historical_query_range*, test_historical_query_sparse), which exercise SeqnosByKey_Bucketed end to end. Recompute the graph and run the include/dependency, formatting and ASCII checks before pushing.
Acceptance: no production include from node into indexing; the recomputed graph has 95 edges, one 7-component SCC and 18 acyclic components, all with policies. Projection, not a landed result.
Move 2 (with or straight after #8309): take common out through node -> common
All four witnesses are common/configuration.h: history.h:11 and rpc/frontend.h:16 use nothing from it; node_call_types.h:80 needs the JSON declarations for StartupConfig::Start (the /node/create body in rpc/serialization.h); node_interface.h:61 needs only the public ccf/node/startup_config.h. node_state.h also picks up StartType transitively through this header.
#8309 (draft, "Unify operator and node startup configuration") rewrites exactly this header: it introduces public ccf/node/configuration.h and ccf/node/start_type.h, but currently keeps the JSON declarations in src/common/configuration.h and keeps node including it, so it would leave node -> common in place. The cut is therefore best folded into #8309 or done immediately after it:
Place the DECLARE_JSON_* declarations for the public configuration types next to the types (public header, as include/ccf/service/* already does) or in a node-owned header, so that node never includes common/configuration.h; drop the two stale includes.
Enforce "common": ["ccf-api", "consensus", "crypto", "ds", "node", "pal", "service"]. common then becomes what its consumers say it is: host/enclave shared configuration and ringbuffer types, consumed only by host and enclave.
Acceptance: with move 1, 92 edges, one 6-component SCC (consensus, endpoints, js, kv, node, service), 19 acyclic components with policies.
Prep that can land any time: relocate service/internal_tables_access.h to node/
internal_tables_access.h is the only source of service -> node (3 witnesses: node/history.h, node/ledger_secrets.h, node/uvm_endorsements.h) and service -> consensus (1: consensus/aft/raft_types.h). Its eight includers are all under src/node/. Moving it removes both edges and adds none (96 -> 94 alone). It does not shrink the SCC by itself, because service -> kv -> service remains, but it leaves service -> kv as service's only core out-edge, so service falls out automatically once kv does. Neither #8190 nor #8309 touches this file.
After that: make kv pure, then js
kv's only core out-edges are kv -> node (4: node/rpc/claims.h from committable_tx.h and generic_serialise_wrapper.h; ccf/node/{cose_signatures_config,startup_config}.h from kv_types.h) and kv -> service (5: service/tables/{shares,signatures,snapshot_evidence}.h from deserialise.h; ccf/service/{consensus_type,reconfiguration_type}.h from kv_types.h). Removing those nine witnesses, together with the prep above, would free kvandservice, leaving a 4-component core (consensus, endpoints, js, node). This is the KV/consensus coupling the original issue asked to justify or remove, and it overlaps #8190 (kv_types.h, store.h), so it should be coordinated with that work rather than raced. node -> js (14 witnesses across governance, js_policy.h and the frontends) is the remaining single-edge cut, but it needs the constitution runtime behind an interface and is the most invasive of the three; it should wait until kv and service are settled.
Full refreshed source-component graph (25 components, 96 edges)
Legend: arrows run from an includer to its dependency. Dashed arrows participate in the remaining cycle; solid arrows do not. Green marks components that became acyclic since the last update (host, enclave, and the new ledger and tls). Red nodes and red arrows mark the cuts proposed above, not landed changes. Groupings are for readability, not dependency layers.
flowchart LR
subgraph roots["Acyclic composition roots and consumers"]
direction LR
host["host/"]
enclave["enclave/"]
apps["apps/"]
end
subgraph core["Remaining cyclic core (8)"]
direction LR
common["common/"]
consensus["consensus/"]
endpoints["endpoints/"]
indexing["indexing/"]
js["js/"]
kv["kv/"]
node["node/"]
service["service/"]
end
subgraph protocols["Acyclic protocols, transport and on-disk formats"]
direction LR
http["http/"]
http_client["http_client/"]
tls["tls/"]
tcp["tcp/"]
snapshots["snapshots/"]
ledger["ledger/"]
end
subgraph support["Acyclic foundation and support"]
direction LR
crypto["crypto/"]
ds["ds/"]
pal["pal/"]
tasks["tasks/"]
cose["cose/"]
msgpack["msgpack/"]
threading["threading/"]
uv["uv/"]
end
apps --> js
common -.-> consensus
common --> crypto
common --> ds
common -.-> node
common --> pal
common -.-> service
consensus --> crypto
consensus --> ds
consensus -.-> kv
consensus -.-> node
consensus -.-> service
crypto --> cose
crypto --> ds
enclave --> common
enclave --> crypto
enclave --> ds
enclave --> indexing
enclave --> js
enclave --> kv
enclave --> node
enclave --> tasks
endpoints --> crypto
endpoints --> ds
endpoints --> http
endpoints -.-> kv
endpoints -.-> node
endpoints -.-> service
host --> common
host --> consensus
host --> crypto
host --> ds
host --> enclave
host --> http_client
host --> kv
host --> ledger
host --> node
host --> pal
host --> service
host --> snapshots
host --> uv
http --> crypto
http --> ds
http_client --> ds
http_client --> uv
indexing --> crypto
indexing --> ds
indexing -.-> kv
indexing -.-> node
indexing --> tasks
js --> crypto
js --> ds
js -.-> endpoints
js -.-> kv
js -.-> node
js --> pal
js -.-> service
kv --> crypto
kv --> ds
kv -.-> node
kv -.-> service
node -.-> common
node -.-> consensus
node --> cose
node --> crypto
node --> ds
node -.-> endpoints
node --> http
node --> http_client
node -.-> indexing
node -.-> js
node -.-> kv
node --> ledger
node --> pal
node -.-> service
node --> snapshots
node --> tasks
node --> tcp
node --> tls
pal --> crypto
pal --> ds
service -.-> consensus
service --> cose
service --> crypto
service --> ds
service -.-> kv
service -.-> node
service --> pal
snapshots --> ds
snapshots --> http
snapshots --> http_client
tasks --> ds
tls --> crypto
tls --> ds
tls --> tasks
tls --> tcp
classDef opportunity fill:#ffebe9,stroke:#cf222e,stroke-width:3px,color:#82071e
classDef extracted fill:#dafbe1,stroke:#1a7f37,stroke-width:2px,color:#116329
class indexing,common opportunity
class host,enclave,ledger,tls extracted
linkStyle 61,69,81,86 stroke:#cf222e,stroke-width:3px
Loading
Measurement scope
Same source-folder projection as the preceding updates: the scanner functions in scripts/check-source-dependencies.py and the exclusions and generated-header list in scripts/source-dependencies.json, followed by strongly connected component analysis. Nodes are top-level src components containing production C/C++ sources or headers; tests, benchmarks and fuzzers are excluded. Public headers under include/ccf are resolved as include targets but not scanned as source vertices; the public-only ccf-api and research targets and third-party/STL dependencies are omitted from the displayed graph. Single- and two-edge cut candidates were enumerated over the 27 core edges; include witness counts are an ownership signal, not an implementation cost estimate. The checker enforces the configured allowlists through scripts/includes-checks.sh; it does not emit this graph or prove global acyclicity.
2026-09-18: indexing is acyclic; the core is down to 7 components
Measured on main at f0ff3f7, using the same production source-folder projection as the previous updates (so the ccf-api policy pseudo-component is not counted as a source-folder edge).
Metric
At efc44e3
Current
Source components
25
25
Direct internal edges
96
91
Cyclic components
8
7
Acyclic components
17
18
Components with explicit policies
17
18
#8398 has landed. It removed node -> indexing and moved service/internal_tables_access.h to node/, removing service -> {consensus, node, cose, pal}. indexing now has an explicit policy and is outside the remaining SCC. The production include graph has not changed since that merge through f0ff3f7.
The remaining cyclic core is:
common, consensus, endpoints, js, kv, node, service
Suggested next cuts
Finish common through Unify operator and node startup configuration #8309. Remove the four remaining node -> common witnesses, all includes of common/configuration.h, by keeping public configuration JSON declarations with the public/node-owned types and removing the two stale includes. Then add the measured common policy. On today's graph, that direct cut gives 90 edges and a 6-component SCC. Since Unify operator and node startup configuration #8309 rewrites these same headers, this should be folded into that PR or land immediately after it, not raced independently.
move RPC claims and startup/config integration out of KV core;
move service-table-aware ledger deserialisation out of kv/deserialise.h into the node/service integration layer.
With the common cut, removing those two direct edges frees both kv and service: projected 88 edges and a 4-component SCC (consensus, endpoints, js, node). This is the highest-leverage next tranche and should add policies for both components.
Put the constitution runtime behind a node-facing interface. Removing node -> js then makes js acyclic: projected 87 edges and a 3-component SCC (consensus, endpoints, node). At that point the remaining work is two explicit seams rather than a general tangle: node <-> consensus and node <-> endpoints.
These projections assume no unrelated source-folder edges are added; each landing PR should rerun the SCC analysis and add the newly valid policy in scripts/source-dependencies.json.
2026-09-28: common is acyclic; five back-edges now separate us from a DAG
Measured on main at ff3966f (#8455), with the same production source-folder projection as the previous updates (ccf-api and research resolved but not shown).
New rust component (app_bridge.cpp), born acyclic but without a policy
Nothing else changed the component graph; other merges only moved witness counts (e.g. node -> service 103 -> 106, enclave -> node 23 -> 27). The open PRs touching core files that I checked (#8405, #8445, #8446, #8433, #8423) do not add or remove core edges; #8190 (draft, last updated 2026-08-24) overlaps the kv work below.
Core edges (17), with direct include witness counts
From \ to
consensus
endpoints
js
kv
node
service
consensus
5
6
5
endpoints
1
7
12
js
7
4
9
4
kv
4
5
node
9
4
14
25
106
service
7
Target layering: node on top
Enumerating all 720 orderings of the six core components, the cheapest feedback arc set is 5 edges / 31 witnesses (bold above), giving the layering
kv < service < {consensus, endpoints} < js < node
i.e. node is the integration layer that owns everything else, which matches how enclave and host already consume it. The runner-up (33 witnesses) instead cuts node -> endpoints and node -> js, putting endpoints/js above node; that would require node to reach its own auth policies and the constitution runtime through injected interfaces, which is more invasive than relocating a handful of node-owned headers and governance extensions. I'd therefore revise suggestion 3 from the last update: rather than hiding the constitution runtime behind a node-facing interface (removing node -> js), move the governance-specific pieces out of js and remove js -> node.
Applying the cuts in order (projections, assuming nothing else changes):
Step
Edges removed
Witnesses
Edges
Cyclic
Acyclic
now
88
6
20
1
kv -> node, kv -> service
9
86
4
22
2
consensus -> node
6
85
3
23
3
endpoints -> node
7
84
2
24
4
js -> node
9
83
0
26
Steps 3 and 4 are order-sensitive: removing js -> node first frees nothing, because js -> endpoints -> node remains.
Suggested next steps
0. Housekeeping (small, independent).
Add "rust": ["ccf-api", "kv"] to source-dependencies.json. rust is the only acyclic component without a policy. Its private-KV witness is kv/compacted_version_conflict.h, also used by js and node; worth deciding whether that exception type belongs in the public ccf/kv API.
Make the checker ratchet toward a DAG: have check-source-dependencies.py compute SCCs and fail if the cyclic set grows beyond an allowlist (currently the six above), shrinking the allowlist with each cut. Today it only enforces per-component allowlists, so a new cycle through unpoliced components would go unnoticed.
node_state.h:76 includes kv/test/null_encryptor.h, a test header, from production code.
Apparently stale includes that inflate core witnesses: consensus/aft/raft_types.h:10 includes node/rpc/rpc_handler.h but does not use RpcHandler; kv/kv_types.h:17 includes ccf/service/consensus_type.h but does not use ConsensusType. Removing them needs includers to stop relying on them transitively.
1. Make kv a leaf of the core (frees kv and service). All 9 witnesses are in four headers:
node/rpc/claims.h (no_claims, entry_leaf) depends only on ccf/claims_digest.h and ds, yet is included by kv/committable_tx.h and kv/generic_serialise_wrapper.h. Move it to kv (or next to ClaimsDigest). Pure relocation; endpoints also includes it, so this removes one endpoints -> node witness too.
kv/deserialise.h includes three service table headers solely for four name constants: SIGNATURES, COSE_SIGNATURES, SERIALISED_MERKLE_TREE, ENCRYPTED_PAST_LEDGER_SECRET. Either give those names a kv-owned home that the service tables reuse, or have the node inject the ledger-integrity predicates when it builds the store. The former is mechanical; the latter is the better long-term boundary.
kv/kv_types.h hosts the Consensus/ConfigurableConsensus/TxHistory interfaces, which pull in ccf/node/configuration.h (only transitively, for make_net_address/split_net_address from ccf/service/node_info_network.h), ccf/node/cose_signatures_config.h and ccf/service/reconfiguration_type.h. Include the net-address helpers directly from a lower owner, and move COSESignaturesConfig and ReconfigurationType (plain value types) to a location below kv, leaving forwarding includes at the public paths for compatibility. Do not move the interfaces into consensus: that would add kv -> consensus and a new cycle. Coordinate with [WIP] Refactor Consensus API and KV Interaction #8190, which rewrites kv_types.h and store.h.
Then add policies for kv (ccf-api, crypto, ds) and service (ccf-api, crypto, ds, kv).
2. Make Raft independent of node (frees consensus). All 6 witnesses are in raft.h plus the stale raft_types.h include. Raft needs: a NodeToNode channel for authenticated send/recv plus NodeMsgType::consensus_msg; a NodeClient only to build RetiredNodeCleanup; and CommitCallbackSubsystem. Define a small consensus-owned transport interface (send/recv of consensus messages) implemented by the node channel manager, have the node construct RetiredNodeCleanup and pass it (or a callback) in, and inject commit notification through an interface rather than the concrete subsystem. Note node/node_client.h already includes consensus/aft/raft_types.h, so the injection must go that way round. #8446 (n2n without ringbuffer) touches the same channel code and is a natural moment to do this.
3. Separate endpoint registries from node RPC internals (frees endpoints). 7 witnesses in three files: endpoint_registry.cpp (claims.h, handled by step 1; rpc_context_impl.h, signature_cache_interface.h, tx_receipt_impl.h), common_endpoint_registry.cpp (rpc/call_types.h, rpc/serialization.h) and authentication/cose_auth.cpp (cose_common.h). These are mostly node-owned implementation types that the registry consumes; give the registry the abstract types it needs (RpcContext, receipt/signature lookup) and move the concrete common_endpoint_registry handlers that need node RPC types under node.
4. Move governance out of js (frees js; the graph becomes a DAG). The 9 js -> node witnesses split into three kinds:
Governance-only extensions: NetworkExtension, NodeExtension and GovEffectsExtension (network.*, node.*, gov_effects.cpp; via network_state.h, gov_effects_interface.h, jwt_management.h, gov_logging.h). Their only installer is node/gov/handlers/proposals.h, so they can move under node/gov/.
Small helpers in the wrong owner: node/rpc/gov_logging.h is just CCF_LOG_FMT(..., "gov") macros over ds (used by console.cpp), and node/uvm_endorsements.h provides UVM endorsement verification used by the general snp_attestation extension; both can move below js (e.g. ds and pal). converters.cpp:14 includes jwt_management.h without using anything JWT-specific and looks stale.
registry.cpp:43 uses rpc_context_impl.h; this should go through the public RpcContext or move with the step 3 split.
That leaves js as the QuickJS runtime plus the JS generic app, consumed by node for the constitution and by apps/enclave for the generic app.
After step 4 every component can have a policy, and the SCC ratchet from step 0 can be tightened to "no cycles", closing the original goal of this issue for this projection. The finer goals from the original description (pure crypto/http/tls depending only on ds, and public headers not including private ones, #2428) remain separate.
Full source-component graph (26 components, 88 edges)
Legend: arrows run from includer to dependency. Dashed arrows are inside the remaining SCC. Green: common, acyclic since the last update. Yellow: rust, new and without a policy. Red nodes and red arrows: step 1 targets and the five back-edges of the proposed layering. Groupings are for readability only.
flowchart LR
subgraph roots["Acyclic composition roots and consumers"]
direction LR
host["host/"]
enclave["enclave/"]
apps["apps/"]
rust["rust/"]
indexing["indexing/"]
common["common/"]
end
subgraph core["Remaining cyclic core (6)"]
direction LR
consensus["consensus/"]
endpoints["endpoints/"]
js["js/"]
kv["kv/"]
node["node/"]
service["service/"]
end
subgraph protocols["Acyclic protocols, transport and on-disk formats"]
direction LR
http["http/"]
http_client["http_client/"]
tls["tls/"]
tcp["tcp/"]
snapshots["snapshots/"]
ledger["ledger/"]
end
subgraph support["Acyclic foundation and support"]
direction LR
crypto["crypto/"]
ds["ds/"]
pal["pal/"]
tasks["tasks/"]
cose["cose/"]
msgpack["msgpack/"]
threading["threading/"]
uv["uv/"]
end
apps --> js
common --> crypto
common --> ds
common --> service
consensus --> crypto
consensus --> ds
consensus -.-> kv
consensus -.-> node
consensus -.-> service
crypto --> cose
crypto --> ds
enclave --> common
enclave --> crypto
enclave --> ds
enclave --> indexing
enclave --> js
enclave --> kv
enclave --> node
enclave --> tasks
endpoints --> crypto
endpoints --> ds
endpoints --> http
endpoints -.-> kv
endpoints -.-> node
endpoints -.-> service
host --> common
host --> consensus
host --> crypto
host --> ds
host --> enclave
host --> http_client
host --> kv
host --> ledger
host --> node
host --> pal
host --> service
host --> snapshots
host --> uv
http --> crypto
http --> ds
http_client --> ds
http_client --> uv
indexing --> crypto
indexing --> ds
indexing --> kv
indexing --> node
indexing --> tasks
js --> crypto
js --> ds
js -.-> endpoints
js -.-> kv
js -.-> node
js --> pal
js -.-> service
kv --> crypto
kv --> ds
kv -.-> node
kv -.-> service
node -.-> consensus
node --> cose
node --> crypto
node --> ds
node -.-> endpoints
node --> http
node --> http_client
node -.-> js
node -.-> kv
node --> ledger
node --> pal
node -.-> service
node --> snapshots
node --> tasks
node --> tcp
node --> tls
pal --> crypto
pal --> ds
rust --> kv
service --> crypto
service --> ds
service -.-> kv
snapshots --> ds
snapshots --> http
snapshots --> http_client
tasks --> ds
tls --> crypto
tls --> ds
tls --> tasks
tls --> tcp
classDef opportunity fill:#ffebe9,stroke:#cf222e,stroke-width:3px,color:#82071e
classDef extracted fill:#dafbe1,stroke:#1a7f37,stroke-width:2px,color:#116329
classDef nopolicy fill:#fff8c5,stroke:#9a6700,stroke-width:2px,color:#7d4e00
class kv,service opportunity
class common extracted
class rust nopolicy
linkStyle 7,23,51,56,57 stroke:#cf222e,stroke-width:3px
Loading
Measurement scope
Same as previous updates: scanner functions from scripts/check-source-dependencies.py and exclusions/generated headers from scripts/source-dependencies.json, followed by SCC analysis and exhaustive enumeration of orderings of the core for the minimum witness-weighted feedback arc set. Nodes are top-level src components with production C/C++ files; tests, benchmarks and fuzzers are excluded. Public headers under include/ccf are attributed to their component (ccf/node/... counts as node) but not scanned as vertices. Witness counts are an ownership signal, not a cost estimate. The 2026-09-18 baseline was re-measured with the same script at f0ff3f7 and reproduces its 91 edges / 7-component SCC.
2026-09-30: kv and service are acyclic; consensus is the next cut
Measured on main at 6a2dc42 (#8470), with the same production source-folder projection as the previous updates (ccf-api and research resolved but not shown). Re-measuring ff3966f and f0ff3f7 with the same script reproduces the 88-edge/6-cyclic and 91-edge/7-cyclic states reported earlier.
Metric
2026-09-28, ff3966f
Current, 6a2dc42
After the proposed step
Source components
26
25
25
Direct internal edges
88
84
83
Cyclic components
6
4
3
Acyclic components
20
21
22
Components with explicit policies
19
21
22
Remaining SCC: consensus, endpoints, js, node. All 21 acyclic components have policies, and scripts/check-source-dependencies.py passes.
kv and service leave the core, exactly as projected by step 1 of the last update. Policies added for kv, service and rust
No other merge since ff3966f changes an edge or a witness count. Since the last update the core witnesses also moved: consensus -> node 6 -> 5 (stale raft_types.h include removed), endpoints -> node 7 -> 6 (claims.h moved to kv).
Core edges (7), with direct include witness counts
From \ to
consensus
endpoints
js
node
consensus
5
endpoints
6
js
7
9
node
9
4
14
Enumerating the 24 orderings, the cheapest feedback arc set is still 3 edges / 20 witnesses (consensus -> node, endpoints -> node, js -> node), i.e. the consensus < endpoints < js < node layering from the last update. The runner-up (23 witnesses) also cuts consensus -> node, but cuts node -> endpoints and node -> js instead of the other two. The single-edge cuts that shrink the core are consensus -> node (5, frees consensus), endpoints -> node (6, frees endpoints), node -> consensus (9) and node -> js (14). Only the first two fit the node-on-top layering. consensus -> node is also the only cut in both of the two cheapest layerings, so taking it next does not constrain the later endpoints/js decision. Cutting js -> node alone does not shrink the SCC, because js -> endpoints -> node remains, so endpoints -> node should come first.
Red: proposed next cut. Dashed orange: the two remaining back-edges after it.
Proposed next step: make Raft independent of node (frees consensus)
All five witnesses are in raft.h:15-19. What Raft actually uses from them is small:
ccf::NodeToNode: send_authenticated(to, NodeMsgType::consensus_msg, msg) (7 call sites), recv_authenticated<T> (7), DroppedMessageException and associate_node_address. It uses none of the other 11 virtual methods. node_types.h is only there for NodeMsgType::consensus_msg.
ccf::NodeClient: only to construct a RetiredNodeCleanup, whose cleanup() Raft calls in two places.
ccf::CommitCallbackSubsystem: trigger_callbacks(TxID, ViewHistory) on commit, plus set_consensus(this) in the constructor. The subsystem only uses that pointer for a null check.
Proposal, with dependencies inverted and no relocation of node code:
Channels. Add aft::ConsensusChannels (consensus/aft/consensus_channels.h) with associate_node_address, send_consensus_message (bytes and templated), recv_authenticated (virtual and templated) and DroppedMessageException, all moved from NodeToNode. ccf::NodeToNode derives from it and implements send_consensus_message as send_authenticated(to, NodeMsgType::consensus_msg, ...). The channel manager does not change, NodeMsgType stays node-owned, and ccf::NodeToNode::DroppedMessageException still names the same type.
Commit notification. Add aft::CommitObserver::on_commit(TxID, const ViewHistory&) (consensus/aft/commit_observer.h). CommitCallbackSubsystem implements it (renaming trigger_callbacks) and drops set_consensus and its pointer.
Retired-node cleanup.Aft takes a std::function<void()> instead of a NodeClient. NodeState::setup_consensus builds the RetiredNodeCleanup from its HTTPNodeClient and passes a lambda, so NodeClient, RetiredNodeCleanup and their http/tasks dependencies stay in node.
Policy. Add "consensus": ["ccf-api", "crypto", "ds", "kv", "service"]. Each entry is necessary.
Result: 83 edges and a 3-component SCC (endpoints, js, node). node -> consensus goes from 9 to 11 witnesses, which is the right direction. Following includes transitively, consensus now reaches 80 other files instead of 103, and none in node, http or tasks (before: 9, 1 and 5, all through the five raft.h includes). The consensus test ChannelStubProxy loses 11 no-op overrides and no longer depends on node_to_node.h. Production behaviour is unchanged. The only behaviour change is in unit tests: they construct Aft with a null NodeClient, so Raft previously queued a cleanup task that would have dereferenced it if it had run. Now it queues nothing.
I prototyped this on 6a2dc42 (not pushed). It touches 9 files (+136/-151), builds ccf, js_generic and the affected tests with clang 21 without warnings, and passes raft_test, raft_enclave_test, raft_scenario_test, indexing_test, channels_test, frontend_test, node_frontend_test and node_client_test. includes-checks.sh, headers-are-included.sh, clang-format 18, ascii-checks.sh and copyright-checks.sh also pass. A PR should also run the reconfiguration e2e tests, which exercise retired-node cleanup, and the e2e tests that wait for commit through CommitCallbackSubsystem.
Guard the SCC in the checker (with or before the step above)
Step 0 of the last update is still open: the checker only enforces per-component allowlists, and #8463 did not change it. The prototype adds "cyclic_components": ["endpoints", "js", "node"] to source-dependencies.json, plus about 65 lines in the checker. The check fails when:
a component outside that list is on a cycle, in which case it prints that SCC and one witness for each edge inside it;
a listed component is no longer on a cycle, so each cut has to shrink the list;
a component is neither listed nor has a policy.
This matters now because of the next section.
Coordination
Fluentd exporter for observability of the raft tracing #8386 (Fluentd exporter) would grow the SCC. It adds src/tracing, and raft_trace_msgpack.h -> tracing/trace.h -> tracing/fluentd_sink.h -> ccf/node/startup_config.h (for CCFConfig::Observability::Fluentd), i.e. consensus -> tracing -> node. Overlaid on today's main, that makes a 5-component SCC (tracing joins), and it would put consensus back into the core after the step above. The current checker accepts it because neither consensus nor tracing has a policy. With the guard above, it fails on the tracing -> node witness today, and also on consensus -> tracing once consensus is out of the list. Suggested fix in Fluentd exporter for observability of the raft tracing #8386: have FluentdSink take a tracing-owned endpoint (host, port, queue capacity) and map the configuration in host/run.cpp, then add "tracing": ["ds", "msgpack"]. Once consensus has a policy, Fluentd exporter for observability of the raft tracing #8386 will also need to add msgpack and tracing to it. With that fix, the combination still gives a 3-component SCC in which tracing and consensus are acyclic.
endpoints -> node (6). Five of the six witnesses are relocations or small splits:
rpc_context_impl.h only uses public headers. Its includers are endpoint_registry.cpp, js/registry.cpp and node/rpc/http_rpc_context.h, so moving it to endpoints removes one js -> node witness too.
signature_cache_interface.h is an interface over service/tables/signatures.h, so it can move to its consumer.
common_endpoint_registry.cpp only needs GetCommit, GetTxStatus and GetAPI from call_types.h and serialization.h. They can be split out with their JSON declarations.
cose_auth.cpp only needs ccf::cose::Signature (a span alias) and is_ecdsa_alg from cose_common.h. Both belong next to the alg constants in crypto/cose.h, along with is_rsa_alg.
tx_receipt_impl.h is the real work: build_receipt_for_committed_tx needs MerkleTreeHistory and HistoryTree from node/history.h. Either a node-implemented subsystem returns the proof, or the Merkle tree wrapper moves below node.
js -> node (9). The governance extensions move to node/gov, as in the last update. The last cut makes the graph a DAG.
Housekeeping
tcp allows ds and tls allows ccf-api, but neither dependency exists any more. These entries can be removed.
Measurement scope
Same as previous updates: scanner functions from scripts/check-source-dependencies.py and exclusions/generated headers from scripts/source-dependencies.json, followed by SCC analysis and exhaustive enumeration of orderings of the core for the minimum witness-weighted feedback arc set. Nodes are top-level src components with production C/C++ files; tests, benchmarks and fuzzers are excluded. Public headers under include/ccf are attributed to their component but not scanned as vertices. The transitive figures additionally follow includes through public headers. The #8386 figures overlay its tracing, msgpack and raft_trace_msgpack.h sources at f1f63cc onto 6a2dc42.
Activity
[-]Clarify dependencies[/-][+]Clarify dependencies between framework source components[/+]eddyashton commentedon Feb 8, 2022
The repo currently contains 3 READMEs which claim the following dependencies:
None of these dependencies are currently enforced. The actual dependencies between the folders under

src/look like this:Dotted lines show mutual/circular dependencies. Initial thoughts:
kv/andconsensus/, and believe it's extremely hard to breakds/tocrypto/crypto/, I wonder if it's worth splitting thisclients/,host/,apps/), we should enforce that distinction while we have itachamayou commentedon Jul 10, 2026
Updated from
mainat488f8da(2026-07-10).This is the same source-folder view as the original diagram: each node is a top-level directory under
src/containing C/C++ source or headers, andA -> Bmeans that a non-test source/header insrc/Adirectly includes a header owned byB(throughsrc/B,include/ccf/B, or a relative path). Tests, benchmarks, fuzzers, and third-party/STL dependencies are omitted. Dashed arrows participate in a dependency cycle; solid arrows do not. The subgraph groupings are only for readability, not dependency layers.flowchart TB subgraph consumers["Acyclic consumers"] direction LR apps["apps/"] clients["clients/"] end subgraph runtime["Node and execution"] direction LR host["host/"] enclave["enclave/"] node["node/"] endpoints["endpoints/"] js["js/"] indexing["indexing/"] snapshots["snapshots/"] end subgraph state["Replicated state and service"] direction LR consensus["consensus/"] kv["kv/"] service["service/"] end subgraph protocols["Protocols and transport"] direction LR http["http/"] tls["tls/"] quic["quic/"] tcp["tcp/"] udp["udp/"] end subgraph support["Foundation and support"] direction LR common["common/"] crypto["crypto/"] ds["ds/"] pal["pal/"] tasks["tasks/"] cose["cose/"] end apps --> js clients --> crypto clients --> ds clients --> http clients --> tls common -.-> consensus common -.-> crypto common -.-> ds common -.-> node common -.-> pal common -.-> service consensus -.-> crypto consensus -.-> ds consensus -.-> enclave consensus -.-> kv consensus -.-> node consensus -.-> pal consensus -.-> service crypto --> cose crypto -.-> ds ds -.-> crypto ds -.-> pal ds -.-> service enclave -.-> common enclave -.-> crypto enclave -.-> ds enclave -.-> host enclave -.-> http enclave -.-> indexing enclave -.-> js enclave -.-> kv enclave -.-> node enclave -.-> pal enclave -.-> quic enclave -.-> service enclave -.-> tasks enclave -.-> tcp enclave -.-> tls enclave -.-> udp endpoints -.-> crypto endpoints -.-> ds endpoints -.-> http endpoints -.-> kv endpoints -.-> node endpoints -.-> pal endpoints -.-> service host -.-> common host -.-> consensus host -.-> crypto host -.-> ds host -.-> enclave host -.-> http host -.-> indexing host -.-> kv host -.-> node host -.-> pal host -.-> service host -.-> snapshots host -.-> tcp host -.-> udp http -.-> crypto http -.-> ds http -.-> enclave http -.-> host http -.-> node indexing -.-> crypto indexing -.-> ds indexing -.-> kv indexing -.-> node indexing -.-> pal js -.-> crypto js -.-> ds js -.-> endpoints js -.-> kv js -.-> node js -.-> pal js -.-> service js -.-> tls kv -.-> crypto kv -.-> ds kv -.-> node kv -.-> pal kv -.-> service node -.-> common node -.-> consensus node --> cose node -.-> crypto node -.-> ds node -.-> enclave node -.-> endpoints node -.-> host node -.-> http node -.-> indexing node -.-> js node -.-> kv node -.-> pal node -.-> service node -.-> snapshots node -.-> tasks pal -.-> crypto pal -.-> ds quic -.-> ds quic -.-> enclave quic -.-> udp service -.-> consensus service --> cose service -.-> crypto service -.-> ds service -.-> kv service -.-> node service -.-> pal snapshots -.-> consensus snapshots -.-> ds snapshots -.-> host snapshots -.-> http tasks -.-> ds tcp -.-> ds tls -.-> crypto tls -.-> ds udp -.-> dsAt this revision there are 23 components and 120 direct internal dependency edges.
apps/andclients/are the only acyclic consumers,cose/is an acyclic sink, and the other 20 components form one strongly connected component. In particular, the original intended boundaries still do not hold:ds/directly depends oncrypto/,pal/, andservice/;kv/directly depends oncrypto/,ds/,node/,pal/, andservice/; andcrypto/directly depends onds/andcose/. Unlike the 2022 graph,host/is no longer a clean outer consumer; it is now part of the cyclic core.achamayou commentedon Sep 2, 2026
Rerun against
mainat9751a52(2026-09-02), using the same include-level methodology and exclusions as the July analysis.There are now 24 components and 116 direct edges. Crucially, the former 20-component cycle has split into:
common,consensus,enclave,endpoints,host,http,indexing,js,kv,node,quic,service,snapshots.crypto,ds,pal.apps,cose,msgpack,tasks,tcp,threading,tls,udp.flowchart TB subgraph consumers["Acyclic consumers"] apps["apps/"] end subgraph runtime["Node and execution"] host["host/"] enclave["enclave/"] node["node/"] endpoints["endpoints/"] js["js/"] indexing["indexing/"] snapshots["snapshots/"] end subgraph state["Replicated state and service"] consensus["consensus/"] kv["kv/"] service["service/"] end subgraph protocols["Protocols and transport"] http["http/"] tls["tls/"] quic["quic/"] tcp["tcp/"] udp["udp/"] end subgraph support["Foundation and support"] common["common/"] crypto["crypto/"] ds["ds/"] pal["pal/"] tasks["tasks/"] cose["cose/"] msgpack["msgpack/"] threading["threading/"] end apps --> js common -.-> consensus common --> crypto common --> ds common -.-> node common --> pal common -.-> service consensus --> crypto consensus --> ds consensus -.-> enclave consensus -.-> kv consensus -.-> node consensus --> pal consensus -.-> service crypto --> cose crypto -.-> ds ds -.-> pal enclave -.-> common enclave --> crypto enclave --> ds enclave -.-> host enclave -.-> http enclave -.-> indexing enclave -.-> js enclave -.-> kv enclave -.-> node enclave --> pal enclave -.-> quic enclave -.-> service enclave --> tasks enclave --> tcp enclave --> tls enclave --> udp endpoints --> crypto endpoints --> ds endpoints -.-> http endpoints -.-> kv endpoints -.-> node endpoints --> pal endpoints -.-> service host -.-> common host -.-> consensus host --> crypto host --> ds host -.-> enclave host -.-> http host -.-> indexing host -.-> kv host -.-> node host --> pal host -.-> service host -.-> snapshots host --> tcp host --> udp http --> crypto http --> ds http -.-> enclave http -.-> host http -.-> node http --> pal indexing --> crypto indexing --> ds indexing -.-> kv indexing -.-> node indexing --> pal js --> crypto js --> ds js -.-> endpoints js -.-> kv js -.-> node js --> pal js -.-> service js --> tls kv --> crypto kv --> ds kv -.-> node kv --> pal kv -.-> service node -.-> common node -.-> consensus node --> cose node --> crypto node --> ds node -.-> enclave node -.-> endpoints node -.-> host node -.-> http node -.-> indexing node -.-> js node -.-> kv node --> pal node -.-> service node -.-> snapshots node --> tasks pal -.-> crypto pal -.-> ds quic --> ds quic -.-> enclave quic --> udp service -.-> consensus service --> cose service --> crypto service --> ds service -.-> kv service -.-> node service --> pal snapshots -.-> consensus snapshots --> ds snapshots -.-> host snapshots -.-> http tasks --> ds tasks --> pal tcp --> ds tls --> crypto tls --> ds udp --> dsDashed arrows participate in a cycle.
Compared with the July baseline at
488f8da:clients/was removed;msgpack/andthreading/were added as internally independent components.cli_helper.hfromds/tocommon/removedds -> cryptoandds -> service, splitting the giant cycle.http -> palandtasks -> palwere added.ds/now depends only onpal/, but remains cyclic throughds -> pal -> crypto -> ds.kv/remains coupled tocrypto,ds,node,pal, andservice;host/remains in the main cyclic core.achamayou commentedon Sep 4, 2026
Rerun against
mainat09568fb(2026-09-04), using the deterministic include analysis now implemented byscripts/check-source-dependencies.pyand the same production-source exclusions as the previous updates.There are now 23 components and 104 direct internal edges. The remaining cyclic core is a single 12-component SCC:
common,consensus,enclave,endpoints,host,http,indexing,js,kv,node,service,snapshots.The other 11 components are acyclic:
apps,cose,crypto,ds,msgpack,pal,tasks,tcp,threading,tls, andudp.Compared with the 2026-09-02 baseline at
9751a52:pal, including the last outgoing edge fromds. This eliminated the separatecrypto -> ds -> pal -> cryptocycle. The resulting enforced foundation isds -> {},crypto -> {cose, ds}, andpal -> {crypto, ds}.scripts/includes-checks.sh. It currently enforces declared dependencies forcrypto,ds, andpal; it does not yet prove that the full repository is a DAG.quiccomponent and its four graph edges, reducing the main SCC from 13 components to 12.Overall, the graph moved from 24 components / 116 edges / 16 cyclic components to 23 components / 104 edges / 12 cyclic components.
Dashed arrows below participate in the remaining cycle; solid arrows do not.
flowchart TB subgraph consumers["Acyclic consumers"] apps["apps/"] end subgraph runtime["Node and execution"] host["host/"] enclave["enclave/"] node["node/"] endpoints["endpoints/"] js["js/"] indexing["indexing/"] snapshots["snapshots/"] end subgraph state["Replicated state and service"] consensus["consensus/"] kv["kv/"] service["service/"] end subgraph protocols["Protocols and transport"] http["http/"] tls["tls/"] tcp["tcp/"] udp["udp/"] end subgraph support["Foundation and support"] common["common/"] crypto["crypto/"] ds["ds/"] pal["pal/"] tasks["tasks/"] cose["cose/"] msgpack["msgpack/"] threading["threading/"] end apps --> js common -.-> consensus common --> crypto common --> ds common -.-> node common --> pal common -.-> service consensus --> crypto consensus --> ds consensus -.-> enclave consensus -.-> kv consensus -.-> node consensus -.-> service crypto --> cose crypto --> ds enclave -.-> common enclave --> crypto enclave --> ds enclave -.-> host enclave -.-> http enclave -.-> indexing enclave -.-> js enclave -.-> kv enclave -.-> node enclave -.-> service enclave --> tasks enclave --> tcp enclave --> tls enclave --> udp endpoints --> crypto endpoints --> ds endpoints -.-> http endpoints -.-> kv endpoints -.-> node endpoints -.-> service host -.-> common host -.-> consensus host --> crypto host --> ds host -.-> enclave host -.-> http host -.-> indexing host -.-> kv host -.-> node host --> pal host -.-> service host -.-> snapshots host --> tcp host --> udp http --> crypto http --> ds http -.-> enclave http -.-> host http -.-> node indexing --> crypto indexing --> ds indexing -.-> kv indexing -.-> node js --> crypto js --> ds js -.-> endpoints js -.-> kv js -.-> node js --> pal js -.-> service js --> tls kv --> crypto kv --> ds kv -.-> node kv -.-> service node -.-> common node -.-> consensus node --> cose node --> crypto node --> ds node -.-> enclave node -.-> endpoints node -.-> host node -.-> http node -.-> indexing node -.-> js node -.-> kv node --> pal node -.-> service node -.-> snapshots node --> tasks pal --> crypto pal --> ds service -.-> consensus service --> cose service --> crypto service --> ds service -.-> kv service -.-> node service --> pal snapshots -.-> consensus snapshots --> ds snapshots -.-> host snapshots -.-> http tasks --> ds tcp --> ds tls --> crypto tls --> ds udp --> dsMove HTTP runtime session adapters into enclave to break http->enclav…
Move HTTP runtime session adapters into enclave to break http->enclav…
Move HTTP runtime session adapters into enclave to break http->enclav…
achamayou commentedon Sep 9, 2026
2026-09-09: HTTP is acyclic; snapshots should be next
Current
mainisab11900. The source dependency inputs are unchanged from the locally analysed7b31f9e: the intervening #8277 adds the Lean model and associated metadata, not C/C++ dependencies.Current state
09568fbab11900There is now one 11-component strongly connected component (SCC):
common,consensus,enclave,endpoints,host,indexing,js,kv,node,service,snapshots.The 14 acyclic components are:
apps,cose,crypto,ds,http,http_client,msgpack,pal,tasks,tcp,threading,tls,udp,uv.All 14 currently acyclic components now have explicit outgoing dependency policies. This is progress toward a component DAG, not a claim that the repository is already a DAG.
Progress since the last update
uv, removinghttp -> host.enclave, removinghttp -> enclaveandhttp -> node. Together with Extract libuv ownership primitives into acyclic uv component #8281, this tookhttpout of the cyclic core.http_client. Its callers now depend on that component rather than treating it as part of the HTTP protocol implementation.The current policies include:
Compared with September 4, the four removed source-component edges are
host -> httpandhttp -> {enclave, host, node}. The six added edges arehost -> {http_client, uv},http_client -> {ds, uv},node -> http_client, andsnapshots -> http_client. The net increase of two edges reflects the new reusable components, not a new cycle.Next move: make
snapshotsacyclic in one small PRIts only outgoing edges into the cyclic core are:
snapshots -> hostfilenames.h:6,snapshot_writer.h:8TimeBoundLoggerhelpersnapshots -> consensussnapshot_writer.h:6consensus::Indexin the twopersist_snapshotindex parameters; no consensus operationsDo both cuts together. Removing either edge alone leaves all 11 components cyclic. No single-edge deletion in the current graph reduces the number of cyclic components. Among the two-edge cuts that do, this pair has the fewest direct include witnesses: three. That is an ownership signal, not an estimate that the complete patch is only three lines.
TimeBoundLoggerfromsrc/host/time_bound_logger.htosrc/ds/time_bound_logger.h. Give the generic helper an appropriateccf::dsnamespace and update all host and snapshot consumers. Its dependencies are already justds, the standard library, andfmt. Preserve the single shareddefault_max_time, including theslow_io_logging_thresholdassignment inhost/run.cpp, and preserve formatting and logging thresholds. Do not duplicate the helper or introduce ads -> hostforwarding include.consensus::Indexparameters with the existingccf::SeqNofromccf/tx_id.h. Both are exactlyuint64_ttoday:Index,SeqNo. Remove the ledger ringbuffer header fromsnapshot_writer.h; do not move consensus machinery intods, create another sequence-number type, or change snapshot bytes/filenames.scripts/source-dependencies.json:"snapshots": ["ccf-api", "ds", "http", "http_client"]. Keep the existing foundation policies unchanged. Resolve any missing direct includes exposed by the moves, rather than relying on unrelated headers to make them compile.logger_test,ledger_test,files_cleanup_test,indexing_test, andsnapshotter_testtargets. Run those unit tests and the existingrecovery_teste2e path. Preserve the writer's exclusive creation, complete writes, fsync/rename sequence, and failure handling, and retain the snapshotter's commit/rollback behaviour. Run the include/dependency and applicable formatting/ASCII checks before pushing.Acceptance criteria: no production include from
snapshotsintohostorconsensus;snapshotshas only the declared outgoing dependencies; the foundation policies still hold; and the recomputed source graph has 15 acyclic components and one 10-component SCC. At this baseline the source edge count should fall from 106 to 104. These are projections for the proposed change, not landed results.This is preferable to another isolated one-include cleanup such as moving
HistoricalTransactionFetcher: removingindexing -> nodealone still leavesindexing -> kv -> node -> indexing, so it does not shrink the SCC. It also avoids bundling the broader configuration, RPC, and KV/consensus work already in flight in #8309, #8117, and #8190.Full refreshed source-component graph (25 components, 106 edges)
As in the previous updates,
A -> Bmeans a production source/header insrc/Adirectly includes a header owned byB. Dashed arrows participate in the remaining SCC; solid arrows do not. Groupings are only for readability.flowchart TB subgraph consumers["Acyclic consumers"] apps["apps/"] end subgraph runtime["Node and execution"] host["host/"] enclave["enclave/"] node["node/"] endpoints["endpoints/"] js["js/"] indexing["indexing/"] snapshots["snapshots/"] end subgraph state["Replicated state and service"] consensus["consensus/"] kv["kv/"] service["service/"] end subgraph protocols["Protocols and transport"] http["http/"] http_client["http_client/"] tls["tls/"] tcp["tcp/"] udp["udp/"] end subgraph support["Foundation and support"] common["common/"] crypto["crypto/"] ds["ds/"] pal["pal/"] tasks["tasks/"] cose["cose/"] msgpack["msgpack/"] threading["threading/"] uv["uv/"] end apps --> js common -.-> consensus common --> crypto common --> ds common -.-> node common --> pal common -.-> service consensus --> crypto consensus --> ds consensus -.-> enclave consensus -.-> kv consensus -.-> node consensus -.-> service crypto --> cose crypto --> ds enclave -.-> common enclave --> crypto enclave --> ds enclave -.-> host enclave --> http enclave -.-> indexing enclave -.-> js enclave -.-> kv enclave -.-> node enclave -.-> service enclave --> tasks enclave --> tcp enclave --> tls enclave --> udp endpoints --> crypto endpoints --> ds endpoints --> http endpoints -.-> kv endpoints -.-> node endpoints -.-> service host -.-> common host -.-> consensus host --> crypto host --> ds host -.-> enclave host --> http_client host -.-> indexing host -.-> kv host -.-> node host --> pal host -.-> service host -.-> snapshots host --> tcp host --> udp host --> uv http --> crypto http --> ds http_client --> ds http_client --> uv indexing --> crypto indexing --> ds indexing -.-> kv indexing -.-> node js --> crypto js --> ds js -.-> endpoints js -.-> kv js -.-> node js --> pal js -.-> service js --> tls kv --> crypto kv --> ds kv -.-> node kv -.-> service node -.-> common node -.-> consensus node --> cose node --> crypto node --> ds node -.-> enclave node -.-> endpoints node -.-> host node --> http node --> http_client node -.-> indexing node -.-> js node -.-> kv node --> pal node -.-> service node -.-> snapshots node --> tasks pal --> crypto pal --> ds service -.-> consensus service --> cose service --> crypto service --> ds service -.-> kv service -.-> node service --> pal snapshots -.-> consensus snapshots --> ds snapshots -.-> host snapshots --> http snapshots --> http_client tasks --> ds tcp --> ds tls --> crypto tls --> ds udp --> dsMeasurement scope
This uses the scanner functions in
scripts/check-source-dependencies.pyand the exclusions/generated-header list inscripts/source-dependencies.json, followed by SCC analysis. It retains the previous updates' source-folder projection: nodes are top-levelsrccomponents containing production C/C++ sources/headers; tests, benchmarks, and fuzz sources are excluded; third-party/STL dependencies and public-only component targets are omitted from the displayed graph.Public headers under
include/ccfare resolved as include targets, but their own contents are not separately scanned as source vertices. In particular,ccf-apiappears in policies but is not a node in this source-folder projection; the other public-only target encountered here isresearch. This is not a linker/build-target or complete transitive public-header graph.The existing checker enforces configured outgoing dependency allowlists through
scripts/includes-checks.sh; it does not currently emit this graph or prove global acyclicity. The full DAG goal therefore remains open.achamayou commentedon Sep 13, 2026
2026-09-13: projected after #8338, left to right
Assuming #8338 merges, the graph below is measured on GitHub's synthetic merge
5e32dfb:mainat31f5f9fplus PR headcb954b1. This is a projection, not a claim that the PR has landed.mainThe only removed source-component edges are
snapshots -> hostandsnapshots -> consensus; none are added.snapshotsleaves the cyclic core, with the enforced boundarysnapshots -> {ccf-api, ds, http, http_client}. All 15 acyclic components have outgoing dependency policies.Legend: arrows run from an includer to its dependency. Dashed arrows participate in the remaining cycle; solid arrows do not. Red nodes and red arrows mark the next candidate ownership boundaries to address, not additional changes assumed in #8338. Green marks
snapshots, acyclic under the merge assumption. Groupings are for readability, not dependency layers.flowchart LR apps["apps/"] snapshots["snapshots/ (post-8338)"] subgraph core["Remaining cyclic core (10)"] direction LR common["common/"] consensus["consensus/"] enclave["enclave/"] endpoints["endpoints/"] host["host/"] indexing["indexing/"] js["js/"] kv["kv/"] node["node/"] service["service/"] end subgraph protocols["Acyclic protocols and transport"] direction LR http["http/"] http_client["http_client/"] tls["tls/"] tcp["tcp/"] udp["udp/"] end subgraph support["Acyclic foundation and support"] direction LR crypto["crypto/"] ds["ds/"] pal["pal/"] tasks["tasks/"] cose["cose/"] msgpack["msgpack/"] threading["threading/"] uv["uv/"] end apps --> js common -.-> consensus common --> crypto common --> ds common -.-> node common --> pal common -.-> service consensus --> crypto consensus --> ds consensus -.-> enclave consensus -.-> kv consensus -.-> node consensus -.-> service crypto --> cose crypto --> ds enclave -.-> common enclave --> crypto enclave --> ds enclave -.-> host enclave --> http enclave -.-> indexing enclave -.-> js enclave -.-> kv enclave -.-> node enclave -.-> service enclave --> tasks enclave --> tcp enclave --> tls enclave --> udp endpoints --> crypto endpoints --> ds endpoints --> http endpoints -.-> kv endpoints -.-> node endpoints -.-> service host -.-> common host -.-> consensus host --> crypto host --> ds host -.-> enclave host --> http_client host -.-> indexing host -.-> kv host -.-> node host --> pal host -.-> service host --> snapshots host --> tcp host --> udp host --> uv http --> crypto http --> ds http_client --> ds http_client --> uv indexing --> crypto indexing --> ds indexing -.-> kv indexing -.-> node js --> crypto js --> ds js -.-> endpoints js -.-> kv js -.-> node js --> pal js -.-> service js --> tls kv --> crypto kv --> ds kv -.-> node kv -.-> service node -.-> common node -.-> consensus node --> cose node --> crypto node --> ds node -.-> enclave node -.-> endpoints node -.-> host node --> http node --> http_client node -.-> indexing node -.-> js node -.-> kv node --> pal node -.-> service node --> snapshots node --> tasks pal --> crypto pal --> ds service -.-> consensus service --> cose service --> crypto service --> ds service -.-> kv service -.-> node service --> pal snapshots --> ds snapshots --> http snapshots --> http_client tasks --> ds tcp --> ds tls --> crypto tls --> ds udp --> ds classDef opportunity fill:#ffebe9,stroke:#cf222e,stroke-width:3px,color:#82071e classDef extracted fill:#dafbe1,stroke:#1a7f37,stroke-width:2px,color:#116329 class host,indexing opportunity class snapshots extracted linkStyle 18,56,57,77 stroke:#cf222e,stroke-width:3pxNext targets in red
host: restore an outer consumerenclave -> host,node -> hostindexing: separate runtime adapters and the KV implementationindexing -> node,indexing -> kvStart with
host. Its remaining incoming dependencies from the cyclic core are all ledger-related: three includes ofhost/ledger.hin the enclave entry/construction path, plusReadLedgerSubsystemand recovery's ledger filename helpers innode. Inject the existingAbstractReadLedgerSubsystemInterfaceinstead of the concrete ledger at the enclave entry point; keep the concrete adapter beside the host-owned ledger, and give the shared ledger filename helpers a lower-level, cycle-free ledger-format owner. Remove both back-edges without introducing replacement cycles, preserving ledger lifetime and recovery behaviour. This would takehostout of the SCC; the existinghost -> node/enclave/...consumer dependencies can remain.Then investigate
indexing's boundary. MoveHistoricalTransactionFetcherbeside its node-owned historical cache, and replace unnecessarily broad private KV includes inindexer.h,seqnos_by_key_bucketed.cpp, andvisit_each_entry_in_map.cppwith the narrow interfaces they actually use. This is not an automatic SCC win: direct includes of publicccf/kv/...headers still count askvin this graph. A genuine acyclic boundary requires either a separately owned lower-level KV API or first removingkv's own back-edges tonodeandservice. Do not hide the dependency through transitive includes. Moving the historical fetcher alone leavesindexing -> kv -> node -> indexing.No single-edge deletion shrinks the remaining 10-component SCC. The two highlighted pairs are the lowest-include-count two-edge cuts that do so in the measured graph, but include count is not implementation cost. Keep the broader configuration and KV/consensus work coordinated with #8309 and #8190; neither, nor #8117, is assumed merged here. Recompute the graph and enforce the resulting boundary after each real extraction.
Measurement scope
Same source-folder projection as the preceding updates, using
check-source-dependencies.pyand its policy/exclusion file, followed by strongly connected component analysis. Nodes are top-levelsrccomponents with production C/C++ files. Public headers are resolved as include targets, but their bodies are not scanned as source vertices; public-onlyccf-apiandresearchtargets, third-party/STL dependencies, tests, benchmarks, and fuzzers are omitted. This is not a complete transitive public-header or build-target graph, and the full DAG goal remains open.achamayou commentedon Sep 17, 2026
2026-09-17: host and enclave are acyclic; indexing is now a one-include cut
Measured on
mainatefc44e3(#8396), using the same source-folder projection as the previous updates.mainatefc44e3indexingcommon(with or after #8309)The remaining 8-component SCC is
common,consensus,endpoints,indexing,js,kv,node,service.The 17 acyclic components are
apps,cose,crypto,ds,enclave,host,http,http_client,ledger,msgpack,pal,snapshots,tasks,tcp,threading,tls,uv. All 17 have explicit outgoing policies inscripts/source-dependencies.json. The full DAG goal remains open.Progress since the last update, per landed PR
snapshots -> {consensus, host}host -> indexingindexing -> taskstasksinstead of the ringbufferenclave -> {tls, udp},host -> udp,js -> tls,tcp -> ds, and the oldtls/udpleaf edgeshost -> {http, tasks}udpand the old TLS wrapper component are deleted: 23 components, 98 edges, still 10 cyclicenclave -> host,node -> host,host -> {http, tasks, tcp}host -> ledger,node -> ledger,enclave -> tls, newledgerandtlsleaf edgeshostleaves the core;ledgerand the newtlsare born acyclic: 25 components, 100 edges, 9 cyclicconsensus -> enclave,node -> enclave,enclave -> {http, service, tcp, tls}node -> {tcp, tls}enclaveleaves the core: 96 edges, 8 cyclicA note on numbers: the descriptions of #8389 and #8396 report 26 components and 114 -> 110 edges because they also count the 14 include edges into the public-only
ccf-apiandresearchtargets. This update keeps the issue's projection, in which those targets are resolved but not displayed: 96 edges (110 if they are included). The two views agree on the SCC.What changed structurally
With
enclavegone,nodeis the only core component that still depends oncommon,indexing, orjs. In the previous updates no single-edge deletion shrank the SCC; now there are three:node -> indexingindexingnode_state.h:34node -> commoncommoncommon/configuration.h; overlaps #8309node -> jsjsjs_policy.h, member/node frontendsEach of these turns the freed component into an outer consumer of the core, exactly as #8389 and #8396 did for
hostandenclave. It does not make them lower-level "pure" components; that remains the goal forkvandservice, see below.Remaining core edges, with direct include witness counts (27 edges):
Core edge table
commonconsensus(1),node(1),service(6)node(4)consensuskv(5),node(6),service(5)common(1),node(8),service(1)endpointskv(1),node(7),service(12)js(7),node(4)indexingkv(4),node(1)node(1)jsendpoints(7),kv(4),node(9),service(4)node(14)kvnode(4),service(5)consensus(5),endpoints(1),indexing(4),js(4),node(24),service(7)nodecommon(4),consensus(8),endpoints(4),indexing(1),js(14),kv(24),service(99)common(1),consensus(6),endpoints(7),indexing(1),js(9),kv(4),service(3)serviceconsensus(1),kv(7),node(3)common(6),consensus(5),endpoints(12),js(4),kv(5),node(99)Move 1 (now): take
indexingout throughnode -> indexingThe single witness is
node_state.h:34includingindexing/indexer.h.NodeStateuses theIndexerfor exactly one thing:tick()callsindexer->update_strategies(elapsed, committed_txid)while the node ispartOfNetwork. TheIndexeris constructed, installed as a subsystem, and owned by the composition root inenclave.h, whose tick handler already ticks the siblinghistorical_state_cacheimmediately afternode->tick(elapsed_ms).Indexer::update_strategiesfrom the composition root. Remove theindexer_parameter fromNodeState::initializeand theindexermember. In the enclave tick handler, directly afternode->tick(elapsed_ms)(so still afterconsensus->periodic), callindexer->update_strategies(elapsed_ms, committed)gated on the existingnode->is_part_of_network(). Expose the committedTxIDfromNodeStatewith a small accessor rather than reaching into consensus fromenclave.h. Do not introduce a node-owned observer interface thatIndexerimplements: that would keep the pair coupled in the other direction and complicate a later lower-levelindexing. The ordering relative ton2n_channels->tickchanges, which does not affect the committed index."indexing": ["ccf-api", "crypto", "ds", "kv", "node", "tasks"], which is exactly the measured outgoing set.indexing -> node(the historical fetcher adapter) andindexing -> kv(transaction deserialisation in the strategies) remain as ordinary consumer dependencies. The 09-13 idea of makingindexinglower thankvstays blocked onkv's own back-edges and is not needed for this cut.indexing_test; runindexing_testand thee2e_logging.pyindexing paths (test_committed_index,test_historical_query_range*,test_historical_query_sparse), which exerciseSeqnosByKey_Bucketedend to end. Recompute the graph and run the include/dependency, formatting and ASCII checks before pushing.Acceptance: no production include from
nodeintoindexing; the recomputed graph has 95 edges, one 7-component SCC and 18 acyclic components, all with policies. Projection, not a landed result.Move 2 (with or straight after #8309): take
commonout throughnode -> commonAll four witnesses are
common/configuration.h:history.h:11andrpc/frontend.h:16use nothing from it;node_call_types.h:80needs the JSON declarations forStartupConfig::Start(the/node/createbody inrpc/serialization.h);node_interface.h:61needs only the publicccf/node/startup_config.h.node_state.halso picks upStartTypetransitively through this header.#8309 (draft, "Unify operator and node startup configuration") rewrites exactly this header: it introduces public
ccf/node/configuration.handccf/node/start_type.h, but currently keeps the JSON declarations insrc/common/configuration.hand keepsnodeincluding it, so it would leavenode -> commonin place. The cut is therefore best folded into #8309 or done immediately after it:DECLARE_JSON_*declarations for the public configuration types next to the types (public header, asinclude/ccf/service/*already does) or in anode-owned header, so thatnodenever includescommon/configuration.h; drop the two stale includes.StartTypeout ofcommon/enclave_interface_types.hintoccf/node/start_type.h, as Unify operator and node startup configuration #8309 already does."common": ["ccf-api", "consensus", "crypto", "ds", "node", "pal", "service"].commonthen becomes what its consumers say it is: host/enclave shared configuration and ringbuffer types, consumed only byhostandenclave.Acceptance: with move 1, 92 edges, one 6-component SCC (
consensus,endpoints,js,kv,node,service), 19 acyclic components with policies.Prep that can land any time: relocate
service/internal_tables_access.htonode/internal_tables_access.his the only source ofservice -> node(3 witnesses:node/history.h,node/ledger_secrets.h,node/uvm_endorsements.h) andservice -> consensus(1:consensus/aft/raft_types.h). Its eight includers are all undersrc/node/. Moving it removes both edges and adds none (96 -> 94 alone). It does not shrink the SCC by itself, becauseservice -> kv -> serviceremains, but it leavesservice -> kvasservice's only core out-edge, soservicefalls out automatically oncekvdoes. Neither #8190 nor #8309 touches this file.After that: make
kvpure, thenjskv's only core out-edges arekv -> node(4:node/rpc/claims.hfromcommittable_tx.handgeneric_serialise_wrapper.h;ccf/node/{cose_signatures_config,startup_config}.hfromkv_types.h) andkv -> service(5:service/tables/{shares,signatures,snapshot_evidence}.hfromdeserialise.h;ccf/service/{consensus_type,reconfiguration_type}.hfromkv_types.h). Removing those nine witnesses, together with the prep above, would freekvandservice, leaving a 4-component core (consensus,endpoints,js,node). This is the KV/consensus coupling the original issue asked to justify or remove, and it overlaps #8190 (kv_types.h,store.h), so it should be coordinated with that work rather than raced.node -> js(14 witnesses across governance,js_policy.hand the frontends) is the remaining single-edge cut, but it needs the constitution runtime behind an interface and is the most invasive of the three; it should wait untilkvandserviceare settled.Full refreshed source-component graph (25 components, 96 edges)
Legend: arrows run from an includer to its dependency. Dashed arrows participate in the remaining cycle; solid arrows do not. Green marks components that became acyclic since the last update (
host,enclave, and the newledgerandtls). Red nodes and red arrows mark the cuts proposed above, not landed changes. Groupings are for readability, not dependency layers.flowchart LR subgraph roots["Acyclic composition roots and consumers"] direction LR host["host/"] enclave["enclave/"] apps["apps/"] end subgraph core["Remaining cyclic core (8)"] direction LR common["common/"] consensus["consensus/"] endpoints["endpoints/"] indexing["indexing/"] js["js/"] kv["kv/"] node["node/"] service["service/"] end subgraph protocols["Acyclic protocols, transport and on-disk formats"] direction LR http["http/"] http_client["http_client/"] tls["tls/"] tcp["tcp/"] snapshots["snapshots/"] ledger["ledger/"] end subgraph support["Acyclic foundation and support"] direction LR crypto["crypto/"] ds["ds/"] pal["pal/"] tasks["tasks/"] cose["cose/"] msgpack["msgpack/"] threading["threading/"] uv["uv/"] end apps --> js common -.-> consensus common --> crypto common --> ds common -.-> node common --> pal common -.-> service consensus --> crypto consensus --> ds consensus -.-> kv consensus -.-> node consensus -.-> service crypto --> cose crypto --> ds enclave --> common enclave --> crypto enclave --> ds enclave --> indexing enclave --> js enclave --> kv enclave --> node enclave --> tasks endpoints --> crypto endpoints --> ds endpoints --> http endpoints -.-> kv endpoints -.-> node endpoints -.-> service host --> common host --> consensus host --> crypto host --> ds host --> enclave host --> http_client host --> kv host --> ledger host --> node host --> pal host --> service host --> snapshots host --> uv http --> crypto http --> ds http_client --> ds http_client --> uv indexing --> crypto indexing --> ds indexing -.-> kv indexing -.-> node indexing --> tasks js --> crypto js --> ds js -.-> endpoints js -.-> kv js -.-> node js --> pal js -.-> service kv --> crypto kv --> ds kv -.-> node kv -.-> service node -.-> common node -.-> consensus node --> cose node --> crypto node --> ds node -.-> endpoints node --> http node --> http_client node -.-> indexing node -.-> js node -.-> kv node --> ledger node --> pal node -.-> service node --> snapshots node --> tasks node --> tcp node --> tls pal --> crypto pal --> ds service -.-> consensus service --> cose service --> crypto service --> ds service -.-> kv service -.-> node service --> pal snapshots --> ds snapshots --> http snapshots --> http_client tasks --> ds tls --> crypto tls --> ds tls --> tasks tls --> tcp classDef opportunity fill:#ffebe9,stroke:#cf222e,stroke-width:3px,color:#82071e classDef extracted fill:#dafbe1,stroke:#1a7f37,stroke-width:2px,color:#116329 class indexing,common opportunity class host,enclave,ledger,tls extracted linkStyle 61,69,81,86 stroke:#cf222e,stroke-width:3pxMeasurement scope
Same source-folder projection as the preceding updates: the scanner functions in
scripts/check-source-dependencies.pyand the exclusions and generated-header list inscripts/source-dependencies.json, followed by strongly connected component analysis. Nodes are top-levelsrccomponents containing production C/C++ sources or headers; tests, benchmarks and fuzzers are excluded. Public headers underinclude/ccfare resolved as include targets but not scanned as source vertices; the public-onlyccf-apiandresearchtargets and third-party/STL dependencies are omitted from the displayed graph. Single- and two-edge cut candidates were enumerated over the 27 core edges; include witness counts are an ownership signal, not an implementation cost estimate. The checker enforces the configured allowlists throughscripts/includes-checks.sh; it does not emit this graph or prove global acyclicity.achamayou commentedon Sep 18, 2026
2026-09-18:
indexingis acyclic; the core is down to 7 componentsMeasured on
mainatf0ff3f7, using the same production source-folder projection as the previous updates (so theccf-apipolicy pseudo-component is not counted as a source-folder edge).efc44e3#8398 has landed. It removed
node -> indexingand movedservice/internal_tables_access.htonode/, removingservice -> {consensus, node, cose, pal}.indexingnow has an explicit policy and is outside the remaining SCC. The production include graph has not changed since that merge throughf0ff3f7.The remaining cyclic core is:
common,consensus,endpoints,js,kv,node,serviceSuggested next cuts
Finish
commonthrough Unify operator and node startup configuration #8309. Remove the four remainingnode -> commonwitnesses, all includes ofcommon/configuration.h, by keeping public configuration JSON declarations with the public/node-owned types and removing the two stale includes. Then add the measuredcommonpolicy. On today's graph, that direct cut gives 90 edges and a 6-component SCC. Since Unify operator and node startup configuration #8309 rewrites these same headers, this should be folded into that PR or land immediately after it, not raced independently.Make
kva core library, coordinated with [WIP] Refactor Consensus API and KV Interaction #8190. Removekv -> nodeandkv -> service, rather than trying to remove the natural consumer edgeservice -> kv:kv/deserialise.hinto the node/service integration layer.With the
commoncut, removing those two direct edges frees bothkvandservice: projected 88 edges and a 4-component SCC (consensus,endpoints,js,node). This is the highest-leverage next tranche and should add policies for both components.Put the constitution runtime behind a node-facing interface. Removing
node -> jsthen makesjsacyclic: projected 87 edges and a 3-component SCC (consensus,endpoints,node). At that point the remaining work is two explicit seams rather than a general tangle:node <-> consensusandnode <-> endpoints.These projections assume no unrelated source-folder edges are added; each landing PR should rerun the SCC analysis and add the newly valid policy in
scripts/source-dependencies.json.achamayou commentedon Sep 28, 2026
2026-09-28:
commonis acyclic; five back-edges now separate us from a DAGMeasured on
mainatff3966f(#8455), with the same production source-folder projection as the previous updates (ccf-apiandresearchresolved but not shown).f0ff3f7ff3966fRemaining SCC:
consensus,endpoints,js,kv,node,service.scripts/check-source-dependencies.pypasses.What changed
node -> common(all 4 witnesses),common -> {consensus, node, pal}commonleaves the core, policycommon -> {crypto, ds, service}added. This was suggested cut 1 from the last update.rust -> kvrustcomponent (app_bridge.cpp), born acyclic but without a policyNothing else changed the component graph; other merges only moved witness counts (e.g.
node -> service103 -> 106,enclave -> node23 -> 27). The open PRs touching core files that I checked (#8405, #8445, #8446, #8433, #8423) do not add or remove core edges; #8190 (draft, last updated 2026-08-24) overlaps thekvwork below.Core edges (17), with direct include witness counts
Target layering: node on top
Enumerating all 720 orderings of the six core components, the cheapest feedback arc set is 5 edges / 31 witnesses (bold above), giving the layering
kv < service < {consensus, endpoints} < js < nodei.e.
nodeis the integration layer that owns everything else, which matches howenclaveandhostalready consume it. The runner-up (33 witnesses) instead cutsnode -> endpointsandnode -> js, puttingendpoints/jsabovenode; that would requirenodeto reach its own auth policies and the constitution runtime through injected interfaces, which is more invasive than relocating a handful of node-owned headers and governance extensions. I'd therefore revise suggestion 3 from the last update: rather than hiding the constitution runtime behind a node-facing interface (removingnode -> js), move the governance-specific pieces out ofjsand removejs -> node.Applying the cuts in order (projections, assuming nothing else changes):
kv -> node,kv -> serviceconsensus -> nodeendpoints -> nodejs -> nodeSteps 3 and 4 are order-sensitive: removing
js -> nodefirst frees nothing, becausejs -> endpoints -> noderemains.Suggested next steps
0. Housekeeping (small, independent).
"rust": ["ccf-api", "kv"]tosource-dependencies.json.rustis the only acyclic component without a policy. Its private-KV witness iskv/compacted_version_conflict.h, also used byjsandnode; worth deciding whether that exception type belongs in the publicccf/kvAPI.check-source-dependencies.pycompute SCCs and fail if the cyclic set grows beyond an allowlist (currently the six above), shrinking the allowlist with each cut. Today it only enforces per-component allowlists, so a new cycle through unpoliced components would go unnoticed.node_state.h:76includeskv/test/null_encryptor.h, a test header, from production code.consensus/aft/raft_types.h:10includesnode/rpc/rpc_handler.hbut does not useRpcHandler;kv/kv_types.h:17includesccf/service/consensus_type.hbut does not useConsensusType. Removing them needs includers to stop relying on them transitively.1. Make
kva leaf of the core (freeskvandservice). All 9 witnesses are in four headers:node/rpc/claims.h(no_claims,entry_leaf) depends only onccf/claims_digest.handds, yet is included bykv/committable_tx.handkv/generic_serialise_wrapper.h. Move it tokv(or next toClaimsDigest). Pure relocation;endpointsalso includes it, so this removes oneendpoints -> nodewitness too.kv/deserialise.hincludes three service table headers solely for four name constants:SIGNATURES,COSE_SIGNATURES,SERIALISED_MERKLE_TREE,ENCRYPTED_PAST_LEDGER_SECRET. Either give those names akv-owned home that the service tables reuse, or have the node inject the ledger-integrity predicates when it builds the store. The former is mechanical; the latter is the better long-term boundary.kv/kv_types.hhosts theConsensus/ConfigurableConsensus/TxHistoryinterfaces, which pull inccf/node/configuration.h(only transitively, formake_net_address/split_net_addressfromccf/service/node_info_network.h),ccf/node/cose_signatures_config.handccf/service/reconfiguration_type.h. Include the net-address helpers directly from a lower owner, and moveCOSESignaturesConfigandReconfigurationType(plain value types) to a location belowkv, leaving forwarding includes at the public paths for compatibility. Do not move the interfaces intoconsensus: that would addkv -> consensusand a new cycle. Coordinate with [WIP] Refactor Consensus API and KV Interaction #8190, which rewriteskv_types.handstore.h.Then add policies for
kv(ccf-api,crypto,ds) andservice(ccf-api,crypto,ds,kv).2. Make Raft independent of
node(freesconsensus). All 6 witnesses are inraft.hplus the staleraft_types.hinclude. Raft needs: aNodeToNodechannel for authenticated send/recv plusNodeMsgType::consensus_msg; aNodeClientonly to buildRetiredNodeCleanup; andCommitCallbackSubsystem. Define a small consensus-owned transport interface (send/recv of consensus messages) implemented by the node channel manager, have the node constructRetiredNodeCleanupand pass it (or a callback) in, and inject commit notification through an interface rather than the concrete subsystem. Notenode/node_client.halready includesconsensus/aft/raft_types.h, so the injection must go that way round. #8446 (n2n without ringbuffer) touches the same channel code and is a natural moment to do this.3. Separate endpoint registries from node RPC internals (frees
endpoints). 7 witnesses in three files:endpoint_registry.cpp(claims.h, handled by step 1;rpc_context_impl.h,signature_cache_interface.h,tx_receipt_impl.h),common_endpoint_registry.cpp(rpc/call_types.h,rpc/serialization.h) andauthentication/cose_auth.cpp(cose_common.h). These are mostly node-owned implementation types that the registry consumes; give the registry the abstract types it needs (RpcContext, receipt/signature lookup) and move the concretecommon_endpoint_registryhandlers that need node RPC types undernode.4. Move governance out of
js(freesjs; the graph becomes a DAG). The 9js -> nodewitnesses split into three kinds:NetworkExtension,NodeExtensionandGovEffectsExtension(network.*,node.*,gov_effects.cpp; vianetwork_state.h,gov_effects_interface.h,jwt_management.h,gov_logging.h). Their only installer isnode/gov/handlers/proposals.h, so they can move undernode/gov/.node/rpc/gov_logging.his justCCF_LOG_FMT(..., "gov")macros overds(used byconsole.cpp), andnode/uvm_endorsements.hprovides UVM endorsement verification used by the generalsnp_attestationextension; both can move belowjs(e.g.dsandpal).converters.cpp:14includesjwt_management.hwithout using anything JWT-specific and looks stale.registry.cpp:43usesrpc_context_impl.h; this should go through the publicRpcContextor move with the step 3 split.That leaves
jsas the QuickJS runtime plus the JS generic app, consumed bynodefor the constitution and byapps/enclavefor the generic app.After step 4 every component can have a policy, and the SCC ratchet from step 0 can be tightened to "no cycles", closing the original goal of this issue for this projection. The finer goals from the original description (pure
crypto/http/tlsdepending only onds, and public headers not including private ones, #2428) remain separate.Full source-component graph (26 components, 88 edges)
Legend: arrows run from includer to dependency. Dashed arrows are inside the remaining SCC. Green:
common, acyclic since the last update. Yellow:rust, new and without a policy. Red nodes and red arrows: step 1 targets and the five back-edges of the proposed layering. Groupings are for readability only.flowchart LR subgraph roots["Acyclic composition roots and consumers"] direction LR host["host/"] enclave["enclave/"] apps["apps/"] rust["rust/"] indexing["indexing/"] common["common/"] end subgraph core["Remaining cyclic core (6)"] direction LR consensus["consensus/"] endpoints["endpoints/"] js["js/"] kv["kv/"] node["node/"] service["service/"] end subgraph protocols["Acyclic protocols, transport and on-disk formats"] direction LR http["http/"] http_client["http_client/"] tls["tls/"] tcp["tcp/"] snapshots["snapshots/"] ledger["ledger/"] end subgraph support["Acyclic foundation and support"] direction LR crypto["crypto/"] ds["ds/"] pal["pal/"] tasks["tasks/"] cose["cose/"] msgpack["msgpack/"] threading["threading/"] uv["uv/"] end apps --> js common --> crypto common --> ds common --> service consensus --> crypto consensus --> ds consensus -.-> kv consensus -.-> node consensus -.-> service crypto --> cose crypto --> ds enclave --> common enclave --> crypto enclave --> ds enclave --> indexing enclave --> js enclave --> kv enclave --> node enclave --> tasks endpoints --> crypto endpoints --> ds endpoints --> http endpoints -.-> kv endpoints -.-> node endpoints -.-> service host --> common host --> consensus host --> crypto host --> ds host --> enclave host --> http_client host --> kv host --> ledger host --> node host --> pal host --> service host --> snapshots host --> uv http --> crypto http --> ds http_client --> ds http_client --> uv indexing --> crypto indexing --> ds indexing --> kv indexing --> node indexing --> tasks js --> crypto js --> ds js -.-> endpoints js -.-> kv js -.-> node js --> pal js -.-> service kv --> crypto kv --> ds kv -.-> node kv -.-> service node -.-> consensus node --> cose node --> crypto node --> ds node -.-> endpoints node --> http node --> http_client node -.-> js node -.-> kv node --> ledger node --> pal node -.-> service node --> snapshots node --> tasks node --> tcp node --> tls pal --> crypto pal --> ds rust --> kv service --> crypto service --> ds service -.-> kv snapshots --> ds snapshots --> http snapshots --> http_client tasks --> ds tls --> crypto tls --> ds tls --> tasks tls --> tcp classDef opportunity fill:#ffebe9,stroke:#cf222e,stroke-width:3px,color:#82071e classDef extracted fill:#dafbe1,stroke:#1a7f37,stroke-width:2px,color:#116329 classDef nopolicy fill:#fff8c5,stroke:#9a6700,stroke-width:2px,color:#7d4e00 class kv,service opportunity class common extracted class rust nopolicy linkStyle 7,23,51,56,57 stroke:#cf222e,stroke-width:3pxMeasurement scope
Same as previous updates: scanner functions from
scripts/check-source-dependencies.pyand exclusions/generated headers fromscripts/source-dependencies.json, followed by SCC analysis and exhaustive enumeration of orderings of the core for the minimum witness-weighted feedback arc set. Nodes are top-levelsrccomponents with production C/C++ files; tests, benchmarks and fuzzers are excluded. Public headers underinclude/ccfare attributed to their component (ccf/node/...counts asnode) but not scanned as vertices. Witness counts are an ownership signal, not a cost estimate. The 2026-09-18 baseline was re-measured with the same script atf0ff3f7and reproduces its 91 edges / 7-component SCC.achamayou commentedon Sep 30, 2026
2026-09-30:
kvandserviceare acyclic;consensusis the next cutMeasured on
mainat6a2dc42(#8470), with the same production source-folder projection as the previous updates (ccf-apiandresearchresolved but not shown). Re-measuringff3966fandf0ff3f7with the same script reproduces the 88-edge/6-cyclic and 91-edge/7-cyclic states reported earlier.ff3966f6a2dc42Remaining SCC:
consensus,endpoints,js,node. All 21 acyclic components have policies, andscripts/check-source-dependencies.pypasses.What changed
src/cose(Rust COSE FFI) deleted in favour ofcrypto/cose.h:crypto -> cose,node -> cosekv -> node,kv -> servicekvandserviceleave the core, exactly as projected by step 1 of the last update. Policies added forkv,serviceandrustNo other merge since
ff3966fchanges an edge or a witness count. Since the last update the core witnesses also moved:consensus -> node6 -> 5 (staleraft_types.hinclude removed),endpoints -> node7 -> 6 (claims.hmoved tokv).Core edges (7), with direct include witness counts
Enumerating the 24 orderings, the cheapest feedback arc set is still 3 edges / 20 witnesses (
consensus -> node,endpoints -> node,js -> node), i.e. theconsensus < endpoints < js < nodelayering from the last update. The runner-up (23 witnesses) also cutsconsensus -> node, but cutsnode -> endpointsandnode -> jsinstead of the other two. The single-edge cuts that shrink the core areconsensus -> node(5, freesconsensus),endpoints -> node(6, freesendpoints),node -> consensus(9) andnode -> js(14). Only the first two fit the node-on-top layering.consensus -> nodeis also the only cut in both of the two cheapest layerings, so taking it next does not constrain the laterendpoints/jsdecision. Cuttingjs -> nodealone does not shrink the SCC, becausejs -> endpoints -> noderemains, soendpoints -> nodeshould come first.graph LR subgraph core["remaining cycle"] consensus -- 5 --> node node -- 9 --> consensus endpoints -- 6 --> node node -- 4 --> endpoints js -- 9 --> node node -- 14 --> js js -- 7 --> endpoints end consensus -- 4 --> service endpoints -- 12 --> service js -- 4 --> service node -- 105 --> service consensus -- 5 --> kv endpoints -- 1 --> kv js -- 4 --> kv node -- 26 --> kv service -- 9 --> kv linkStyle 0 stroke:#cf222e,stroke-width:4px linkStyle 2,4 stroke:#bc4c00,stroke-width:2px,stroke-dasharray:5Red: proposed next cut. Dashed orange: the two remaining back-edges after it.
Proposed next step: make Raft independent of
node(freesconsensus)All five witnesses are in
raft.h:15-19. What Raft actually uses from them is small:ccf::NodeToNode:send_authenticated(to, NodeMsgType::consensus_msg, msg)(7 call sites),recv_authenticated<T>(7),DroppedMessageExceptionandassociate_node_address. It uses none of the other 11 virtual methods.node_types.his only there forNodeMsgType::consensus_msg.ccf::NodeClient: only to construct aRetiredNodeCleanup, whosecleanup()Raft calls in two places.ccf::CommitCallbackSubsystem:trigger_callbacks(TxID, ViewHistory)on commit, plusset_consensus(this)in the constructor. The subsystem only uses that pointer for a null check.Proposal, with dependencies inverted and no relocation of node code:
aft::ConsensusChannels(consensus/aft/consensus_channels.h) withassociate_node_address,send_consensus_message(bytes and templated),recv_authenticated(virtual and templated) andDroppedMessageException, all moved fromNodeToNode.ccf::NodeToNodederives from it and implementssend_consensus_messageassend_authenticated(to, NodeMsgType::consensus_msg, ...). The channel manager does not change,NodeMsgTypestays node-owned, andccf::NodeToNode::DroppedMessageExceptionstill names the same type.aft::CommitObserver::on_commit(TxID, const ViewHistory&)(consensus/aft/commit_observer.h).CommitCallbackSubsystemimplements it (renamingtrigger_callbacks) and dropsset_consensusand its pointer.Afttakes astd::function<void()>instead of aNodeClient.NodeState::setup_consensusbuilds theRetiredNodeCleanupfrom itsHTTPNodeClientand passes a lambda, soNodeClient,RetiredNodeCleanupand theirhttp/tasksdependencies stay innode."consensus": ["ccf-api", "crypto", "ds", "kv", "service"]. Each entry is necessary.Result: 83 edges and a 3-component SCC (
endpoints,js,node).node -> consensusgoes from 9 to 11 witnesses, which is the right direction. Following includes transitively,consensusnow reaches 80 other files instead of 103, and none innode,httportasks(before: 9, 1 and 5, all through the fiveraft.hincludes). The consensus testChannelStubProxyloses 11 no-op overrides and no longer depends onnode_to_node.h. Production behaviour is unchanged. The only behaviour change is in unit tests: they constructAftwith a nullNodeClient, so Raft previously queued a cleanup task that would have dereferenced it if it had run. Now it queues nothing.I prototyped this on
6a2dc42(not pushed). It touches 9 files (+136/-151), buildsccf,js_genericand the affected tests with clang 21 without warnings, and passesraft_test,raft_enclave_test,raft_scenario_test,indexing_test,channels_test,frontend_test,node_frontend_testandnode_client_test.includes-checks.sh,headers-are-included.sh, clang-format 18,ascii-checks.shandcopyright-checks.shalso pass. A PR should also run the reconfiguration e2e tests, which exercise retired-node cleanup, and the e2e tests that wait for commit throughCommitCallbackSubsystem.Guard the SCC in the checker (with or before the step above)
Step 0 of the last update is still open: the checker only enforces per-component allowlists, and #8463 did not change it. The prototype adds
"cyclic_components": ["endpoints", "js", "node"]tosource-dependencies.json, plus about 65 lines in the checker. The check fails when:This matters now because of the next section.
Coordination
src/tracing, andraft_trace_msgpack.h->tracing/trace.h->tracing/fluentd_sink.h->ccf/node/startup_config.h(forCCFConfig::Observability::Fluentd), i.e.consensus -> tracing -> node. Overlaid on today'smain, that makes a 5-component SCC (tracingjoins), and it would putconsensusback into the core after the step above. The current checker accepts it because neitherconsensusnortracinghas a policy. With the guard above, it fails on thetracing -> nodewitness today, and also onconsensus -> tracingonceconsensusis out of the list. Suggested fix in Fluentd exporter for observability of the raft tracing #8386: haveFluentdSinktake a tracing-owned endpoint (host, port, queue capacity) and map the configuration inhost/run.cpp, then add"tracing": ["ds", "msgpack"]. Onceconsensushas a policy, Fluentd exporter for observability of the raft tracing #8386 will also need to addmsgpackandtracingto it. With that fix, the combination still gives a 3-component SCC in whichtracingandconsensusare acyclic.LedgerEnclaveargument in the samemake_shared<RaftType>statement. [WIP] Refactor Consensus API and KV Interaction #8190, Replace bundled fmt with std::format #8368, Lean spec of ccfraft and trace validation #8382 and [WIP] Systematic testing: Test the full KV + History + Consensus stack, with deterministic and randomly-explored interleavings #8238 (all drafts) touchraft.h. All of these overlaps are mechanical.After that
endpoints -> node(6). Five of the six witnesses are relocations or small splits:rpc_context_impl.honly uses public headers. Its includers areendpoint_registry.cpp,js/registry.cppandnode/rpc/http_rpc_context.h, so moving it toendpointsremoves onejs -> nodewitness too.signature_cache_interface.his an interface overservice/tables/signatures.h, so it can move to its consumer.common_endpoint_registry.cpponly needsGetCommit,GetTxStatusandGetAPIfromcall_types.handserialization.h. They can be split out with their JSON declarations.cose_auth.cpponly needsccf::cose::Signature(a span alias) andis_ecdsa_algfromcose_common.h. Both belong next to thealgconstants incrypto/cose.h, along withis_rsa_alg.tx_receipt_impl.his the real work:build_receipt_for_committed_txneedsMerkleTreeHistoryandHistoryTreefromnode/history.h. Either a node-implemented subsystem returns the proof, or the Merkle tree wrapper moves belownode.js -> node(9). The governance extensions move tonode/gov, as in the last update. The last cut makes the graph a DAG.Housekeeping
tcpallowsdsandtlsallowsccf-api, but neither dependency exists any more. These entries can be removed.Measurement scope
Same as previous updates: scanner functions from
scripts/check-source-dependencies.pyand exclusions/generated headers fromscripts/source-dependencies.json, followed by SCC analysis and exhaustive enumeration of orderings of the core for the minimum witness-weighted feedback arc set. Nodes are top-levelsrccomponents with production C/C++ files; tests, benchmarks and fuzzers are excluded. Public headers underinclude/ccfare attributed to their component but not scanned as vertices. The transitive figures additionally follow includes through public headers. The #8386 figures overlay itstracing,msgpackandraft_trace_msgpack.hsources atf1f63cconto6a2dc42.