Skip to content

Clarify dependencies between framework source components #3517

Description

Current status (2026-09-18)

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:

  1. 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.
  2. 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.
  3. 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.

See the latest measured state and cut rationale.

Original scope

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.

A few high-level goals to start with:

  • Public headers should not include any private headers (Private headers should not be needed to build CCF apps #2428).
  • 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.

Activity

  1. changed the title [-]Clarify dependencies[/-] [+]Clarify dependencies between framework source components[/+] on Feb 4, 2022
  2. eddyashton commented on Feb 8, 2022

    @eddyashton
    MemberAuthor

    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:
    image

    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
  3. removed this from the 4.x milestone on Jan 25, 2024
  4. achamayou commented on Jul 10, 2026

    @achamayou
    Member

    Updated from main at 488f8da (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, 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.

  5. achamayou commented on Sep 2, 2026

    @achamayou
    Member

    Rerun against main at 9751a52 (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:

    • A 13-component cycle: common, consensus, enclave, endpoints, host, http, indexing, js, kv, node, quic, service, snapshots.
    • A 3-component cycle: crypto, ds, pal.
    • Eight acyclic components: 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 --> ds
    
    Loading

    Dashed arrows participate in a cycle.

    Compared with the July baseline at 488f8da:

    • clients/ was removed; msgpack/ and threading/ were added as internally independent components.
    • Moving cli_helper.h from ds/ to common/ removed ds -> crypto and ds -> service, splitting the giant cycle.
    • http -> pal and tasks -> pal were added.
    • ds/ now depends only on pal/, but remains cyclic through ds -> pal -> crypto -> ds.
    • kv/ remains coupled to crypto, ds, node, pal, and service; host/ remains in the main cyclic core.
  6. achamayou commented on Sep 4, 2026

    @achamayou
    Member

    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:

    • 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, and udp.

    Compared with the 2026-09-02 baseline at 9751a52:

    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
    
    Loading
  7. achamayou commented on Sep 9, 2026

    @achamayou
    Member

    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):

    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

    The current policies include:

    ds          -> {}
    crypto      -> {cose, ds}
    pal         -> {crypto, ds}
    uv          -> {}
    http        -> {ccf-api, crypto, ds}
    http_client -> {ccf-api, ds, uv}
    

    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.

    Next move: make snapshots acyclic in one small PR

    Its only outgoing edges into the cyclic core are:

    Edge to remove All direct include witnesses Actual dependency
    snapshots -> host filenames.h:6, snapshot_writer.h:8 The generic TimeBoundLogger helper
    snapshots -> consensus snapshot_writer.h:6 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.

    1. 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.
    2. 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.
    3. 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.
    4. 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.

  8. achamayou commented on Sep 13, 2026

    @achamayou
    Member

    2026-09-13: projected after #8338, left to right

    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.

    State Components Direct edges Components in cycles Acyclic components Outgoing policies
    Current main 25 106 11 14 14
    After #8338 25 104 10 15 15

    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.

  9. achamayou commented on Sep 17, 2026

    @achamayou
    Member

    2026-09-17: host and enclave are acyclic; indexing is now a one-include cut

    Measured on main at efc44e3 (#8396), using the same source-folder projection as the previous updates.

    State Components Direct edges Components in cycles Acyclic components Outgoing policies
    2026-09-13 projection, after #8338 25 104 10 15 15
    main at efc44e3 25 96 8 17 17
    Proposed move 1: indexing 25 95 7 18 18
    Proposed move 2: common (with or after #8309) 25 92 6 19 19

    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.

    Progress since the last update, per landed PR

    PR Removed source-component edges Added Result
    #8338 snapshots -> {consensus, host} none 104 edges, 10 cyclic, exactly as projected
    #7955 host -> indexing indexing -> tasks LFS access now goes through tasks instead of the ringbuffer
    #8117 enclave -> {tls, udp}, host -> udp, js -> tls, tcp -> ds, and the old tls/udp leaf edges host -> {http, tasks} udp and the old TLS wrapper component are deleted: 23 components, 98 edges, still 10 cyclic
    #8389 enclave -> host, node -> host, host -> {http, tasks, tcp} host -> ledger, node -> ledger, enclave -> tls, new ledger and tls leaf edges host leaves the core; ledger and the new tls are born acyclic: 25 components, 100 edges, 9 cyclic
    #8396 consensus -> enclave, node -> enclave, enclave -> {http, service, tcp, tls} node -> {tcp, tls} enclave leaves the core: 96 edges, 8 cyclic

    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:

    Single-edge cut Direct include witnesses Frees Note
    node -> indexing 1 indexing node_state.h:34
    node -> common 4 common all four are common/configuration.h; overlaps #8309
    node -> js 14 js governance handlers, js_policy.h, member/node frontends

    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):

    Core edge table
    Component Outgoing core edges (witnesses) Incoming core edges (witnesses)
    common consensus (1), node (1), service (6) node (4)
    consensus kv (5), node (6), service (5) common (1), node (8), service (1)
    endpoints kv (1), node (7), service (12) js (7), node (4)
    indexing kv (4), node (1) node (1)
    js endpoints (7), kv (4), node (9), service (4) node (14)
    kv node (4), service (5) consensus (5), endpoints (1), indexing (4), js (4), node (24), service (7)
    node common (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)
    service consensus (1), kv (7), node (3) common (6), consensus (5), endpoints (12), js (4), kv (5), node (99)

    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).

    1. 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.
    2. 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.
    3. 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:

    1. 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.
    2. Take StartType out of common/enclave_interface_types.h into ccf/node/start_type.h, as Unify operator and node startup configuration #8309 already does.
    3. 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 kv and service, 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.

  10. achamayou commented on Sep 18, 2026

    @achamayou
    Member

    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

    1. 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.

    2. Make kv a core library, coordinated with [WIP] Refactor Consensus API and KV Interaction #8190. Remove kv -> node and kv -> service, rather than trying to remove the natural consumer edge service -> kv:

      • 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.

    3. 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.

  11. achamayou commented on Sep 28, 2026

    @achamayou
    Member

    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).

    Metric 2026-09-18, f0ff3f7 Current, ff3966f
    Source components 25 26
    Direct internal edges 91 88
    Cyclic components 7 6
    Acyclic components 18 20
    Components with explicit policies 18 19

    Remaining SCC: consensus, endpoints, js, kv, node, service. scripts/check-source-dependencies.py passes.

    What changed

    PR Removed Added Result
    #8309 node -> common (all 4 witnesses), common -> {consensus, node, pal} none common leaves the core, policy common -> {crypto, ds, service} added. This was suggested cut 1 from the last update.
    #8200 none rust -> kv 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.

  12. achamayou commented on Sep 30, 2026

    @achamayou
    Member

    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.

    What changed

    PR Removed Added Result
    #8459 src/cose (Rust COSE FFI) deleted in favour of crypto/cose.h: crypto -> cose, node -> cose none 25 components, 86 edges
    #8463 kv -> node, kv -> service none 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.

    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:5
    
    Loading

    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:

    1. 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.
    2. 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.
    3. 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.
    4. 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

    After that

    • 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions