Skip to content

Commit

Permalink
Distinguish public ReadOnlyStore from private Store (#3632)
Browse files Browse the repository at this point in the history
  • Loading branch information
eddyashton authored Mar 28, 2022
1 parent f177477 commit cedbd7d
Show file tree
Hide file tree
Showing 34 changed files with 322 additions and 175 deletions.
2 changes: 1 addition & 1 deletion .daily_canary
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Run!!!!!
Run!!!!
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- New `GET /node/consensus` endpoint now also returns primary node ID and current view (#3666).
- The `enclave::` namespace has been removed, and all types which were under it are now under `ccf::`. This will affect any apps using `enclave::RpcContext`, which should be replaced with `ccf::RpcContext` (#3664).
- HTTP parsing errors are now recorded per-interface and returned by `GET /node/metrics` (#3671).
- The `kv::Store` type is no longer visible to application code, and is replaced by a simpler `kv::ReadOnlyStore`. This is the interface given to historical queries to access historical state and enforces read-only access, without exposing internal implementation details of the store. This should have no impact on JS apps, but C++ apps will need to replace calls to `store->current_txid()` with calls to `store->get_txid()`, and `store->create_tx()` to `store->create_read_only_tx()`.

## [2.0.0-rc4]

Expand Down
2 changes: 1 addition & 1 deletion doc/build_apps/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This section describes how CCF applications can be developed and deployed to a C

Applications can be written in JavaScript/TypeScript or C++. An application consists of a collection of endpoints that can be triggered by :term:`Users`. Each endpoint can define an :ref:`build_apps/logging_cpp:API Schema` to validate user requests.

These endpoints can read or mutate the state of a unique :ref:`build_apps/kv/index:Key-Value Store` that represents the internal state of the application. Applications define a set of ``Maps`` (see :ref:`build_apps/kv/kv_how_to:Creating a Map`), mapping from a key to a value. When an application endpoint is triggered, the effects on the Store are committed atomically.
These endpoints can read or mutate the state of a unique :ref:`build_apps/kv/index:Key-Value Store` that represents the internal state of the application. Applications define a set of ``Maps`` (see :doc:`kv/kv_how_to`), mapping from a key to a value. When an application endpoint is triggered, the effects on the Store are committed atomically.

.. panels::

Expand Down
9 changes: 1 addition & 8 deletions doc/build_apps/kv/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ This page presents the API that a CCF application must use to access and mutate

A CCF application should store its data in one or more :cpp:type:`kv::Map`. The name, type, and serialisation of these maps is under the application's control. Each invocation of an :cpp:class:`ccf::EndpointRegistry::Endpoint` is given a :cpp:class:`kv::Tx` transaction object, through which it can read and write to its :cpp:type:`kv::Map`.

Store
-----

.. doxygenclass:: kv::Store
:project: CCF
:members: create, get

Map
---

Expand Down Expand Up @@ -48,7 +41,7 @@ Transaction

.. doxygenclass:: kv::Tx
:project: CCF
:members: rw, ro, wo
:members: rw, wo

Handles
-------
Expand Down
56 changes: 25 additions & 31 deletions doc/build_apps/kv/kv_how_to.rst
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
Key-Value Store How-To
======================

The Key-Value :cpp:class:`kv::Store` is a collection of :cpp:type:`kv::Map` objects that are available from all the end-points of an application. There is one unique ``Store`` created in the enclave of each node that is passed to the constructor of all applications.
The `Key-Value Store` (KV) consists of a set of :cpp:type:`kv::Map` objects that are available to the endpoints of an application. Endpoint handlers create handles which allow them to read and write from these :cpp:type:`kv::Map` objects. The framework handles conflicts between concurrent execution of multiple transactions, and produces a consistent order of transactions which is replicated between nodes, allowing the entries to be read from multiple nodes. This page outlines the core concepts and C++ APIs used to interact with the KV.

.. code-block:: cpp
Store tables;
Creating a Map
--------------
Map Naming
----------

A :cpp:type:`kv::Map` (often referred to as a ``Table``) is a collection of key-value pairs of a given type. The :cpp:type:`kv::Map` itself is identified by its name, which is used to lookup the map :cpp:type:`kv::Map` in a :cpp:class:`kv::Store` during a transaction.
A :cpp:type:`kv::Map` (often referred to as a `Table`) is a collection of key-value pairs of a given type. The :cpp:type:`kv::Map` itself is identified by its name, which is used to lookup the map :cpp:type:`kv::Map` from the local store during a transaction.

If a ``Map`` with the given name did not previously exist, it will be created in this transaction..
If a :cpp:type:`kv::Map` with the given name did not previously exist, it will be created in this transaction.

A ``Map`` can either be created as private (default) or public. Public map's names begin with a ``public:`` prefix, any any other name indicates a private map. Transactions on private maps are written to the ledger in encrypted form and can only be decrypted in the enclave of the nodes that have joined the network. Transactions on public maps are written to the ledger as plaintext and can be read from outside the enclave (only their integrity is protected). The security domain of a map (public or private) cannot be changed after its creation, since this is encoded in the map's name. Public and private maps with similar names are distinct; writes to "public:foo" have no impact on "foo", and vice versa.
A :cpp:type:`kv::Map` can either be created as private (default) or public. Public map's names begin with a ``public:`` prefix, any any other name indicates a private map. For instance the name ``public:foo`` to a public map, while ``foo`` refers to a private map. Transactions on private maps are written to the ledger in encrypted form and can only be decrypted in the enclave of nodes that have joined the network. Transactions on public maps are written to the ledger as plaintext and can be read from outside the enclave; only their integrity is protected. The security domain of a map (public or private) cannot be changed after its creation, since this is encoded in the map's name. Public and private maps with similar names in different domains are distinct; writes to ``public:foo`` have no impact on ``foo``, and vice versa.

Transaction Semantics
---------------------

Accessing the Transaction
-------------------------
A transaction (:cpp:class:`kv::Tx`) encapsulates an individual endpoint invocation's atomic interaction with the KV. Transactions may read from and write to multiple :cpp:type:`kv::Map`, and each is automatically ordered, applied, serialised, and committed by the framework.

A :cpp:class:`kv::Tx` corresponds to the atomic operations that can be executed on the Key-Value ``Store``. A transaction can affect one or multiple ``Map`` and are automatically committed by CCF once the endpoint's handler returns successfully.
A reference to a new :cpp:class:`kv::Tx` is passed to each endpoint handler, and used to interact with the KV.

A single ``Transaction`` (``tx``) is passed to each endpoint of an application and should be used to interact with the Key-Value ``Store``.
Each :cpp:class:`kv::Tx` gets a consistent, opaque view of the KV, including the values which have been left by previous writes. Any writes produced by this transaction will be visible to all future transactions.

When the end-point successfully completes, the node on which the end-point was triggered attempts to commit the transaction to apply the changes to the Store. Once the transaction is committed successfully, it is automatically replicated by CCF and should globally commit.
When the endpoint handler indicates that its :cpp:class:`kv::Tx` should be applied (see :ref:`this section <build_apps/kv/kv_how_to:Applying and reverting writes>` for details), the executing node attempts to apply the changes to its local KV. If this produces conflicts with concurrently executing transactions, it will be automatically re-executed. Once the transaction is applied successfully, it is automatically replicated to other nodes and will, if the network is healthy, eventually be globally committed.

For each :cpp:type:`kv::Map` that a transaction wants to write to or read from, a :cpp:class:`kv::MapHandle` must first be acquired. These are acquired from the :cpp:func:`kv::Tx::rw` (`read-write`) method. These may be acquired either by name (in which case the desired type must be explicitly specified as a template parameter), or by using a :cpp:type:`kv::Map` instance which defines both the map's name and key-value types.

Expand All @@ -39,7 +36,7 @@ By name:
auto map2_handle = tx.rw<kv::Map<string, uint64_t>>("public:map2");
auto map3_handle = tx.rw<kv::Map<uint64_t, MyCustomClass>>("map3");
By ``Map``:
By :cpp:type:`kv::Map`:

.. code-block:: cpp
Expand All @@ -54,19 +51,18 @@ By ``Map``:
The latter approach introduces a named binding between the map's name and the types of its keys and values, reducing the chance for errors where code attempts to read a map with the wrong type.

As noted above, this access may cause the ``Map`` to be created, if it did not previously. In fact all ``Maps`` are created like this, in the first transaction in which they are written to. Within a transaction, a newly created ``Map`` behaves exactly the same as an existing ``Map`` with no keys - the framework views these as semantically identical, and offers no way for the application logic to tell them apart. Any writes to a newly created ``Map`` will be persisted when the transaction commits, and future transactions will be able to access this ``Map`` by name to read those writes.

.. note:: As mentioned above, there is no need to explicitly declare a :cpp:type:`kv::Map` before it is used. The first write to a :cpp:type:`kv::Map` implicitly creates it in the underlying KV. Within a transaction, a newly created :cpp:type:`kv::Map` behaves exactly the same as an existing :cpp:type:`kv::Map` with no keys - the framework views these as semantically identical, and offers no way for the application logic to tell them apart. Any writes to a newly created :cpp:type:`kv::Map` will be persisted when the transaction commits, and future transactions will be able to access this :cpp:type:`kv::Map` by name to read those writes.

Accessing Map content via a Handle
----------------------------------

Once a :cpp:class:`kv::MapHandle` on a specific :cpp:type:`kv::Map` has been obtained, it is possible to:

- test (:cpp:func:`kv::MapHandle::has`) whether a key has any associated value;
- read (:cpp:func:`kv::MapHandle::get`) the value associated with a key;
- write (:cpp:func:`kv::MapHandle::put`) a new value for a key;
- delete (:cpp:func:`kv::MapHandle::remove`) a key and its current value;
- iterate (:cpp:func:`kv::MapHandle::foreach`) through all key-value pairs.
- test (:cpp:func:`kv::ReadableMapHandle::has`) whether a key has any associated value;
- read (:cpp:func:`kv::ReadableMapHandle::get`) the value associated with a key;
- write (:cpp:func:`kv::WriteableMapHandle::put`) a new value for a key;
- delete (:cpp:func:`kv::WriteableMapHandle::remove`) a key and its current value;
- iterate (:cpp:func:`kv::ReadableMapHandle::foreach`) through all key-value pairs.

.. code-block:: cpp
Expand All @@ -93,7 +89,7 @@ Once a :cpp:class:`kv::MapHandle` on a specific :cpp:type:`kv::Map` has been obt
Read/Write safety
-----------------

If you are only reading from or only writing to a given :cpp:type:`kv::Map` you can retrieve a `read-only` or `write-only` handle for it, turning unexpected reads/writes (which would introduce unintended dependencies between transactions) into compile-time errors. Instead of calling :cpp:func:`kv::Tx::rw` to get a handle which can both read and write, you can call :cpp:func:`kv::Tx::ro` to acquire a read-only handle or :cpp:func:`kv::Tx::wo` to acquire a write-only handle.
If you are only reading from or only writing to a given :cpp:type:`kv::Map` you can retrieve a `read-only` or `write-only` handle for it. This will turn unexpected reads/writes (which would introduce unintended dependencies between transactions) into compile-time errors. Instead of calling :cpp:func:`kv::Tx::rw` to get a handle which can both read and write, you can call :cpp:func:`kv::ReadOnlyTx::ro` to acquire a `read-only` handle or :cpp:func:`kv::Tx::wo` to acquire a `write-only` handle.

.. code-block:: cpp
Expand Down Expand Up @@ -124,11 +120,11 @@ Note that, as in the sample above, it is possible to acquire different kinds of
Removing a key
--------------

If a Key-Value pair was written to a ``Map`` by a previous ``Transaction``, it is possible to delete this key. Because of the append-only nature of the ``Store``, this Key-Value pair is not actually removed from the ``Map`` but instead explicitly marked as deleted from the version that the corresponding ``Transaction`` is committed at.
If a Key-Value pair was written to a :cpp:type:`kv::Map` by a previous :cpp:class:`kv::Tx`, it is possible to delete this key. Because of the append-only nature of the KV, this Key-Value pair is not actually removed from the :cpp:type:`kv::Map` but instead explicitly marked as deleted in the version that the deleting :cpp:class:`kv::Tx` is applied at.

.. code-block:: cpp
// In transaction A, assuming that "key1" has already been committed
// In transaction A, assuming that "key1" has already been written to
auto handle = tx.rw(map_priv);
auto v = handle->get("key1"); // v.value() == "value1"
handle->remove("key1");
Expand All @@ -141,8 +137,7 @@ If a Key-Value pair was written to a ``Map`` by a previous ``Transaction``, it i
Global commit
-------------

A ``Map`` is globally committed at a specific :cpp:type:`kv::Version` when it is not possible to access the state of that ``Map`` prior to that version.
This is useful when it is certain that the state of the ``Store`` prior to a specific version will never need to be read or modified. A transaction is automatically globally committed once the consensus protocol has established that a majority of nodes in the CCF network have successfully received and acknowledged that transaction.
A transaction is automatically globally committed once the consensus protocol has established that a majority of nodes in the CCF network have successfully received and acknowledged that transaction. To operate on durable state, an application may want to query the globally committed state rather than the current state of the KV.

The :cpp:func:`kv::MapHandle::get_globally_committed` member function returns the value of a key that we know has been globally committed.

Expand Down Expand Up @@ -171,7 +166,7 @@ Miscellaneous

Values can only be retrieved directly (:cpp:func:`kv::MapHandle::get`) for a given target key. However, it is sometimes necessary to access unknown keys, or to iterate through all Key-Value pairs.

CCF offers a member function :cpp:func:`kv::MapHandle::foreach` to iterate over all the elements written to that ``Map`` so far, and run a lambda function for each Key-Value pair. Note that a :cpp:class:`kv::MapHandle::foreach` loop can be ended early by returning ``false`` from this lambda, while ``true`` should be returned to continue iteration.
CCF offers a member function :cpp:func:`kv::MapHandle::foreach` to iterate over all the elements written to that :cpp:type:`kv::Map` so far, and run a lambda function for each Key-Value pair. Note that a :cpp:class:`kv::MapHandle::foreach` loop can be ended early by returning ``false`` from this lambda, while ``true`` should be returned to continue iteration.

.. code-block:: cpp
Expand All @@ -189,14 +184,13 @@ CCF offers a member function :cpp:func:`kv::MapHandle::foreach` to iterate over
if (/* condition*/)
{
return false;
}
});
Applying and reverting writes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Changes to the ``Store`` are made by atomic transactions. For a given :cpp:class:`kv::Tx`, either all of its writes are applied, or none are. Only applied writes are replicated and may be globally committed. Transactions may be abandoned without applying their writes - their changes will never be seen by other transactions.
Changes to the KV are made by atomic transactions. For a given :cpp:class:`kv::Tx`, either all of its writes are applied, or none are. Only applied writes are replicated and may be globally committed. Transactions may be abandoned without applying their writes - their changes will never be seen by other transactions.

By default CCF decides which transactions are successful (so should be applied to the persistent store) by looking at the status code contained in the response: all transactions producing ``2xx`` status codes will be applied, while any other status code will be treated as an error and will `not` be applied to the persistent store. If this behaviour is not desired, for instance when an app wants to log incoming requests even though they produce an error, then it can be dynamically overridden by explicitly telling CCF whether it should apply a given transaction:

Expand Down
2 changes: 1 addition & 1 deletion doc/build_apps/logging_cpp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The Logging application simply has:

.. note::

:cpp:class:`kv::Store` tables are the only interface between CCF and the replicated application, and the sole mechanism for it to have distributed state.
:cpp:class:`kv::Map` tables are the only interface between CCF and the replicated application, and the sole mechanism for it to have distributed state.

The Logging application keeps its state in a pair of tables, one containing private encrypted logs and the other containing public unencrypted logs. Their type is defined as:

Expand Down
Loading

0 comments on commit cedbd7d

Please sign in to comment.