Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added temporal sharding support, allowing completed ledger ranges to be sealed into immutable, independently-addressable shards while the active shard continues accepting writes. Sharding is disabled by default for backward compatibility. New governance proposal actions: `seal_current_shard`, `set_shard_policy`. New `ccf.node.sealShard()`, `ccf.node.setShardPolicy()` JavaScript governance APIs. New `CCFConfig::Sharding` configuration section. Shard metadata is stored in public governance tables (`public:ccf.gov.shards.info`, `public:ccf.gov.shards.policy`). Auto-seal is supported based on seqno count or time thresholds. Sealed shard data is automatically archived to the first `ledger.read_only_directories` entry for shared access across nodes.
- Added support for inline transaction receipt construction at commit time. Endpoint authors can use `build_receipt_for_committed_tx()` to construct a full `TxReceiptImpl` from the `CommittedTxInfo` passed to their `ConsensusCommittedEndpointFunction` callback. See the logging sample app (`/log/blocking/private/receipt`) for example usage (#7785).

### Changed
Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,15 @@ if(BUILD_TESTS)
PRIVATE ccf_kv ccf_endpoints ccf_tasks
)

add_unit_test(
sharding_test
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/sharding.cpp
)
target_link_libraries(
sharding_test
PRIVATE ccf_kv ccf_endpoints ccf_tasks
)

add_unit_test(
node_info_json_test
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/node_info_json.cpp
Expand Down
30 changes: 30 additions & 0 deletions doc/host_config_schema/cchost_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,36 @@
"description": "This section includes configuration for periodic cleanup of old files (snapshots, ledger chunks)",
"additionalProperties": false
},
"sharding": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false,
"description": "Enable temporal sharding of the ledger."
},
"auto_seal_after_seqno_count": {
"type": "integer",
"default": 0,
"minimum": 0,
"description": "Automatically seal the active shard after this many committed sequence numbers. 0 disables auto-seal by count."
},
"auto_seal_after_duration_s": {
"type": "integer",
"default": 0,
"minimum": 0,
"description": "Automatically seal the active shard after this many seconds. 0 disables auto-seal by time."
},
"max_active_shard_memory_mb": {
"type": "integer",
"default": 0,
"minimum": 0,
"description": "Advisory memory limit for the active shard in megabytes. 0 means unlimited."
}
},
"description": "This section includes configuration for temporal sharding of the ledger",
"additionalProperties": false
},
"logging": {
"type": "object",
"properties": {
Expand Down
8 changes: 8 additions & 0 deletions doc/operations/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ This section describes how :term:`Operators` manage the different nodes constitu

---

:fa:`layer-group` :doc:`sharding`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Seal completed ledger ranges into immutable, independently-addressable shards.

---

:fa:`terminal` :doc:`operator_rpc_api`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -117,4 +124,5 @@ This section describes how :term:`Operators` manage the different nodes constitu
platforms/index
troubleshooting
resource_usage
sharding
operator_rpc_api
153 changes: 153 additions & 0 deletions doc/operations/sharding.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
Temporal Sharding
=================

Temporal sharding allows operators to seal completed time ranges of the ledger into immutable, independently-addressable shards. The active shard continues accepting writes while sealed shards are automatically archived to the shared read-only ledger directory for access by all nodes.

Sharding is disabled by default and has no effect on existing single-shard behaviour.

Concepts
--------

**Shard**
A contiguous range of ledger sequence numbers. At any time, exactly one shard is *active* (accepting writes). Older shards progress through *Sealing* and *Sealed* states.

**Shard seal**
The process of closing the active shard at a committed sequence number, triggering a snapshot at the boundary, rekeying the ledger secret, and opening a new active shard.

**Shard policy**
Governance-controlled thresholds that may trigger automatic sealing based on sequence number count or elapsed time.

Prerequisites
-------------

Sharding requires at least one entry in ``ledger.read_only_directories``. This is the shared mount path (e.g. a Kubernetes PVC) where all nodes look for committed ledger chunks. When a shard is sealed, its ledger chunks are hard-linked (or copied) into ``<first read_only_directory>/shards/<shard_id>/``, making sealed shard data automatically available to all nodes mounting the same volume.

If ``ledger.read_only_directories`` is empty when sharding is enabled, the node logs an error and sharding is not activated.

Configuration
-------------

Sharding is configured in the node's JSON configuration file:

.. code-block:: json

{
"ledger": {
"directory": "ledger",
"read_only_directories": ["/shared/ledger"]
},
"sharding": {
"enabled": true,
"auto_seal_after_seqno_count": 100000,
"auto_seal_after_duration_s": 3600,
"max_active_shard_memory_mb": 1024
}
}

.. list-table::
:header-rows: 1
:widths: 35 10 55

* - Field
- Default
- Description
* - ``enabled``
- ``false``
- Enable temporal sharding.
* - ``auto_seal_after_seqno_count``
- ``0``
- Automatically seal the active shard after this many committed sequence numbers. ``0`` disables auto-seal by count.
* - ``auto_seal_after_duration_s``
- ``0``
- Automatically seal the active shard after this many seconds. ``0`` disables auto-seal by time.
* - ``max_active_shard_memory_mb``
- ``0``
- Advisory memory limit for the active shard. ``0`` means unlimited.

When sharding is enabled, the initial shard (shard 0) is created automatically when the service transitions to open.

Governance Actions
------------------

Members can manage shards through governance proposals.

``seal_current_shard``
~~~~~~~~~~~~~~~~~~~~~~

Seals the currently active shard at the latest committed sequence number. This initiates a two-phase process: the shard is marked as **Sealing**, a snapshot is triggered at the boundary, and the ledger is rekeyed. Once the snapshot is committed asynchronously, the shard transitions to **Sealed** and its data is archived to the shared read-only ledger directory.

.. code-block:: json

{
"actions": [
{
"name": "seal_current_shard"
}
]
}

``set_shard_policy``
~~~~~~~~~~~~~~~~~~~~

Updates the shard policy. All fields are optional — unspecified fields default to ``0`` (disabled).

.. code-block:: json

{
"actions": [
{
"name": "set_shard_policy",
"args": {
"auto_seal_after_seqno_count": 100000,
"auto_seal_after_duration_s": 3600,
"max_active_shard_memory_mb": 1024
}
}
]
}

``migrate_shard`` has been removed — sealed shards are automatically archived to the first ``ledger.read_only_directories`` entry.

KV Tables
---------

Shard metadata is stored in public governance tables:

- ``public:ccf.gov.shards.info`` — Maps shard ID to ``ShardInfo`` (shard boundaries, status, snapshot seqno).
- ``public:ccf.gov.shards.policy`` — Singleton ``ShardPolicyInfo`` (auto-seal thresholds).

Sealed Shard Storage
--------------------

When a shard is sealed, the primary node sends a ``ledger_shard_sealed`` message to the host process. The host hard-links (or copies, if cross-device) all committed ledger chunk files covering the shard's sequence number range from ``ledger.directory`` into:

.. code-block:: text

<first read_only_directory>/shards/<shard_id>/

This means all nodes mounting the same shared volume automatically have access to sealed shard data for historical queries, without requiring explicit migration.

Shard Lifecycle
---------------

1. **Active** — The shard is open and accepting writes.
2. **Sealing** — A seal has been initiated. A snapshot is being taken at the shard boundary and the ledger is being rekeyed. The shard remains in this state until the snapshot is committed.
3. **Sealed** — The snapshot has been committed. The shard is immutable and its data has been archived to the shared read-only directory.

.. code-block:: text

Active ──seal_current_shard──> Sealing ──snapshot committed (async)──> Sealed

The transition from Sealing to Sealed is **asynchronous**: when the snapshotter commits the shard-seal snapshot, it fires a callback (``on_shard_seal_committed``) that updates the shard status in the KV and notifies the host to archive the ledger chunks. This ensures that sealed shard data is only archived after the boundary snapshot is durable.

Auto-seal
---------

When ``auto_seal_after_seqno_count`` or ``auto_seal_after_duration_s`` is configured and sharding is enabled, the primary node periodically checks these thresholds. If either threshold is exceeded, the active shard is sealed automatically without requiring a governance proposal.

Recovery
--------

During service recovery, the sharding state is restored from the KV store. The node identifies the current active shard and resumes from its start sequence number. Ledger secret chains are preserved across shard boundaries via the existing encrypted past ledger secret mechanism.

If a shard is found in the **Sealing** state during recovery (i.e. the node crashed after initiating a seal but before the snapshot was committed), the seal can be retried via a new ``seal_current_shard`` proposal once the service is open.
11 changes: 11 additions & 0 deletions include/ccf/node/startup_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ namespace ccf
bool operator==(const FilesCleanup&) const = default;
};
FilesCleanup files_cleanup = {};

struct Sharding
{
bool enabled = false;
size_t auto_seal_after_seqno_count = 0;
size_t auto_seal_after_duration_s = 0;
size_t max_active_shard_memory_mb = 0;

bool operator==(const Sharding&) const = default;
};
Sharding sharding = {};
};

struct RecoveryDecisionProtocolConfig
Expand Down
65 changes: 65 additions & 0 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1604,4 +1604,69 @@ const actions = new Map([
},
),
],
[
"seal_current_shard",
new Action(
function (args) {
checkNone(args);
},
function (args) {
ccf.node.sealShard();
},
),
],
[
"set_shard_policy",
new Action(
function (args) {
if (args.auto_seal_after_seqno_count !== undefined) {
checkType(
args.auto_seal_after_seqno_count,
"number",
"auto_seal_after_seqno_count",
);
checkBounds(
args.auto_seal_after_seqno_count,
0,
Number.MAX_SAFE_INTEGER,
"auto_seal_after_seqno_count",
);
}
if (args.auto_seal_after_duration_s !== undefined) {
checkType(
args.auto_seal_after_duration_s,
"number",
"auto_seal_after_duration_s",
);
checkBounds(
args.auto_seal_after_duration_s,
0,
Number.MAX_SAFE_INTEGER,
"auto_seal_after_duration_s",
);
}
if (args.max_active_shard_memory_mb !== undefined) {
checkType(
args.max_active_shard_memory_mb,
"number",
"max_active_shard_memory_mb",
);
checkBounds(
args.max_active_shard_memory_mb,
0,
Number.MAX_SAFE_INTEGER,
"max_active_shard_memory_mb",
);
}
},
function (args) {
const policy = {
auto_seal_after_seqno_count: args.auto_seal_after_seqno_count || 0,
auto_seal_after_duration_s: args.auto_seal_after_duration_s || 0,
max_active_shard_memory_mb: args.max_active_shard_memory_mb || 0,
};
ccf.node.setShardPolicy(ccf.jsonCompatibleToBuf(policy));
},
),
],
]);
65 changes: 65 additions & 0 deletions samples/minimal_ccf/app/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1511,4 +1511,69 @@ const actions = new Map([
function (args) {},
),
],
[
"seal_current_shard",
new Action(
function (args) {
checkNone(args);
},
function (args) {
ccf.node.sealShard();
},
),
],
[
"set_shard_policy",
new Action(
function (args) {
if (args.auto_seal_after_seqno_count !== undefined) {
checkType(
args.auto_seal_after_seqno_count,
"number",
"auto_seal_after_seqno_count",
);
checkBounds(
args.auto_seal_after_seqno_count,
0,
Number.MAX_SAFE_INTEGER,
"auto_seal_after_seqno_count",
);
}
if (args.auto_seal_after_duration_s !== undefined) {
checkType(
args.auto_seal_after_duration_s,
"number",
"auto_seal_after_duration_s",
);
checkBounds(
args.auto_seal_after_duration_s,
0,
Number.MAX_SAFE_INTEGER,
"auto_seal_after_duration_s",
);
}
if (args.max_active_shard_memory_mb !== undefined) {
checkType(
args.max_active_shard_memory_mb,
"number",
"max_active_shard_memory_mb",
);
checkBounds(
args.max_active_shard_memory_mb,
0,
Number.MAX_SAFE_INTEGER,
"max_active_shard_memory_mb",
);
}
},
function (args) {
const policy = {
auto_seal_after_seqno_count: args.auto_seal_after_seqno_count || 0,
auto_seal_after_duration_s: args.auto_seal_after_duration_s || 0,
max_active_shard_memory_mb: args.max_active_shard_memory_mb || 0,
};
ccf.node.setShardPolicy(ccf.jsonCompatibleToBuf(policy));
},
),
],
]);
Loading
Loading