зеркало из https://github.com/microsoft/CCF.git
Support for shareless members (#1866)
This commit is contained in:
Родитель
82e4d007fd
Коммит
7afef2cc2b
|
@ -773,6 +773,12 @@ if(BUILD_TESTS)
|
|||
CONSENSUS cft
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME membership
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/membership.py
|
||||
CONSENSUS cft
|
||||
)
|
||||
|
||||
if(NOT SAN)
|
||||
add_e2e_test(
|
||||
NAME connections_cft
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
Running CCF Applications
|
||||
========================
|
||||
|
||||
.. note:: For a quick and easy way to run a CCF application locally, try :ref:`starting a test network <quickstart/test_network:Starting a Test Network>`, specifying the desired enclave image.
|
||||
.. note:: For a quick and easy way to run a CCF application locally, try :doc:`/quickstart/test_network`, specifying the desired enclave image.
|
||||
|
||||
Debugging
|
||||
---------
|
||||
|
||||
To connect a debugger to a CCF node, the configuration passed to `oesign sign` must have debugging enabled (``Debug=1``). This `must` be disabled for production enclaves, to ensure confidentiality is maintained. If using the ``sign_app_library`` function defined in ``ccf_app.cmake``, two variants will be produced for each enclave. ``name.enclave.so.debuggable`` will have debugging enabled (meaning a debugger may be attached - the optimisation level is handled independently), while ``name.enclave.so.signed`` produces a final debugging-disabled enclave. The produced binaries are otherwise identical.
|
||||
To connect a debugger to a CCF node, the configuration passed to ``oesign sign`` must have debugging enabled (``Debug=1``). This `must` be disabled for production enclaves, to ensure confidentiality is maintained. If using the ``sign_app_library`` function defined in ``ccf_app.cmake``, two variants will be produced for each enclave. ``name.enclave.so.debuggable`` will have debugging enabled (meaning a debugger may be attached - the optimisation level is handled independently), while ``name.enclave.so.signed`` produces a final debugging-disabled enclave. The produced binaries are otherwise identical.
|
||||
|
||||
Additionally, the ``cchost`` binary must be told that the enclave type is debug:
|
||||
|
||||
|
|
|
@ -47,9 +47,9 @@ Once the proposal to recover the network has passed under the rules of the :term
|
|||
Submitting Recovery Shares
|
||||
--------------------------
|
||||
|
||||
To restore private transactions and complete the recovery procedure, members should submit their recovery shares. The number of members required to submit their shares is set by the ``recovery_threshold`` CCF configuration parameter and :ref:`can be updated by the consortium at any time <members/common_member_operations:Updating Recovery Threshold>`.
|
||||
To restore private transactions and complete the recovery procedure, recovery members (i.e. members whose public encryption key has been registered in CCF) should submit their recovery shares. The number of members required to submit their shares is set by the ``recovery_threshold`` CCF configuration parameter and :ref:`can be updated by the consortium at any time <members/common_member_operations:Updating Recovery Threshold>`.
|
||||
|
||||
.. note:: The members who submit their recovery shares do not necessarily have to be the members who previously accepted the recovery.
|
||||
.. note:: The recovery members who submit their recovery shares do not necessarily have to be the members who previously accepted the recovery.
|
||||
|
||||
The recovery share retrieval, decryption and submission steps are conveniently performed by the ``submit_recovery_share.sh`` script as follows:
|
||||
|
||||
|
@ -77,7 +77,7 @@ When the recovery threshold is reached, the ``POST recovery_share`` RPC returns
|
|||
|
||||
Once the recovery of the private ledger is complete on a quorum of nodes that have joined the new network, the ledger is fully recovered and users are able to continue issuing business transactions.
|
||||
|
||||
.. note:: Recovery shares are updated every time a new member is added or retired and when the ledger is rekeyed. It also possible for members to update the recovery shares via the ``update_recovery_shares`` proposal.
|
||||
.. note:: Recovery shares are updated every time a new recovery member is added or retired and when the ledger is rekeyed. It also possible for members to update the recovery shares via the ``update_recovery_shares`` proposal.
|
||||
|
||||
Summary Diagram
|
||||
---------------
|
||||
|
|
|
@ -14,16 +14,19 @@ The ``keygenerator.sh`` script can be used to generate the member’s certificat
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ keygenerator.sh --name member_name --gen-enc-key
|
||||
$ keygenerator.sh --name member_name [--gen-enc-key]
|
||||
-- Generating identity private key and certificate for participant "member_name"...
|
||||
Identity curve: secp384r1
|
||||
Identity private key generated at: member_name_privk.pem
|
||||
Identity certificate generated at: member_name_cert.pem (to be registered in CCF)
|
||||
# Only if --gen-enc-key is used:
|
||||
-- Generating RSA encryption key pair for participant "member_name"...
|
||||
Encryption private key generated at: member_name_enc_privk.pem
|
||||
Encryption public key generated at: member_name_enc_pubk.pem (to be registered in CCF)
|
||||
|
||||
The member’s private keys (e.g. ``member_name_privk.pem`` and ``member_name_enc_privk.pem``) should be stored on a trusted device while the certificate (e.g. ``member_name_cert.pem``) and public encryption key (e.g. ``member_name_enc_pubk.pem``) should be registered in CCF by members.
|
||||
Members that are registered in CCF `with` a public encryption key are recovery members. Each recovery member is given a recovery share (see :ref:`members/accept_recovery:Submitting Recovery Shares`) that can be used to recover a defunct service. Members registered `without` a public encryption key are not given recovery shares and cannot recover the defunct service.
|
||||
|
||||
The member’s identity and encryption private keys (e.g. ``member_name_privk.pem`` and ``member_name_enc_privk.pem``) should be stored on a trusted device (e.g. HSM) while the certificate (e.g. ``member_name_cert.pem``) and public encryption key (e.g. ``member_name_enc_pubk.pem``) should be registered in CCF by members.
|
||||
|
||||
.. note:: See :ref:`design/cryptography:Algorithms and Curves` for the list of supported cryptographic curves for member identity.
|
||||
|
||||
|
|
|
@ -25,17 +25,22 @@ To create a new CCF network, the first node of the network should be invoked wit
|
|||
[--sig-ms-interval number_of_milliseconds]
|
||||
start
|
||||
--network-cert-file /path/to/network_certificate
|
||||
--member-info /path/to/member1_cert,/path/to/member1_enc_pub[,</path/to/member1_data>]
|
||||
[--member-info /path/to/member2_cert,/path/to/member2_enc_pub ...]
|
||||
--member-info /path/to/member1_cert[,/path/to/member1_enc_pubk[,/path/to/member1_data]]
|
||||
[--member-info /path/to/member2_cert[,/path/to/member2_enc_pubk[,/path/to/member2_data]] ...]
|
||||
--gov-script /path/to/lua/governance_script
|
||||
|
||||
CCF nodes can be started by using IP Addresses (both IPv4 and IPv6 are supported) or by specifying a fully qualified domain name. If an FQDN is used then ``--domain`` should be passed to the node at startup. Once a DNS has been setup it will be possible to connect to the node over TLS by using the node's domain name.
|
||||
|
||||
When starting up, the node generates its own key pair and outputs the certificate associated with its public key at the location specified by ``--node-cert-file``. The certificate of the freshly-created CCF network is also output at the location specified by ``--network-cert-file``.
|
||||
|
||||
.. note:: The network certificate should be distributed to users and members to be used as the certificate authority (CA) when establishing a TLS connection with any of the nodes part of the CCF network. When using curl, this is passed as the ``--cacert`` argument.
|
||||
.. note:: The network certificate should be distributed to users and members to be used as the certificate authority (CA) when establishing a TLS connection with any of the nodes part of the CCF network. When using ``curl``, this is passed as the ``--cacert`` argument.
|
||||
|
||||
The certificates and recovery public keys of initial members of the consortium are specified via ``--member-info``. For example, if 3 members should be added to CCF, operators should specify ``--member-info member1_cert.pem,member1_enc_pubk.pem``, ``--member-info member2_cert.pem,member2_enc_pubk.pem``, ``--member-info member3_cert.pem,member3_enc_pubk.pem``.
|
||||
The certificates, encryption public keys and member data of initial members of the consortium are specified via ``--member-info``. For example:
|
||||
|
||||
- A recovery member with member data: ``--member-info member_cert.pem,member_enc_pubk.pem,member_data.json``
|
||||
- A recovery member with no member data: ``--member-info member_cert.pem,member_enc_pubk.pem``
|
||||
- A non-recovery member with member data: ``--member-info member_cert.pem,,member_data.json`` (note the empty public encryption key path "``,,``")
|
||||
- A non-recovery member with no member data: ``--member-info member_cert.pem``
|
||||
|
||||
The :term:`Constitution`, as defined by the initial members, should be passed via the ``--gov-script`` option.
|
||||
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
Starting a Test Network
|
||||
=======================
|
||||
Using CCF Sandbox Environment
|
||||
=============================
|
||||
|
||||
.. note:: Before starting a CCF test network, make sure that:
|
||||
.. note:: Before starting a CCF sandbox environment, make sure that:
|
||||
|
||||
- The CCF runtime environment has successfully been setup (see :ref:`environment setup instructions <quickstart/run_setup:Setup CCF Runtime Environment>`).
|
||||
- CCF is installed (see :ref:`installation steps <quickstart/install:Install>`)
|
||||
|
||||
The quickest way to start a CCF test network is to use the `sandbox.sh <https://github.com/microsoft/CCF/blob/master/tests/sandbox/sandbox.sh>`_ test script, specifying the :doc:`enclave image </developers/index>` to run.
|
||||
The quickest way to start a CCF sandbox is to use the `sandbox.sh <https://github.com/microsoft/CCF/blob/master/tests/sandbox/sandbox.sh>`_ test script, specifying the :doc:`enclave image </developers/index>` to run.
|
||||
|
||||
The script creates a new one node test CCF network running locally. All the governance requests required to open the network to users are automatically issued.
|
||||
The script creates a new one node CCF test network running locally. All the governance requests required to open the network to users are automatically issued.
|
||||
|
||||
For example, deploying the ``liblogging`` example application:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cd CCF/build
|
||||
$ ../sandbox.sh --package ./liblogging.virtual.so
|
||||
$ /opt/ccf/bin/sandbox.sh --package ./liblogging.virtual.so
|
||||
Setting up Python environment...
|
||||
Python environment successfully setup
|
||||
[16:14:05.294] Starting 1 CCF nodes...
|
||||
|
@ -29,11 +28,11 @@ For example, deploying the ``liblogging`` example application:
|
|||
|
||||
.. note::
|
||||
|
||||
- `sandbox.sh` defaults to using CCF's `virtual` mode, which does not require or make use of SGX. To load debug or release enclaves and make use of SGX, `--enclave-type` must be set to the right value, for example: `./sandbox.sh --enclave-type release -p ./liblogging.enclave.so.signed`
|
||||
- ``sandbox.sh`` defaults to using CCF's `virtual` mode, which does not require or make use of SGX. To load debug or release enclaves and make use of SGX, ``--enclave-type`` must be set to the right value, for example: ``sandbox.sh --enclave-type release -p ./liblogging.enclave.so.signed``
|
||||
- The ``--verbose`` argument can be used to display all commands issued by operators and members to start the network.
|
||||
- Snapshots can be generated at regular intervals by the primary node of the service using the ``--snapshot-tx-interval <interval>`` option.
|
||||
|
||||
The log files (``out`` and ``err``) and ledger directory (``<node_id>.ledger``) for each CCF node can be found under ``./workspace/test_network_<node_id>``.
|
||||
The log files (``out`` and ``err``) and ledger directory (``<node_id>.ledger``) for each CCF node can be found under ``./workspace/sandbox_<node_id>``.
|
||||
|
||||
.. note:: The first time the command is run, a Python virtual environment will be created. This may take a few seconds. It will not be run the next time the ``sandbox.sh`` script is started.
|
||||
|
||||
|
@ -48,10 +47,9 @@ Additionally, if snapshots were generated by the defunct service (using the ``--
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cd CCF/build
|
||||
$ cp -r ./workspace/sandbox_0/0.ledger .
|
||||
$ cp -r ./workspace/sanbox_0/snapshots . # Optional, only if snapshots are available
|
||||
$ ./sandbox.sh -e release -p liblogging.enclave.so.signed --recover --ledger-dir 0.ledger --common-dir ./workspace/sandbox_common/ [--snapshot-dir snapshots]
|
||||
$ cp -r ./workspace/sandbox_0/snapshots . # Optional, only if snapshots are available
|
||||
$ /opt/ccf/bin/sandbox.sh -p ./liblogging.virtual.so --recover --ledger-dir 0.ledger --common-dir ./workspace/sandbox_common/ [--snapshot-dir snapshots]
|
||||
Setting up Python environment...
|
||||
Python environment successfully setup
|
||||
[16:24:29.563] Starting 1 CCF nodes...
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Python Client Tutorial
|
||||
======================
|
||||
|
||||
This tutorial describes how a Python client can securely issue requests to a running CCF network. It is assumed that the CCF network has already been started (e.g. after having :ref:`deployed a test network <quickstart/test_network:Starting a Test Network>`).
|
||||
This tutorial describes how a Python client can securely issue requests to a running CCF network. It is assumed that the CCF network has already been started (e.g. after having :doc:`deployed a sandbox service </quickstart/test_network>`).
|
||||
|
||||
.. note:: See :ref:`Python Client API <users/python_api:Python Client API>` for the complete API specification.
|
||||
|
||||
|
@ -26,7 +26,7 @@ Set the following CCF node variables:
|
|||
port = <node-port> # Node port (int)
|
||||
ca = "<path/to/network/cert>" # Network certificate path
|
||||
|
||||
.. note:: :ref:`When starting a test network <quickstart/test_network:Starting a Test Network>`, use any node's IP address and port number. All certificates and keys can be found in the associated ``common_dir`` folder.
|
||||
.. note:: :doc:`When starting a CCF sandbox </quickstart/test_network>`, use any node's IP address and port number. All certificates and keys can be found in the associated ``common_dir`` folder.
|
||||
|
||||
Create a new :py:class:`ccf.clients.CCFClient` instance which will create a secure TLS connection to the target node part of the network specified via ``ca``:
|
||||
|
||||
|
|
|
@ -190,13 +190,19 @@ def cli_proposal(func):
|
|||
|
||||
@cli_proposal
|
||||
def new_member(
|
||||
member_cert_path: str, member_enc_pubk_path: str, member_data: Any = None, **kwargs
|
||||
member_cert_path: str,
|
||||
member_enc_pubk_path: Optional[str] = None,
|
||||
member_data: Any = None,
|
||||
**kwargs,
|
||||
):
|
||||
LOG.debug("Generating new_member proposal")
|
||||
|
||||
# Read certs
|
||||
member_cert = open(member_cert_path).read()
|
||||
encryption_pub_key = open(member_enc_pubk_path).read()
|
||||
|
||||
encryption_pub_key = None
|
||||
if member_enc_pubk_path is not None:
|
||||
encryption_pub_key = open(member_enc_pubk_path).read()
|
||||
|
||||
# Script which proposes adding a new member
|
||||
proposal_script_text = """
|
||||
|
@ -215,6 +221,18 @@ def new_member(
|
|||
}
|
||||
|
||||
# Sample vote script which checks the expected member is being added, and no other actions are being taken
|
||||
|
||||
verify_encryption_pubk_text = (
|
||||
f"""
|
||||
expected_enc_pub_key = [====[{encryption_pub_key}]====]
|
||||
if not call.args.encryption_pub_key == expected_enc_pub_key then
|
||||
return false
|
||||
end
|
||||
"""
|
||||
if encryption_pub_key is not None
|
||||
else ""
|
||||
)
|
||||
|
||||
verifying_vote_text = f"""
|
||||
tables, calls = ...
|
||||
if #calls ~= 1 then
|
||||
|
@ -231,10 +249,7 @@ def new_member(
|
|||
return false
|
||||
end
|
||||
|
||||
expected_enc_pub_key = [====[{encryption_pub_key}]====]
|
||||
if not call.args.encryption_pub_key == expected_enc_pub_key then
|
||||
return false
|
||||
end
|
||||
{verify_encryption_pubk_text}
|
||||
|
||||
return true
|
||||
"""
|
||||
|
|
|
@ -106,7 +106,6 @@ struct NodeContext : public ccfapp::AbstractNodeContext
|
|||
|
||||
auto user_caller = kp -> self_sign("CN=name");
|
||||
auto user_caller_der = tls::make_verifier(user_caller) -> der_cert_data();
|
||||
std::vector<uint8_t> dummy_enc_pubk = {1, 2, 3};
|
||||
|
||||
auto init_frontend(
|
||||
NetworkTables& network,
|
||||
|
@ -123,7 +122,7 @@ auto init_frontend(
|
|||
for (uint8_t i = 0; i < n_members; i++)
|
||||
{
|
||||
member_certs.push_back(kp->self_sign("CN=name_member"));
|
||||
gen.add_member(member_certs.back(), dummy_enc_pubk);
|
||||
gen.add_member(member_certs.back());
|
||||
}
|
||||
|
||||
if (created_members != nullptr)
|
||||
|
@ -310,18 +309,14 @@ TEST_CASE("simple lua apps")
|
|||
auto get_ctx = enclave::make_rpc_context(user_session, packed);
|
||||
// expect to see 3 members in state active
|
||||
map<string, MemberInfo> expected = {
|
||||
{"0",
|
||||
{active_members[0], dummy_enc_pubk, nullptr, MemberStatus::ACCEPTED}},
|
||||
{"1",
|
||||
{active_members[1], dummy_enc_pubk, nullptr, MemberStatus::ACCEPTED}},
|
||||
{"2",
|
||||
{active_members[2], dummy_enc_pubk, nullptr, MemberStatus::ACCEPTED}}};
|
||||
{"0", {{active_members[0]}, MemberStatus::ACCEPTED}},
|
||||
{"1", {{active_members[1]}, MemberStatus::ACCEPTED}},
|
||||
{"2", {{active_members[2]}, MemberStatus::ACCEPTED}}};
|
||||
check_success(frontend->process(get_ctx).value(), expected);
|
||||
|
||||
// (2) try to write to members table
|
||||
const auto put_packed = make_pc(
|
||||
"put_member",
|
||||
{{"k", 99}, {"v", MemberInfo{{}, {}, nullptr, MemberStatus::ACCEPTED}}});
|
||||
"put_member", {{"k", 99}, {"v", MemberInfo{{}, MemberStatus::ACCEPTED}}});
|
||||
auto put_ctx = enclave::make_rpc_context(user_session, put_packed);
|
||||
check_error(
|
||||
frontend->process(put_ctx).value(), HTTP_STATUS_INTERNAL_SERVER_ERROR);
|
||||
|
|
|
@ -71,17 +71,8 @@ namespace cli
|
|||
struct ParsedMemberInfo
|
||||
{
|
||||
std::string cert_file;
|
||||
std::string enc_pub_file;
|
||||
std::optional<std::string> enc_pubk_file;
|
||||
std::optional<std::string> member_data_file;
|
||||
|
||||
ParsedMemberInfo(
|
||||
const std::string& cert,
|
||||
const std::string& enc_pub_file,
|
||||
const std::optional<std::string>& data_file) :
|
||||
cert_file(cert),
|
||||
enc_pub_file(enc_pub_file),
|
||||
member_data_file(data_file)
|
||||
{}
|
||||
};
|
||||
|
||||
CLI::Option* add_member_info_option(
|
||||
|
@ -103,50 +94,66 @@ namespace cli
|
|||
chunks.emplace_back(chunk);
|
||||
}
|
||||
|
||||
if (chunks.size() < 2 || chunks.size() > 3)
|
||||
if (chunks.empty() || chunks.size() > 3)
|
||||
{
|
||||
throw CLI::ValidationError(
|
||||
option_name,
|
||||
"Member info is not in format "
|
||||
"member_cert.pem,member_encryption_public_key.pem[,member_data."
|
||||
"json]");
|
||||
"Member info is not in expected format: "
|
||||
"member_cert.pem[,member_enc_pubk.pem[,member_data.json]]");
|
||||
}
|
||||
|
||||
auto cert = chunks[0];
|
||||
auto encryption_pub_key = chunks[1];
|
||||
ParsedMemberInfo member_info;
|
||||
member_info.cert_file = chunks.at(0);
|
||||
if (chunks.size() == 2)
|
||||
{
|
||||
member_info.enc_pubk_file = chunks.at(1);
|
||||
}
|
||||
else if (chunks.size() == 3)
|
||||
{
|
||||
// Only read encryption public key if there is something between two
|
||||
// commas
|
||||
if (!chunks.at(1).empty())
|
||||
{
|
||||
member_info.enc_pubk_file = chunks.at(1);
|
||||
}
|
||||
member_info.member_data_file = chunks.at(2);
|
||||
}
|
||||
|
||||
// Validate that member certificate and public encryption key exist
|
||||
// Validate that member info files exist, when specified
|
||||
auto validator = CLI::detail::ExistingFileValidator();
|
||||
auto err_str = validator(cert);
|
||||
auto err_str = validator(member_info.cert_file);
|
||||
if (!err_str.empty())
|
||||
{
|
||||
throw CLI::ValidationError(option_name, err_str);
|
||||
}
|
||||
|
||||
err_str = validator(encryption_pub_key);
|
||||
if (!err_str.empty())
|
||||
if (member_info.enc_pubk_file.has_value())
|
||||
{
|
||||
throw CLI::ValidationError(option_name, err_str);
|
||||
}
|
||||
|
||||
std::optional<std::string> member_data = std::nullopt;
|
||||
|
||||
if (chunks.size() == 3)
|
||||
{
|
||||
member_data = chunks[2];
|
||||
err_str = validator(member_data.value());
|
||||
err_str = validator(member_info.enc_pubk_file.value());
|
||||
if (!err_str.empty())
|
||||
{
|
||||
throw CLI::ValidationError(option_name, err_str);
|
||||
}
|
||||
}
|
||||
parsed.emplace_back(cert, encryption_pub_key, member_data);
|
||||
|
||||
if (member_info.member_data_file.has_value())
|
||||
{
|
||||
err_str = validator(member_info.member_data_file.value());
|
||||
if (!err_str.empty())
|
||||
{
|
||||
throw CLI::ValidationError(option_name, err_str);
|
||||
}
|
||||
}
|
||||
|
||||
parsed.emplace_back(member_info);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto* option = app.add_option(option_name, fun, option_desc, true);
|
||||
option->type_name("member_cert.pem,member_enc_pubk.pem")->type_size(-1);
|
||||
option
|
||||
->type_name("member_cert.pem[,member_enc_pubk.pem[,member_data.json]]")
|
||||
->type_size(-1);
|
||||
|
||||
return option;
|
||||
}
|
||||
|
|
|
@ -362,8 +362,8 @@ int main(int argc, char** argv)
|
|||
*start,
|
||||
members_info,
|
||||
"--member-info",
|
||||
"Initial consortium members information (public identity,encryption public "
|
||||
"key,member data)")
|
||||
"Initial consortium members information "
|
||||
"(member_cert.pem[,member_enc_pubk.pem[,member_data.json]])")
|
||||
->required();
|
||||
|
||||
std::optional<size_t> recovery_threshold = std::nullopt;
|
||||
|
@ -372,7 +372,7 @@ int main(int argc, char** argv)
|
|||
"--recovery-threshold",
|
||||
recovery_threshold,
|
||||
"Number of member shares required for recovery. Defaults to total number "
|
||||
"of initial consortium members.")
|
||||
"of initial consortium members with a public encryption key.")
|
||||
->check(CLI::PositiveNumber)
|
||||
->type_name("UINT");
|
||||
|
||||
|
@ -453,20 +453,36 @@ int main(int argc, char** argv)
|
|||
|
||||
if (*start)
|
||||
{
|
||||
// Count members with public encryption key as only these members will be
|
||||
// handed a recovery share.
|
||||
// Note that it is acceptable to start a network without any member having
|
||||
// a recovery share. The service will check that at least one recovery
|
||||
// member is added before the service can be opened.
|
||||
size_t members_with_pubk_count = 0;
|
||||
for (auto const& mi : members_info)
|
||||
{
|
||||
if (mi.enc_pubk_file.has_value())
|
||||
{
|
||||
members_with_pubk_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recovery_threshold.has_value())
|
||||
{
|
||||
LOG_INFO_FMT(
|
||||
"--recovery-threshold unset. Defaulting to number of initial "
|
||||
"consortium members ({}).",
|
||||
members_info.size());
|
||||
recovery_threshold = members_info.size();
|
||||
"Recovery threshold unset. Defaulting to number of initial "
|
||||
"consortium members with a public encryption key ({}).",
|
||||
members_with_pubk_count);
|
||||
recovery_threshold = members_with_pubk_count;
|
||||
}
|
||||
else if (recovery_threshold.value() > members_info.size())
|
||||
else if (recovery_threshold.value() > members_with_pubk_count)
|
||||
{
|
||||
throw std::logic_error(fmt::format(
|
||||
"--recovery-threshold cannot be greater than total number "
|
||||
"of initial consortium members (specified via --member-info "
|
||||
"options)"));
|
||||
"Recovery threshold ({}) cannot be greater than total number ({})"
|
||||
"of initial consortium members with a public encryption "
|
||||
"key (specified via --member-info options)",
|
||||
recovery_threshold.value(),
|
||||
members_with_pubk_count));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -643,6 +659,14 @@ int main(int argc, char** argv)
|
|||
|
||||
for (auto const& m_info : members_info)
|
||||
{
|
||||
std::optional<std::vector<uint8_t>> public_encryption_key_file =
|
||||
std::nullopt;
|
||||
if (m_info.enc_pubk_file.has_value())
|
||||
{
|
||||
public_encryption_key_file =
|
||||
files::slurp(m_info.enc_pubk_file.value());
|
||||
}
|
||||
|
||||
nlohmann::json md = nullptr;
|
||||
if (m_info.member_data_file.has_value())
|
||||
{
|
||||
|
@ -651,9 +675,7 @@ int main(int argc, char** argv)
|
|||
}
|
||||
|
||||
ccf_config.genesis.members_info.emplace_back(
|
||||
files::slurp(m_info.cert_file),
|
||||
files::slurp(m_info.enc_pub_file),
|
||||
md);
|
||||
files::slurp(m_info.cert_file), public_encryption_key_file, md);
|
||||
}
|
||||
ccf_config.genesis.gov_script = files::slurp_string(gov_script);
|
||||
ccf_config.genesis.recovery_threshold = recovery_threshold.value();
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace ccf
|
|||
struct Config
|
||||
{
|
||||
// Number of required shares to decrypt ledger secrets (recovery)
|
||||
size_t recovery_threshold;
|
||||
size_t recovery_threshold = 0;
|
||||
|
||||
MSGPACK_DEFINE(recovery_threshold)
|
||||
};
|
||||
|
|
|
@ -86,10 +86,23 @@ namespace ccf
|
|||
cv->put(0, consensus_type);
|
||||
}
|
||||
|
||||
auto add_member(
|
||||
const tls::Pem& member_cert,
|
||||
const tls::Pem& encryption_pub_key,
|
||||
const nlohmann::json& member_data = nullptr)
|
||||
auto get_active_recovery_members()
|
||||
{
|
||||
auto members_view = tx.get_view(tables.members);
|
||||
std::map<MemberId, tls::Pem> active_members_info;
|
||||
|
||||
members_view->foreach(
|
||||
[&active_members_info](const MemberId& mid, const MemberInfo& mi) {
|
||||
if (mi.status == MemberStatus::ACTIVE && mi.is_recovery())
|
||||
{
|
||||
active_members_info[mid] = mi.encryption_pub_key.value();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return active_members_info;
|
||||
}
|
||||
|
||||
MemberId add_member(const MemberPubInfo& member_pub_info)
|
||||
{
|
||||
auto [m, mc, v, ma, sig] = tx.get_view(
|
||||
tables.members,
|
||||
|
@ -100,7 +113,8 @@ namespace ccf
|
|||
|
||||
// The key to a CertDERs table must be a DER, for easy comparison against
|
||||
// the DER peer cert retrieved from the connection
|
||||
auto member_cert_der = tls::make_verifier(member_cert)->der_cert_data();
|
||||
auto member_cert_der =
|
||||
tls::make_verifier(member_pub_info.cert)->der_cert_data();
|
||||
|
||||
auto member_id = mc->get(member_cert_der);
|
||||
if (member_id.has_value())
|
||||
|
@ -110,13 +124,7 @@ namespace ccf
|
|||
}
|
||||
|
||||
const auto id = get_next_id(v, ValueIds::NEXT_MEMBER_ID);
|
||||
m->put(
|
||||
id,
|
||||
MemberInfo(
|
||||
member_cert,
|
||||
encryption_pub_key,
|
||||
member_data,
|
||||
MemberStatus::ACCEPTED));
|
||||
m->put(id, MemberInfo(member_pub_info, MemberStatus::ACCEPTED));
|
||||
mc->put(member_cert_der, id);
|
||||
|
||||
auto s = sig->get(0);
|
||||
|
@ -131,11 +139,6 @@ namespace ccf
|
|||
return id;
|
||||
}
|
||||
|
||||
auto add_member(const MemberPubInfo& info)
|
||||
{
|
||||
return add_member(info.cert, info.encryption_pub_key, info.member_data);
|
||||
}
|
||||
|
||||
void activate_member(MemberId member_id)
|
||||
{
|
||||
auto members = tx.get_view(tables.members);
|
||||
|
@ -146,15 +149,20 @@ namespace ccf
|
|||
"Member {} cannot be activated as they do not exist", member_id));
|
||||
}
|
||||
|
||||
if (member->status == MemberStatus::ACCEPTED)
|
||||
// Only accepted members can transition to accepted state
|
||||
if (member->status != MemberStatus::ACCEPTED)
|
||||
{
|
||||
member->status = MemberStatus::ACTIVE;
|
||||
if (get_active_members_count() >= max_active_members_count)
|
||||
{
|
||||
throw std::logic_error(fmt::format(
|
||||
"No more than {} active members are allowed",
|
||||
max_active_members_count));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
member->status = MemberStatus::ACTIVE;
|
||||
if (
|
||||
member->is_recovery() &&
|
||||
(get_active_recovery_members().size() >= max_active_recovery_members))
|
||||
{
|
||||
throw std::logic_error(fmt::format(
|
||||
"No more than {} active recovery members are allowed",
|
||||
max_active_recovery_members));
|
||||
}
|
||||
members->put(member_id, member.value());
|
||||
}
|
||||
|
@ -170,20 +178,30 @@ namespace ccf
|
|||
return false;
|
||||
}
|
||||
|
||||
if (member_to_retire->status == MemberStatus::ACTIVE)
|
||||
if (member_to_retire->status != MemberStatus::ACTIVE)
|
||||
{
|
||||
// If the member was active, it had a recovery share. Check that
|
||||
// the new number of active members is still sufficient for
|
||||
// recovery.
|
||||
auto active_members_count_after = get_active_members_count() - 1;
|
||||
LOG_DEBUG_FMT(
|
||||
"Could not retire member {}: member is not active", member_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the member was active and had a recovery share, check that
|
||||
// the new number of active members is still sufficient for
|
||||
// recovery
|
||||
if (member_to_retire->is_recovery())
|
||||
{
|
||||
// Because the member to retire is active, there is at least one active
|
||||
// member (i.e. get_active_recovery_members_count_after >= 0)
|
||||
size_t get_active_recovery_members_count_after =
|
||||
get_active_recovery_members().size() - 1;
|
||||
auto recovery_threshold = get_recovery_threshold();
|
||||
if (active_members_count_after < recovery_threshold)
|
||||
if (get_active_recovery_members_count_after < recovery_threshold)
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Failed to retire member {}: number of active members ({}) "
|
||||
"would be less than recovery threshold ({})",
|
||||
"Failed to retire member {}: number of active recovery members "
|
||||
"({}) would be less than recovery threshold ({})",
|
||||
member_id,
|
||||
active_members_count_after,
|
||||
get_active_recovery_members_count_after,
|
||||
recovery_threshold);
|
||||
return false;
|
||||
}
|
||||
|
@ -297,12 +315,13 @@ namespace ccf
|
|||
{
|
||||
auto service_view = tx.get_view(tables.service);
|
||||
|
||||
if (get_active_members_count() < get_recovery_threshold())
|
||||
auto active_recovery_members_count = get_active_recovery_members().size();
|
||||
if (active_recovery_members_count < get_recovery_threshold())
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Cannot open network as number of active members "
|
||||
"({}) is less than recovery threshold ({})",
|
||||
get_active_members_count(),
|
||||
"Cannot open network as number of active recovery members ({}) is "
|
||||
"less than recovery threshold ({})",
|
||||
active_recovery_members_count,
|
||||
get_recovery_threshold());
|
||||
return false;
|
||||
}
|
||||
|
@ -409,50 +428,17 @@ namespace ccf
|
|||
codeid_view->put(node_code_id, CodeStatus::ACCEPTED);
|
||||
}
|
||||
|
||||
size_t get_active_members_count()
|
||||
{
|
||||
auto members_view = tx.get_view(tables.members);
|
||||
size_t active_members_count = 0;
|
||||
|
||||
members_view->foreach(
|
||||
[&active_members_count](const MemberId&, const MemberInfo& mi) {
|
||||
if (mi.status == MemberStatus::ACTIVE)
|
||||
{
|
||||
active_members_count++;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return active_members_count;
|
||||
}
|
||||
|
||||
auto get_active_members_enc_pub()
|
||||
{
|
||||
auto members_view = tx.get_view(tables.members);
|
||||
std::map<MemberId, tls::Pem> active_members_info;
|
||||
|
||||
members_view->foreach(
|
||||
[&active_members_info](const MemberId& mid, const MemberInfo& mi) {
|
||||
if (mi.status == MemberStatus::ACTIVE)
|
||||
{
|
||||
active_members_info[mid] = mi.encryption_pub_key;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return active_members_info;
|
||||
}
|
||||
|
||||
void add_key_share_info(const RecoverySharesInfo& key_share_info)
|
||||
{
|
||||
auto shares_view = tx.get_view(tables.shares);
|
||||
shares_view->put(0, key_share_info);
|
||||
}
|
||||
|
||||
bool set_recovery_threshold(size_t threshold)
|
||||
bool set_recovery_threshold(size_t threshold, bool allow_zero = false)
|
||||
{
|
||||
auto config_view = tx.get_view(tables.config);
|
||||
|
||||
if (threshold == 0)
|
||||
if (!allow_zero && threshold == 0)
|
||||
{
|
||||
LOG_FAIL_FMT("Cannot set recovery threshold to 0");
|
||||
return false;
|
||||
|
@ -477,14 +463,15 @@ namespace ccf
|
|||
}
|
||||
else if (service_status.value() == ServiceStatus::OPEN)
|
||||
{
|
||||
auto active_members_count = get_active_members_count();
|
||||
if (threshold > active_members_count)
|
||||
auto get_active_recovery_members_count =
|
||||
get_active_recovery_members().size();
|
||||
if (threshold > get_active_recovery_members_count)
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Cannot set recovery threshold to {} as it is greater than the "
|
||||
"number of active members ({})",
|
||||
"number of active recovery members ({})",
|
||||
threshold,
|
||||
active_members_count);
|
||||
get_active_recovery_members_count);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,46 +30,43 @@ MSGPACK_ADD_ENUM(ccf::MemberStatus);
|
|||
namespace ccf
|
||||
{
|
||||
// Current limitations of secret sharing library (sss).
|
||||
// This could be mitigated by not handing a recovery share to every member.
|
||||
static constexpr size_t max_active_members_count = 255;
|
||||
static constexpr size_t max_active_recovery_members = 255;
|
||||
|
||||
struct MemberPubInfo
|
||||
{
|
||||
tls::Pem cert;
|
||||
tls::Pem encryption_pub_key;
|
||||
|
||||
// If encryption public key is set, the member is a recovery member
|
||||
std::optional<tls::Pem> encryption_pub_key = std::nullopt;
|
||||
nlohmann::json member_data = nullptr;
|
||||
|
||||
MemberPubInfo() {}
|
||||
|
||||
MemberPubInfo(
|
||||
const tls::Pem& cert_,
|
||||
const tls::Pem& encryption_pub_key_,
|
||||
const nlohmann::json& member_data_) :
|
||||
const std::optional<tls::Pem>& encryption_pub_key_ = std::nullopt,
|
||||
const nlohmann::json& member_data_ = nullptr) :
|
||||
cert(cert_),
|
||||
encryption_pub_key(encryption_pub_key_),
|
||||
member_data(member_data_)
|
||||
{}
|
||||
|
||||
MemberPubInfo(
|
||||
std::vector<uint8_t>&& cert_,
|
||||
std::vector<uint8_t>&& encryption_pub_key_,
|
||||
nlohmann::json&& member_data_) :
|
||||
cert(std::move(cert_)),
|
||||
encryption_pub_key(std::move(encryption_pub_key_)),
|
||||
member_data(std::move(member_data_))
|
||||
{}
|
||||
|
||||
bool operator==(const MemberPubInfo& rhs) const
|
||||
{
|
||||
return cert == rhs.cert && encryption_pub_key == rhs.encryption_pub_key &&
|
||||
member_data == rhs.member_data;
|
||||
}
|
||||
|
||||
bool is_recovery() const
|
||||
{
|
||||
return encryption_pub_key.has_value();
|
||||
}
|
||||
|
||||
MSGPACK_DEFINE(cert, encryption_pub_key, member_data);
|
||||
};
|
||||
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(MemberPubInfo)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(MemberPubInfo, cert, encryption_pub_key)
|
||||
DECLARE_JSON_OPTIONAL_FIELDS(MemberPubInfo, member_data)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(MemberPubInfo, cert)
|
||||
DECLARE_JSON_OPTIONAL_FIELDS(MemberPubInfo, encryption_pub_key, member_data)
|
||||
|
||||
struct MemberInfo : public MemberPubInfo
|
||||
{
|
||||
|
@ -77,12 +74,8 @@ namespace ccf
|
|||
|
||||
MemberInfo() {}
|
||||
|
||||
MemberInfo(
|
||||
const tls::Pem& cert_,
|
||||
const tls::Pem& encryption_pub_key_,
|
||||
const nlohmann::json& member_data_,
|
||||
MemberStatus status_) :
|
||||
MemberPubInfo(cert_, encryption_pub_key_, member_data_),
|
||||
MemberInfo(const MemberPubInfo& member_pub_info, MemberStatus status_) :
|
||||
MemberPubInfo(member_pub_info),
|
||||
status(status_)
|
||||
{}
|
||||
|
||||
|
|
|
@ -650,15 +650,16 @@ namespace ccf
|
|||
|
||||
if (!g.retire_member(member_id))
|
||||
{
|
||||
LOG_FAIL_FMT("Failed to retire member {}", member_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (member_info->status == MemberStatus::ACTIVE)
|
||||
if (
|
||||
member_info->status == MemberStatus::ACTIVE &&
|
||||
member_info->is_recovery())
|
||||
{
|
||||
// A retired member should not have access to the private ledger
|
||||
// going forward. New recovery shares are also issued to remaining
|
||||
// active members.
|
||||
// A retired member with recovery share should not have access to
|
||||
// the private ledger going forward so rekey ledger, issuing new
|
||||
// share to remaining active members
|
||||
if (!node.rekey_ledger(tx))
|
||||
{
|
||||
return false;
|
||||
|
@ -844,9 +845,6 @@ namespace ccf
|
|||
this->network.node_code_ids,
|
||||
proposal_id);
|
||||
}},
|
||||
// For now, members can propose to accept a recovery with shares. In
|
||||
// that case, members will have to submit their shares after this
|
||||
// proposal is accepted.
|
||||
{"accept_recovery",
|
||||
[this](ObjectId proposal_id, kv::Tx& tx, const nlohmann::json&) {
|
||||
if (node.is_part_of_public_network())
|
||||
|
@ -868,8 +866,8 @@ namespace ccf
|
|||
{"open_network",
|
||||
[this](ObjectId proposal_id, kv::Tx& tx, const nlohmann::json&) {
|
||||
// On network open, the service checks that a sufficient number of
|
||||
// members have become active. If so, recovery shares are allocated
|
||||
// to each active member.
|
||||
// recovery members have become active. If so, recovery shares are
|
||||
// allocated to each recovery member
|
||||
try
|
||||
{
|
||||
share_manager.issue_shares(tx);
|
||||
|
@ -1503,8 +1501,10 @@ namespace ccf
|
|||
auto ack = [this](EndpointContext& args, nlohmann::json&& params) {
|
||||
const auto signed_request = args.rpc_ctx->get_signed_request();
|
||||
|
||||
auto [ma_view, sig_view] =
|
||||
args.tx.get_view(this->network.member_acks, this->network.signatures);
|
||||
auto [ma_view, sig_view, members_view] = args.tx.get_view(
|
||||
this->network.member_acks,
|
||||
this->network.signatures,
|
||||
this->network.members);
|
||||
const auto ma = ma_view->get(args.caller_id);
|
||||
if (!ma)
|
||||
{
|
||||
|
@ -1533,18 +1533,32 @@ namespace ccf
|
|||
|
||||
// update member status to ACTIVE
|
||||
GenesisGenerator g(this->network, args.tx);
|
||||
g.activate_member(args.caller_id);
|
||||
try
|
||||
{
|
||||
g.activate_member(args.caller_id);
|
||||
}
|
||||
catch (const std::logic_error& e)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
fmt::format("Error activating new member: {}", e.what()));
|
||||
}
|
||||
|
||||
auto service_status = g.get_service_status();
|
||||
if (!service_status.has_value())
|
||||
{
|
||||
throw std::logic_error("No service currently available");
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
"No service currently available");
|
||||
}
|
||||
|
||||
if (service_status.value() == ServiceStatus::OPEN)
|
||||
auto member_info = members_view->get(args.caller_id);
|
||||
if (
|
||||
service_status.value() == ServiceStatus::OPEN &&
|
||||
member_info->is_recovery())
|
||||
{
|
||||
// When the service is OPEN, new active members are allocated new
|
||||
// recovery shares
|
||||
// When the service is OPEN and the new active member is a recovery
|
||||
// member, all recovery members are allocated new recovery shares
|
||||
try
|
||||
{
|
||||
share_manager.issue_shares(args.tx);
|
||||
|
@ -1607,7 +1621,7 @@ namespace ccf
|
|||
if (!encrypted_share.has_value())
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
HTTP_STATUS_NOT_FOUND,
|
||||
fmt::format(
|
||||
"Recovery share not found for member {}", args.caller_id));
|
||||
}
|
||||
|
@ -1679,7 +1693,7 @@ namespace ccf
|
|||
}
|
||||
|
||||
LOG_DEBUG_FMT(
|
||||
"Reached secret sharing threshold {}", g.get_recovery_threshold());
|
||||
"Reached recovery threshold {}", g.get_recovery_threshold());
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -1733,7 +1747,16 @@ namespace ccf
|
|||
g.add_member(info);
|
||||
}
|
||||
|
||||
g.set_recovery_threshold(in.recovery_threshold);
|
||||
// Note that it is acceptable to start a network without any member
|
||||
// having a recovery share. The service will check that at least one
|
||||
// recovery member is added before the service is opened.
|
||||
if (!g.set_recovery_threshold(in.recovery_threshold, true))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
fmt::format(
|
||||
"Could not set recovery threshold to {}", in.recovery_threshold));
|
||||
}
|
||||
|
||||
g.add_consensus(in.consensus_type);
|
||||
|
||||
|
|
|
@ -428,8 +428,6 @@ auto invalid_caller_der = tls::make_verifier(invalid_caller) -> der_cert_data();
|
|||
|
||||
auto anonymous_caller_der = std::vector<uint8_t>();
|
||||
|
||||
std::vector<uint8_t> dummy_enc_pubk = {1, 2, 3};
|
||||
|
||||
auto user_session = make_shared<enclave::SessionContext>(
|
||||
enclave::InvalidSessionId, user_caller_der);
|
||||
auto backup_user_session = make_shared<enclave::SessionContext>(
|
||||
|
@ -464,8 +462,8 @@ void prepare_callers(NetworkState& network)
|
|||
g.create_service({});
|
||||
user_id = g.add_user({user_caller});
|
||||
nos_id = g.add_user({nos_caller});
|
||||
member_id = g.add_member(member_caller, dummy_enc_pubk);
|
||||
invalid_member_id = g.add_member(invalid_caller, dummy_enc_pubk);
|
||||
member_id = g.add_member(member_caller);
|
||||
invalid_member_id = g.add_member(invalid_caller);
|
||||
CHECK(g.finalize() == kv::CommitSuccess::OK);
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ auto member_cert = kp -> self_sign("CN=name_member");
|
|||
auto verifier_mem = tls::make_verifier(member_cert);
|
||||
auto member_caller = verifier_mem -> der_cert_data();
|
||||
auto user_cert = kp -> self_sign("CN=name_user");
|
||||
std::vector<uint8_t> dummy_enc_pubk = {1, 2, 3};
|
||||
auto dummy_enc_pubk = tls::make_rsa_key_pair() -> public_key_pem();
|
||||
|
||||
auto encryptor = std::make_shared<kv::NullTxEncryptor>();
|
||||
|
||||
|
@ -205,16 +205,27 @@ auto get_vote(
|
|||
frontend_process(frontend, getter, caller));
|
||||
}
|
||||
|
||||
auto activate(
|
||||
MemberRpcFrontend& frontend,
|
||||
const tls::KeyPairPtr& kp,
|
||||
const tls::Pem& caller)
|
||||
{
|
||||
const auto state_digest_req =
|
||||
create_request(nullptr, "ack/update_state_digest");
|
||||
const auto ack = parse_response_body<std::vector<uint8_t>>(
|
||||
frontend_process(frontend, state_digest_req, caller));
|
||||
|
||||
StateDigest params;
|
||||
params.state_digest = ack;
|
||||
const auto ack_req = create_signed_request(params, "ack", kp);
|
||||
return frontend_process(frontend, ack_req, caller);
|
||||
}
|
||||
|
||||
auto get_cert(uint64_t member_id, tls::KeyPairPtr& kp_mem)
|
||||
{
|
||||
return kp_mem->self_sign("CN=new member" + to_string(member_id));
|
||||
}
|
||||
|
||||
auto gen_public_encryption_key()
|
||||
{
|
||||
return tls::make_rsa_key_pair()->public_key_pem();
|
||||
}
|
||||
|
||||
auto init_frontend(
|
||||
NetworkTables& network,
|
||||
GenesisGenerator& gen,
|
||||
|
@ -227,7 +238,7 @@ auto init_frontend(
|
|||
for (uint8_t i = 0; i < n_members; i++)
|
||||
{
|
||||
member_certs.push_back(get_cert(i, kp));
|
||||
gen.activate_member(gen.add_member(member_certs.back(), {}));
|
||||
gen.activate_member(gen.add_member(member_certs.back()));
|
||||
}
|
||||
|
||||
set_whitelists(gen);
|
||||
|
@ -249,7 +260,7 @@ DOCTEST_TEST_CASE("Member query/read")
|
|||
StubNodeState node(share_manager);
|
||||
MemberRpcFrontend frontend(network, node, share_manager);
|
||||
frontend.open();
|
||||
const auto member_id = gen.add_member(member_cert, {});
|
||||
const auto member_id = gen.add_member(member_cert);
|
||||
gen.finalize();
|
||||
|
||||
const enclave::SessionContext member_session(
|
||||
|
@ -352,10 +363,10 @@ DOCTEST_TEST_CASE("Proposer ballot")
|
|||
gen.create_service({});
|
||||
|
||||
const auto proposer_cert = get_cert(0, kp);
|
||||
const auto proposer_id = gen.add_member(proposer_cert, {});
|
||||
const auto proposer_id = gen.add_member(proposer_cert);
|
||||
gen.activate_member(proposer_id);
|
||||
const auto voter_cert = get_cert(1, kp);
|
||||
const auto voter_id = gen.add_member(voter_cert, {});
|
||||
const auto voter_id = gen.add_member(voter_cert);
|
||||
gen.activate_member(voter_id);
|
||||
|
||||
set_whitelists(gen);
|
||||
|
@ -466,15 +477,15 @@ DOCTEST_TEST_CASE("Add new members until there are 7 then reject")
|
|||
StubNodeState node(share_manager);
|
||||
// add three initial active members
|
||||
// the proposer
|
||||
auto proposer_id = gen.add_member(member_cert, gen_public_encryption_key());
|
||||
auto proposer_id = gen.add_member({member_cert, dummy_enc_pubk});
|
||||
gen.activate_member(proposer_id);
|
||||
|
||||
// the voters
|
||||
const auto voter_a_cert = get_cert(1, kp);
|
||||
auto voter_a = gen.add_member(voter_a_cert, gen_public_encryption_key());
|
||||
auto voter_a = gen.add_member({voter_a_cert, dummy_enc_pubk});
|
||||
gen.activate_member(voter_a);
|
||||
const auto voter_b_cert = get_cert(2, kp);
|
||||
auto voter_b = gen.add_member(voter_b_cert, gen_public_encryption_key());
|
||||
auto voter_b = gen.add_member({voter_b_cert, dummy_enc_pubk});
|
||||
gen.activate_member(voter_b);
|
||||
|
||||
set_whitelists(gen);
|
||||
|
@ -512,7 +523,7 @@ DOCTEST_TEST_CASE("Add new members until there are 7 then reject")
|
|||
return Calls:call("new_member", member_info)
|
||||
)xxx");
|
||||
proposal.parameter["cert"] = cert_pem;
|
||||
proposal.parameter["encryption_pub_key"] = gen_public_encryption_key();
|
||||
proposal.parameter["encryption_pub_key"] = dummy_enc_pubk;
|
||||
|
||||
const auto propose = create_signed_request(proposal, "proposals", kp);
|
||||
|
||||
|
@ -682,8 +693,8 @@ DOCTEST_TEST_CASE("Accept node")
|
|||
|
||||
const auto member_0_cert = get_cert(0, new_kp);
|
||||
const auto member_1_cert = get_cert(1, kp);
|
||||
const auto member_0 = gen.add_member(member_0_cert, {});
|
||||
const auto member_1 = gen.add_member(member_1_cert, {});
|
||||
const auto member_0 = gen.add_member(member_0_cert);
|
||||
const auto member_1 = gen.add_member(member_1_cert);
|
||||
gen.activate_member(member_0);
|
||||
gen.activate_member(member_1);
|
||||
|
||||
|
@ -1046,8 +1057,8 @@ DOCTEST_TEST_CASE("Remove proposal")
|
|||
|
||||
ShareManager share_manager(network);
|
||||
StubNodeState node(share_manager);
|
||||
gen.activate_member(gen.add_member(member_cert, {}));
|
||||
gen.activate_member(gen.add_member(cert, {}));
|
||||
gen.activate_member(gen.add_member(member_cert));
|
||||
gen.activate_member(gen.add_member(cert));
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(lua::Interpreter().invoke<json>(gov_script_file));
|
||||
gen.finalize();
|
||||
|
@ -1221,9 +1232,9 @@ DOCTEST_TEST_CASE("Vetoed proposal gets rejected")
|
|||
ShareManager share_manager(network);
|
||||
StubNodeState node(share_manager);
|
||||
const auto voter_a_cert = get_cert(1, kp);
|
||||
auto voter_a = gen.add_member(voter_a_cert, {});
|
||||
auto voter_a = gen.add_member(voter_a_cert);
|
||||
const auto voter_b_cert = get_cert(2, kp);
|
||||
auto voter_b = gen.add_member(voter_b_cert, {});
|
||||
auto voter_b = gen.add_member(voter_b_cert);
|
||||
gen.activate_member(voter_a);
|
||||
gen.activate_member(voter_b);
|
||||
set_whitelists(gen);
|
||||
|
@ -1275,7 +1286,7 @@ DOCTEST_TEST_CASE("Add and remove user via proposed calls")
|
|||
ShareManager share_manager(network);
|
||||
StubNodeState node(share_manager);
|
||||
const auto member_cert = get_cert(0, kp);
|
||||
gen.activate_member(gen.add_member(member_cert, {}));
|
||||
gen.activate_member(gen.add_member(member_cert));
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(lua::Interpreter().invoke<json>(gov_script_file));
|
||||
gen.finalize();
|
||||
|
@ -1375,7 +1386,7 @@ DOCTEST_TEST_CASE(
|
|||
// Operating member, as indicated by member data
|
||||
const auto operator_cert = get_cert(0, kp);
|
||||
const auto operator_id =
|
||||
gen.add_member(operator_cert, {}, operator_member_data());
|
||||
gen.add_member({operator_cert, {}, operator_member_data()});
|
||||
gen.activate_member(operator_id);
|
||||
|
||||
// Non-operating members
|
||||
|
@ -1383,7 +1394,7 @@ DOCTEST_TEST_CASE(
|
|||
for (size_t i = 1; i < 4; i++)
|
||||
{
|
||||
auto cert = get_cert(i, kp);
|
||||
auto id = gen.add_member(cert, {});
|
||||
auto id = gen.add_member(cert);
|
||||
gen.activate_member(id);
|
||||
members[id] = cert;
|
||||
}
|
||||
|
@ -1495,12 +1506,12 @@ DOCTEST_TEST_CASE("Passing operator change" * doctest::test_suite("operator"))
|
|||
auto new_ca = new_kp->self_sign("CN=new node");
|
||||
NodeInfo ni;
|
||||
ni.cert = new_ca;
|
||||
gen.add_node(ni);
|
||||
auto node_id = gen.add_node(ni);
|
||||
|
||||
// Operating member, as indicated by member data
|
||||
const auto operator_cert = get_cert(0, kp);
|
||||
const auto operator_id =
|
||||
gen.add_member(operator_cert, {}, operator_member_data());
|
||||
gen.add_member({operator_cert, std::nullopt, operator_member_data()});
|
||||
gen.activate_member(operator_id);
|
||||
|
||||
// Non-operating members
|
||||
|
@ -1508,14 +1519,11 @@ DOCTEST_TEST_CASE("Passing operator change" * doctest::test_suite("operator"))
|
|||
for (size_t i = 1; i < 4; i++)
|
||||
{
|
||||
auto cert = get_cert(i, kp);
|
||||
auto id = gen.add_member(cert, {});
|
||||
auto id = gen.add_member({cert, dummy_enc_pubk});
|
||||
gen.activate_member(id);
|
||||
members[id] = cert;
|
||||
}
|
||||
|
||||
// Set a recovery threshold (otherwise retiring a member throws)
|
||||
gen.set_recovery_threshold(1);
|
||||
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(
|
||||
lua::Interpreter().invoke<json>(operator_gov_script_file));
|
||||
|
@ -1531,7 +1539,6 @@ DOCTEST_TEST_CASE("Passing operator change" * doctest::test_suite("operator"))
|
|||
const ccf::Script vote_for("return true");
|
||||
const ccf::Script vote_against("return false");
|
||||
|
||||
auto node_id = 0;
|
||||
{
|
||||
DOCTEST_INFO("Check node exists with status pending");
|
||||
auto read_values =
|
||||
|
@ -1582,7 +1589,6 @@ DOCTEST_TEST_CASE("Passing operator change" * doctest::test_suite("operator"))
|
|||
)xxx");
|
||||
|
||||
proposal.parameter["cert"] = new_operator_cert;
|
||||
proposal.parameter["encryption_pub_key"] = dummy_enc_pubk;
|
||||
proposal.parameter["member_data"] = operator_member_data();
|
||||
|
||||
const auto propose = create_signed_request(proposal, "proposals", kp);
|
||||
|
@ -1680,9 +1686,9 @@ DOCTEST_TEST_CASE(
|
|||
ni.cert = new_ca;
|
||||
gen.add_node(ni);
|
||||
|
||||
// Not operating member, as indicated by member data
|
||||
// Not operating member
|
||||
const auto proposer_cert = get_cert(0, kp);
|
||||
const auto proposer_id = gen.add_member(proposer_cert, {}, nullptr);
|
||||
const auto proposer_id = gen.add_member(proposer_cert);
|
||||
gen.activate_member(proposer_id);
|
||||
|
||||
// Non-operating members
|
||||
|
@ -1690,7 +1696,7 @@ DOCTEST_TEST_CASE(
|
|||
for (size_t i = 1; i < 3; i++)
|
||||
{
|
||||
auto cert = get_cert(i, kp);
|
||||
auto id = gen.add_member(cert, {});
|
||||
auto id = gen.add_member(cert);
|
||||
gen.activate_member(id);
|
||||
members[id] = cert;
|
||||
}
|
||||
|
@ -1799,7 +1805,7 @@ DOCTEST_TEST_CASE("User data")
|
|||
GenesisGenerator gen(network, gen_tx);
|
||||
gen.init_values();
|
||||
gen.create_service({});
|
||||
const auto member_id = gen.add_member(member_cert, {});
|
||||
const auto member_id = gen.add_member(member_cert);
|
||||
gen.activate_member(member_id);
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(lua::Interpreter().invoke<json>(gov_script_file));
|
||||
|
@ -1952,7 +1958,7 @@ DOCTEST_TEST_CASE("Submit recovery shares")
|
|||
auto cert = get_cert(i, kp);
|
||||
auto enc_kp = tls::make_rsa_key_pair();
|
||||
|
||||
auto id = gen.add_member(cert, enc_kp->public_key_pem());
|
||||
auto id = gen.add_member({cert, enc_kp->public_key_pem()});
|
||||
gen.activate_member(id);
|
||||
members[id] = {cert, enc_kp};
|
||||
}
|
||||
|
@ -2059,8 +2065,9 @@ DOCTEST_TEST_CASE("Submit recovery shares")
|
|||
}
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("Maximum number of active members")
|
||||
DOCTEST_TEST_CASE("Number of active members with recovery shares limits")
|
||||
{
|
||||
auto level_before = logger::config::level();
|
||||
logger::config::level() = logger::INFO;
|
||||
|
||||
NetworkState network;
|
||||
|
@ -2078,37 +2085,54 @@ DOCTEST_TEST_CASE("Maximum number of active members")
|
|||
GenesisGenerator gen(network, gen_tx);
|
||||
gen.init_values();
|
||||
gen.create_service({});
|
||||
gen.set_recovery_threshold(1);
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(lua::Interpreter().invoke<json>(gov_script_file));
|
||||
|
||||
for (size_t i = 1; i < max_active_members_count + 1; i++)
|
||||
DOCTEST_INFO("Add one too many members with recovery share");
|
||||
{
|
||||
auto cert = get_cert(i, kp);
|
||||
members[gen.add_member(cert, {})] = cert;
|
||||
// Members are not yet active
|
||||
for (size_t i = 0; i < max_active_recovery_members + 1; i++)
|
||||
{
|
||||
auto cert = get_cert(i, kp);
|
||||
members[gen.add_member({cert, dummy_enc_pubk})] = cert;
|
||||
}
|
||||
gen.finalize();
|
||||
}
|
||||
gen.finalize();
|
||||
|
||||
for (auto const& m : members)
|
||||
DOCTEST_INFO("Activate members until reaching limit");
|
||||
{
|
||||
const auto state_digest_req =
|
||||
create_request(nullptr, "ack/update_state_digest");
|
||||
const auto ack = parse_response_body<std::vector<uint8_t>>(
|
||||
frontend_process(frontend, state_digest_req, m.second));
|
||||
|
||||
StateDigest params;
|
||||
params.state_digest = ack;
|
||||
const auto ack_req = create_signed_request(params, "ack", kp);
|
||||
const auto resp = frontend_process(frontend, ack_req, m.second);
|
||||
|
||||
if (m.first >= max_active_members_count)
|
||||
for (auto const& m : members)
|
||||
{
|
||||
DOCTEST_CHECK(resp.status == HTTP_STATUS_FORBIDDEN);
|
||||
auto resp = activate(frontend, kp, m.second);
|
||||
|
||||
if (m.first >= max_active_recovery_members)
|
||||
{
|
||||
DOCTEST_CHECK(resp.status == HTTP_STATUS_FORBIDDEN);
|
||||
}
|
||||
else
|
||||
{
|
||||
DOCTEST_CHECK(resp.status == HTTP_STATUS_OK);
|
||||
DOCTEST_CHECK(parse_response_body<bool>(resp));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DOCTEST_CHECK(resp.status == HTTP_STATUS_OK);
|
||||
DOCTEST_CHECK(parse_response_body<bool>(resp));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
DOCTEST_INFO("It is still OK to add and activate a non-recovery member");
|
||||
{
|
||||
auto gen_tx = network.tables->create_tx();
|
||||
GenesisGenerator gen(network, gen_tx);
|
||||
auto cert = get_cert(members.size(), kp);
|
||||
gen.add_member(cert); // No public encryption key added
|
||||
gen.finalize();
|
||||
auto resp = activate(frontend, kp, cert);
|
||||
|
||||
DOCTEST_CHECK(resp.status == HTTP_STATUS_OK);
|
||||
DOCTEST_CHECK(parse_response_body<bool>(resp));
|
||||
}
|
||||
|
||||
// Revert logging
|
||||
logger::config::level() = level_before;
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("Open network sequence")
|
||||
|
@ -2138,7 +2162,7 @@ DOCTEST_TEST_CASE("Open network sequence")
|
|||
for (size_t i = 0; i < members_count; i++)
|
||||
{
|
||||
auto cert = get_cert(i, kp);
|
||||
auto id = gen.add_member(cert, {});
|
||||
auto id = gen.add_member({cert, dummy_enc_pubk});
|
||||
members[id] = {cert, {}};
|
||||
}
|
||||
gen.set_recovery_threshold(recovery_threshold);
|
||||
|
|
|
@ -211,7 +211,8 @@ TEST_CASE("Add a node to an open service")
|
|||
|
||||
gen.create_service({});
|
||||
gen.set_recovery_threshold(1);
|
||||
gen.activate_member(gen.add_member(member_cert, {}));
|
||||
gen.activate_member(
|
||||
gen.add_member({member_cert, tls::make_rsa_key_pair()->public_key_pem()}));
|
||||
REQUIRE(gen.open_service());
|
||||
gen.finalize();
|
||||
|
||||
|
|
|
@ -117,14 +117,30 @@ namespace ccf
|
|||
ls_wrapping_key.get_raw_data<SecretSharing::SplitSecret>();
|
||||
|
||||
GenesisGenerator g(network, tx);
|
||||
auto active_members_info = g.get_active_members_enc_pub();
|
||||
auto active_recovery_members_info = g.get_active_recovery_members();
|
||||
size_t recovery_threshold = g.get_recovery_threshold();
|
||||
|
||||
if (active_recovery_members_info.size() == 0)
|
||||
{
|
||||
throw std::logic_error(
|
||||
"There should be at least one active recovery member to issue "
|
||||
"recovery shares");
|
||||
}
|
||||
|
||||
if (recovery_threshold == 0)
|
||||
{
|
||||
throw std::logic_error(
|
||||
"Recovery threshold should be set before recovery "
|
||||
"shares are computed");
|
||||
}
|
||||
|
||||
auto shares = SecretSharing::split(
|
||||
secret_to_split, active_members_info.size(), recovery_threshold);
|
||||
secret_to_split,
|
||||
active_recovery_members_info.size(),
|
||||
recovery_threshold);
|
||||
|
||||
size_t share_index = 0;
|
||||
for (auto const& [member_id, enc_pub_key] : active_members_info)
|
||||
for (auto const& [member_id, enc_pub_key] : active_recovery_members_info)
|
||||
{
|
||||
auto member_enc_pubk = tls::make_rsa_public_key(enc_pub_key);
|
||||
auto raw_share = std::vector<uint8_t>(
|
||||
|
|
|
@ -12,8 +12,24 @@ return {
|
|||
REJECTED = -1
|
||||
STATE_ACTIVE = "ACTIVE"
|
||||
|
||||
-- returns true if the member is a recovery member
|
||||
function is_recovery_member(member)
|
||||
member_info = tables["public:ccf.gov.members"]:get(member)
|
||||
if member_info then
|
||||
member_enc_pubk = member_info.encryption_pub_key
|
||||
if member_enc_pubk then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- defines which of the members are operators
|
||||
function is_operator(member)
|
||||
-- Operators cannot be recovery members
|
||||
if is_recovery_member(member) then
|
||||
return false
|
||||
end
|
||||
member_info = tables["public:ccf.gov.members"]:get(member)
|
||||
if member_info then
|
||||
member_data = member_info.member_data
|
||||
|
|
|
@ -49,6 +49,11 @@ namespace tls
|
|||
return s == rhs.s;
|
||||
}
|
||||
|
||||
bool operator!=(const Pem& rhs) const
|
||||
{
|
||||
return !(*this == rhs);
|
||||
}
|
||||
|
||||
const std::string& str() const
|
||||
{
|
||||
return s;
|
||||
|
|
|
@ -37,12 +37,20 @@ class Consortium:
|
|||
# If a list of member IDs is passed in, generate fresh member identities.
|
||||
# Otherwise, recover the state of the consortium from the state of CCF.
|
||||
if member_ids is not None:
|
||||
for m_id, m_data in member_ids:
|
||||
self.recovery_threshold = 0
|
||||
for m_id, has_share, m_data in member_ids:
|
||||
new_member = infra.member.Member(
|
||||
m_id, curve, common_dir, share_script, key_generator, m_data
|
||||
m_id,
|
||||
curve,
|
||||
common_dir,
|
||||
share_script,
|
||||
has_share,
|
||||
key_generator,
|
||||
m_data,
|
||||
)
|
||||
if has_share:
|
||||
self.recovery_threshold += 1
|
||||
self.members.append(new_member)
|
||||
self.recovery_threshold = len(self.members)
|
||||
else:
|
||||
with remote_node.client("member0") as mc:
|
||||
r = mc.post(
|
||||
|
@ -50,26 +58,33 @@ class Consortium:
|
|||
{
|
||||
"text": """tables = ...
|
||||
non_retired_members = {}
|
||||
tables["public:ccf.gov.members"]:foreach(function(member_id, details)
|
||||
if details["status"] ~= "RETIRED" then
|
||||
table.insert(non_retired_members, {member_id, details["status"]})
|
||||
tables["public:ccf.gov.members"]:foreach(function(member_id, info)
|
||||
if info["status"] ~= "RETIRED" then
|
||||
table.insert(non_retired_members, {member_id, info})
|
||||
end
|
||||
end)
|
||||
return non_retired_members
|
||||
"""
|
||||
},
|
||||
)
|
||||
for m in r.body.json():
|
||||
for m_id, info in r.body.json():
|
||||
new_member = infra.member.Member(
|
||||
m[0], curve, self.common_dir, share_script
|
||||
m_id,
|
||||
curve,
|
||||
self.common_dir,
|
||||
share_script,
|
||||
is_recovery_member="encryption_pub_key" in info,
|
||||
)
|
||||
status = info["status"]
|
||||
if (
|
||||
infra.member.MemberStatus[m[1]]
|
||||
infra.member.MemberStatus[status]
|
||||
== infra.member.MemberStatus.ACTIVE
|
||||
):
|
||||
new_member.set_active()
|
||||
self.members.append(new_member)
|
||||
LOG.info(f"Successfully recovered member {m[0]} with status {m[1]}")
|
||||
LOG.info(
|
||||
f"Successfully recovered member {m_id} with status {status}"
|
||||
)
|
||||
|
||||
r = mc.post(
|
||||
"/gov/query",
|
||||
|
@ -108,27 +123,40 @@ class Consortium:
|
|||
for m in self.members:
|
||||
m.ack(remote_node)
|
||||
|
||||
def generate_and_propose_new_member(self, remote_node, curve, member_data=None):
|
||||
def generate_and_propose_new_member(
|
||||
self, remote_node, curve, recovery_member=True, member_data=None
|
||||
):
|
||||
# The Member returned by this function is in state ACCEPTED. The new Member
|
||||
# should ACK to become active.
|
||||
new_member_id = len(self.members)
|
||||
new_member = infra.member.Member(
|
||||
new_member_id, curve, self.common_dir, self.share_script, self.key_generator
|
||||
new_member_id,
|
||||
curve,
|
||||
self.common_dir,
|
||||
self.share_script,
|
||||
is_recovery_member=recovery_member,
|
||||
key_generator=self.key_generator,
|
||||
)
|
||||
|
||||
proposal_body, careful_vote = self.make_proposal(
|
||||
"new_member",
|
||||
os.path.join(self.common_dir, f"member{new_member_id}_cert.pem"),
|
||||
os.path.join(self.common_dir, f"member{new_member_id}_enc_pubk.pem"),
|
||||
os.path.join(self.common_dir, f"member{new_member_id}_enc_pubk.pem")
|
||||
if recovery_member
|
||||
else None,
|
||||
member_data,
|
||||
)
|
||||
|
||||
proposal = self.get_any_active_member().propose(remote_node, proposal_body)
|
||||
return proposal, new_member, careful_vote
|
||||
proposal.vote_for = careful_vote
|
||||
|
||||
def generate_and_add_new_member(self, remote_node, curve, member_data=None):
|
||||
return (proposal, new_member, careful_vote)
|
||||
|
||||
def generate_and_add_new_member(
|
||||
self, remote_node, curve, recovery_member=True, member_data=None
|
||||
):
|
||||
proposal, new_member, careful_vote = self.generate_and_propose_new_member(
|
||||
remote_node, curve
|
||||
remote_node, curve, recovery_member, member_data
|
||||
)
|
||||
self.vote_using_majority(remote_node, proposal, careful_vote)
|
||||
|
||||
|
@ -140,18 +168,34 @@ class Consortium:
|
|||
def get_members_info(self):
|
||||
info = []
|
||||
for m in self.members:
|
||||
i = (f"member{m.member_id}_cert.pem", f"member{m.member_id}_enc_pubk.pem")
|
||||
md = f"member{m.member_id}_data.json"
|
||||
if os.path.exists(os.path.join(self.common_dir, md)):
|
||||
i = i + (md,)
|
||||
info.append(i)
|
||||
info += [m.member_info]
|
||||
return info
|
||||
|
||||
def get_active_members(self):
|
||||
return [member for member in self.members if member.is_active()]
|
||||
|
||||
def get_any_active_member(self):
|
||||
return random.choice(self.get_active_members())
|
||||
def get_active_recovery_members(self):
|
||||
return [
|
||||
member
|
||||
for member in self.members
|
||||
if (member.is_active() and member.is_recovery_member)
|
||||
]
|
||||
|
||||
def get_active_non_recovery_members(self):
|
||||
return [
|
||||
member
|
||||
for member in self.members
|
||||
if (member.is_active() and not member.is_recovery_member)
|
||||
]
|
||||
|
||||
def get_any_active_member(self, recovery_member=None):
|
||||
if recovery_member is not None:
|
||||
if recovery_member == True:
|
||||
return random.choice(self.get_active_recovery_members())
|
||||
elif recovery_member == False:
|
||||
return random.choice(self.get_active_non_recovery_members())
|
||||
else:
|
||||
return random.choice(self.get_active_members())
|
||||
|
||||
def get_member_by_id(self, member_id):
|
||||
return next(
|
||||
|
@ -355,7 +399,7 @@ class Consortium:
|
|||
with remote_node.client() as nc:
|
||||
check_commit = infra.checker.Checker(nc)
|
||||
|
||||
for m in self.get_active_members():
|
||||
for m in self.get_active_recovery_members():
|
||||
r = m.get_and_submit_recovery_share(remote_node)
|
||||
submitted_shares_count += 1
|
||||
check_commit(r)
|
||||
|
@ -371,8 +415,11 @@ class Consortium:
|
|||
"set_recovery_threshold", recovery_threshold
|
||||
)
|
||||
proposal = self.get_any_active_member().propose(remote_node, proposal_body)
|
||||
self.recovery_threshold = recovery_threshold
|
||||
return self.vote_using_majority(remote_node, proposal, careful_vote)
|
||||
proposal.vote_for = careful_vote
|
||||
r = self.vote_using_majority(remote_node, proposal, careful_vote)
|
||||
if proposal.state == infra.proposal.ProposalState.Accepted:
|
||||
self.recovery_threshold = recovery_threshold
|
||||
return r
|
||||
|
||||
def add_new_code(self, remote_node, new_code_id):
|
||||
proposal_body, careful_vote = self.make_proposal("new_node_code", new_code_id)
|
||||
|
|
|
@ -167,9 +167,6 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False):
|
|||
parser.add_argument(
|
||||
"--pdb", help="Break to debugger on exception", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--notify-server", help="Server host to notify progress to (host:port)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--workspace",
|
||||
help="Temporary directory where nodes store their logs, ledgers, quotes, etc.",
|
||||
|
@ -230,6 +227,12 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False):
|
|||
type=int,
|
||||
default=3,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--initial-recovery-member-count",
|
||||
help="Number of initial members that are handed recovery shares",
|
||||
type=int,
|
||||
default=3,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ledger-recovery-timeout",
|
||||
help="On recovery, maximum timeout (s) while reading the ledger",
|
||||
|
|
|
@ -10,6 +10,7 @@ import http
|
|||
import os
|
||||
import base64
|
||||
import json
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class NoRecoveryShareFound(Exception):
|
||||
|
@ -24,6 +25,12 @@ class MemberStatus(Enum):
|
|||
RETIRED = 2
|
||||
|
||||
|
||||
class MemberInfo(NamedTuple):
|
||||
certificate_file: str
|
||||
encryption_pub_key_file: Optional[str]
|
||||
member_data_file: Optional[str]
|
||||
|
||||
|
||||
class Member:
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -31,6 +38,7 @@ class Member:
|
|||
curve,
|
||||
common_dir,
|
||||
share_script,
|
||||
is_recovery_member=True,
|
||||
key_generator=None,
|
||||
member_data=None,
|
||||
):
|
||||
|
@ -39,17 +47,33 @@ class Member:
|
|||
self.status_code = MemberStatus.ACCEPTED
|
||||
self.share_script = share_script
|
||||
self.member_data = member_data
|
||||
self.is_recovery_member = is_recovery_member
|
||||
|
||||
self.member_info = MemberInfo(
|
||||
f"member{self.member_id}_cert.pem",
|
||||
f"member{self.member_id}_enc_pubk.pem" if is_recovery_member else None,
|
||||
f"member{self.member_id}_data.json" if member_data else None,
|
||||
)
|
||||
|
||||
if key_generator is not None:
|
||||
# For now, all members are given an encryption key (for recovery)
|
||||
member = f"member{member_id}"
|
||||
infra.proc.ccall(
|
||||
key_generator,
|
||||
|
||||
key_generator_args = [
|
||||
"--name",
|
||||
f"{member}",
|
||||
"--curve",
|
||||
f"{curve.name}",
|
||||
"--gen-enc-key",
|
||||
]
|
||||
|
||||
if is_recovery_member:
|
||||
key_generator_args += [
|
||||
"--gen-enc-key",
|
||||
]
|
||||
|
||||
infra.proc.ccall(
|
||||
key_generator,
|
||||
*key_generator_args,
|
||||
path=self.common_dir,
|
||||
log_output=False,
|
||||
).check_returncode()
|
||||
|
@ -60,15 +84,12 @@ class Member:
|
|||
os.path.join(self.common_dir, f"member{self.member_id}_privk.pem")
|
||||
)
|
||||
assert os.path.isfile(
|
||||
os.path.join(self.common_dir, f"member{self.member_id}_cert.pem")
|
||||
)
|
||||
assert os.path.isfile(
|
||||
os.path.join(self.common_dir, f"member{self.member_id}_enc_privk.pem")
|
||||
os.path.join(self.common_dir, self.member_info.certificate_file)
|
||||
)
|
||||
|
||||
if member_data is not None:
|
||||
if self.member_data is not None:
|
||||
with open(
|
||||
os.path.join(self.common_dir, f"member{self.member_id}_data.json"), "w"
|
||||
os.path.join(self.common_dir, self.member_info.member_data_file), "w"
|
||||
) as md:
|
||||
json.dump(member_data, md)
|
||||
|
||||
|
@ -131,6 +152,9 @@ class Member:
|
|||
return r
|
||||
|
||||
def get_and_decrypt_recovery_share(self, remote_node):
|
||||
if not self.is_recovery_member:
|
||||
raise ValueError(f"Member {self.member_id} does not have a recovery share")
|
||||
|
||||
with remote_node.client(f"member{self.member_id}") as mc:
|
||||
r = mc.get("/gov/recovery_share")
|
||||
if r.status_code != http.HTTPStatus.OK.value:
|
||||
|
@ -146,7 +170,9 @@ class Member:
|
|||
)
|
||||
|
||||
def get_and_submit_recovery_share(self, remote_node):
|
||||
# For now, all members are given an encryption key (for recovery)
|
||||
if not self.is_recovery_member:
|
||||
raise ValueError(f"Member {self.member_id} does not have a recovery share")
|
||||
|
||||
res = infra.proc.ccall(
|
||||
self.share_script,
|
||||
f"https://{remote_node.host}:{remote_node.rpc_port}",
|
||||
|
|
|
@ -340,17 +340,23 @@ class Network:
|
|||
self._setup_common_folder(args.gov_script)
|
||||
|
||||
mc = max(1, args.initial_member_count)
|
||||
initial_member_ids = [(i, None) for i in range(mc)]
|
||||
initial_member_ids.extend(
|
||||
(i, {"is_operator": True})
|
||||
for i in range(mc, mc + args.initial_operator_count)
|
||||
)
|
||||
initial_members_info = []
|
||||
for i in range(mc):
|
||||
initial_members_info += [
|
||||
(
|
||||
i,
|
||||
(i < args.initial_recovery_member_count),
|
||||
{"is_operator": True}
|
||||
if (i < args.initial_operator_count)
|
||||
else None,
|
||||
)
|
||||
]
|
||||
|
||||
self.consortium = infra.consortium.Consortium(
|
||||
self.common_dir,
|
||||
self.key_generator,
|
||||
self.share_script,
|
||||
initial_member_ids,
|
||||
initial_members_info,
|
||||
args.participants_curve,
|
||||
)
|
||||
initial_users = list(range(max(0, args.initial_user_count)))
|
||||
|
@ -385,7 +391,7 @@ class Network:
|
|||
self.consortium.set_jwt_issuer(remote_node=primary, json_path=path)
|
||||
|
||||
self.consortium.add_users(primary, initial_users)
|
||||
LOG.info("Initial set of users added")
|
||||
LOG.info(f"Initial set of users added: {len(initial_users)}")
|
||||
|
||||
self.consortium.open_network(remote_node=primary)
|
||||
self.status = ServiceStatus.OPEN
|
||||
|
|
|
@ -680,15 +680,24 @@ class CCFRemote(object):
|
|||
"--network-cert-file=networkcert.pem",
|
||||
f"--gov-script={os.path.basename(gov_script)}",
|
||||
]
|
||||
data_files += [os.path.join(os.path.basename(self.common_dir), gov_script)]
|
||||
if members_info is None:
|
||||
raise ValueError(
|
||||
"Starting node should be given at least one tuple of (member certificate, member public encryption key[, member data])"
|
||||
"Starting node should be given at least one member info"
|
||||
)
|
||||
for mi in members_info:
|
||||
member_info_cmd = f"--member-info={mi.certificate_file}"
|
||||
if mi.encryption_pub_key_file is not None:
|
||||
member_info_cmd += f",{mi.encryption_pub_key_file}"
|
||||
elif mi.member_data_file is not None:
|
||||
member_info_cmd += ","
|
||||
if mi.member_data_file is not None:
|
||||
member_info_cmd += f",{mi.member_data_file}"
|
||||
for mf in mi:
|
||||
data_files.append(os.path.join(self.common_dir, mf))
|
||||
cmd += [f"--member-info={','.join(mi)}"]
|
||||
data_files += [os.path.join(os.path.basename(self.common_dir), gov_script)]
|
||||
if mf is not None:
|
||||
data_files.append(os.path.join(self.common_dir, mf))
|
||||
cmd += [member_info_cmd]
|
||||
|
||||
elif start_type == StartType.join:
|
||||
cmd += [
|
||||
"join",
|
||||
|
@ -697,8 +706,10 @@ class CCFRemote(object):
|
|||
f"--join-timer={join_timer}",
|
||||
]
|
||||
data_files += [os.path.join(self.common_dir, "networkcert.pem")]
|
||||
|
||||
elif start_type == StartType.recover:
|
||||
cmd += ["recover", "--network-cert-file=networkcert.pem"]
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected CCFRemote start type {start_type}. Should be start, join or recover"
|
||||
|
|
|
@ -7,81 +7,12 @@ import infra.network
|
|||
import infra.consortium
|
||||
import ccf.proposal_generator
|
||||
from infra.proposal import ProposalState
|
||||
import random
|
||||
|
||||
import suite.test_requirements as reqs
|
||||
|
||||
from loguru import logger as LOG
|
||||
|
||||
|
||||
@reqs.description("Set recovery threshold")
|
||||
def test_set_recovery_threshold(network, args, recovery_threshold=None):
|
||||
if recovery_threshold is None:
|
||||
# If the recovery threshold is not specified, a new threshold is
|
||||
# randomly selected based on the number of active members. The new
|
||||
# recovery threshold is guaranteed to be different from the
|
||||
# previous one.
|
||||
list_recovery_threshold = list(
|
||||
range(1, len(network.consortium.get_active_members()) + 1)
|
||||
)
|
||||
list_recovery_threshold.remove(network.consortium.recovery_threshold)
|
||||
recovery_threshold = random.choice(list_recovery_threshold)
|
||||
|
||||
primary, _ = network.find_primary()
|
||||
network.consortium.set_recovery_threshold(primary, recovery_threshold)
|
||||
LOG.info(f"Recovery threshold is now {recovery_threshold}")
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Add a new member to the consortium (+ activation)")
|
||||
def test_add_member(network, args):
|
||||
primary, _ = network.find_primary()
|
||||
|
||||
member_data = {
|
||||
"example": "of",
|
||||
"structured": ["and", {"nested": "arbitrary data"}],
|
||||
}
|
||||
|
||||
new_member = network.consortium.generate_and_add_new_member(
|
||||
primary,
|
||||
curve=infra.network.ParticipantsCurve(args.participants_curve).next(),
|
||||
member_data=member_data,
|
||||
)
|
||||
|
||||
try:
|
||||
new_member.get_and_decrypt_recovery_share(primary)
|
||||
assert False, "New accepted members are not given recovery shares"
|
||||
except infra.member.NoRecoveryShareFound as e:
|
||||
assert e.response.body.text() == "Only active members are given recovery shares"
|
||||
|
||||
r = new_member.ack(primary)
|
||||
with primary.client() as nc:
|
||||
check_commit = infra.checker.Checker(nc)
|
||||
check_commit(r)
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Retire an existing member")
|
||||
@reqs.sufficient_member_count()
|
||||
def test_retire_member(network, args, member_to_retire=None):
|
||||
primary, _ = network.find_primary()
|
||||
|
||||
if member_to_retire is None:
|
||||
member_to_retire = network.consortium.get_any_active_member()
|
||||
network.consortium.retire_member(primary, member_to_retire)
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Issue new recovery shares (without re-key)")
|
||||
def test_update_recovery_shares(network, args):
|
||||
primary, _ = network.find_primary()
|
||||
network.consortium.update_recovery_shares(primary)
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Send an unsigned request where signature is required")
|
||||
def test_missing_signature(network, args):
|
||||
primary, _ = network.find_primary()
|
||||
|
@ -107,36 +38,6 @@ def test_missing_signature(network, args):
|
|||
return network
|
||||
|
||||
|
||||
def assert_recovery_shares_update(func, network, args, **kwargs):
|
||||
primary, _ = network.find_primary()
|
||||
|
||||
recovery_threshold_before = network.consortium.recovery_threshold
|
||||
active_members_before = network.consortium.get_active_members()
|
||||
already_active_member = network.consortium.get_any_active_member()
|
||||
saved_share = already_active_member.get_and_decrypt_recovery_share(primary)
|
||||
|
||||
if func is test_retire_member:
|
||||
# When retiring a member, the active member which retrieved their share
|
||||
# should not be retired for them to be able to compare their share afterwards.
|
||||
member_to_retire = [
|
||||
m
|
||||
for m in network.consortium.get_active_members()
|
||||
if m is not already_active_member
|
||||
][0]
|
||||
func(network, args, member_to_retire)
|
||||
elif func is test_set_recovery_threshold and "recovery_threshold" in kwargs:
|
||||
func(network, args, recovery_threshold=kwargs["recovery_threshold"])
|
||||
else:
|
||||
func(network, args)
|
||||
|
||||
if (
|
||||
recovery_threshold_before != network.consortium.recovery_threshold
|
||||
or active_members_before != network.consortium.get_active_members
|
||||
):
|
||||
new_share = already_active_member.get_and_decrypt_recovery_share(primary)
|
||||
assert saved_share != new_share, "New recovery shares should have been issued"
|
||||
|
||||
|
||||
def run(args):
|
||||
with infra.network.network(
|
||||
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
||||
|
@ -260,63 +161,6 @@ def run(args):
|
|||
response = new_member.vote(primary, trust_node_proposal, careful_vote)
|
||||
assert response.status_code == params_error
|
||||
|
||||
# Membership changes trigger re-sharing and re-keying and are
|
||||
# only supported with Raft
|
||||
if args.consensus == "cft":
|
||||
LOG.debug("New member proposes to retire member 0")
|
||||
network.consortium.retire_member(
|
||||
primary, network.consortium.get_member_by_id(0)
|
||||
)
|
||||
|
||||
LOG.debug("Retired member cannot make a new proposal")
|
||||
try:
|
||||
network.consortium.get_member_by_id(0).propose(
|
||||
primary, proposal_trust_0
|
||||
)
|
||||
assert False, "Retired member cannot make a new proposal"
|
||||
except infra.proposal.ProposalNotCreated as e:
|
||||
assert e.response.status_code == http.HTTPStatus.FORBIDDEN.value
|
||||
assert e.response.body.text() == "Member is not active"
|
||||
|
||||
LOG.debug("New member should still be able to make a new proposal")
|
||||
new_proposal = new_member.propose(primary, proposal_trust_0)
|
||||
assert new_proposal.state == ProposalState.Open
|
||||
|
||||
LOG.info(
|
||||
"Recovery threshold is originally set to the original number of members"
|
||||
)
|
||||
LOG.info("Retiring a member should not be possible")
|
||||
try:
|
||||
assert_recovery_shares_update(test_retire_member, network, args)
|
||||
assert False, "Retiring a member should not be possible"
|
||||
except infra.proposal.ProposalNotAccepted as e:
|
||||
assert e.proposal.state == infra.proposal.ProposalState.Failed
|
||||
|
||||
assert_recovery_shares_update(test_add_member, network, args)
|
||||
assert_recovery_shares_update(test_retire_member, network, args)
|
||||
|
||||
LOG.info("Set different recovery thresholds")
|
||||
assert_recovery_shares_update(
|
||||
test_set_recovery_threshold, network, args, recovery_threshold=1
|
||||
)
|
||||
test_set_recovery_threshold(
|
||||
network,
|
||||
args,
|
||||
recovery_threshold=network.consortium.recovery_threshold,
|
||||
)
|
||||
|
||||
LOG.info(
|
||||
"Setting the recovery threshold above the number of active members is not possible"
|
||||
)
|
||||
try:
|
||||
test_set_recovery_threshold(
|
||||
network,
|
||||
args,
|
||||
recovery_threshold=len(network.consortium.get_active_members()) + 1,
|
||||
)
|
||||
except infra.proposal.ProposalNotAccepted as e:
|
||||
assert e.proposal.state == infra.proposal.ProposalState.Failed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = infra.e2e_args.cli_args()
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the Apache 2.0 License.
|
||||
import http
|
||||
|
||||
import infra.e2e_args
|
||||
import infra.network
|
||||
import infra.consortium
|
||||
import random
|
||||
|
||||
import suite.test_requirements as reqs
|
||||
|
||||
from loguru import logger as LOG
|
||||
|
||||
|
||||
@reqs.description("Add and activate a new member to the consortium")
|
||||
def test_add_member(network, args, recovery_member=True):
|
||||
primary, _ = network.find_primary()
|
||||
|
||||
member_data = {
|
||||
"example": "of",
|
||||
"structured": ["and", {"nested": "arbitrary data"}],
|
||||
}
|
||||
|
||||
new_member = network.consortium.generate_and_add_new_member(
|
||||
primary,
|
||||
curve=infra.network.ParticipantsCurve(args.participants_curve).next(),
|
||||
member_data=member_data,
|
||||
recovery_member=recovery_member,
|
||||
)
|
||||
|
||||
r = new_member.ack(primary)
|
||||
with primary.client() as nc:
|
||||
nc.wait_for_commit(r)
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Retire existing member")
|
||||
@reqs.sufficient_recovery_member_count()
|
||||
def test_retire_member(network, args, member_to_retire=None, recovery_member=True):
|
||||
primary, _ = network.find_primary()
|
||||
if member_to_retire is None:
|
||||
member_to_retire = network.consortium.get_any_active_member(recovery_member)
|
||||
network.consortium.retire_member(primary, member_to_retire)
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Issue new recovery shares (without re-key)")
|
||||
def test_update_recovery_shares(network, args):
|
||||
primary, _ = network.find_primary()
|
||||
network.consortium.update_recovery_shares(primary)
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Set recovery threshold")
|
||||
def test_set_recovery_threshold(network, args, recovery_threshold=None):
|
||||
if recovery_threshold is None:
|
||||
# If the recovery threshold is not specified, a new threshold is
|
||||
# randomly selected based on the number of active recovery members.
|
||||
# The new recovery threshold is guaranteed to be different from the
|
||||
# previous one.
|
||||
list_recovery_threshold = list(
|
||||
range(1, len(network.consortium.get_active_recovery_members()) + 1)
|
||||
)
|
||||
list_recovery_threshold.remove(network.consortium.recovery_threshold)
|
||||
recovery_threshold = random.choice(list_recovery_threshold)
|
||||
|
||||
primary, _ = network.find_primary()
|
||||
network.consortium.set_recovery_threshold(primary, recovery_threshold)
|
||||
return network
|
||||
|
||||
|
||||
def assert_recovery_shares_update(are_shared_updated, func, network, args, **kwargs):
|
||||
primary, _ = network.find_primary()
|
||||
|
||||
saved_recovery_shares = {}
|
||||
for m in network.consortium.get_active_recovery_members():
|
||||
saved_recovery_shares[m] = m.get_and_decrypt_recovery_share(primary)
|
||||
|
||||
if func is test_retire_member:
|
||||
recovery_member = kwargs.pop("recovery_member")
|
||||
member_to_retire = network.consortium.get_any_active_member(
|
||||
recovery_member=recovery_member
|
||||
)
|
||||
if recovery_member:
|
||||
saved_recovery_shares.pop(member_to_retire)
|
||||
|
||||
func(network, args, member_to_retire)
|
||||
elif func is test_set_recovery_threshold and "recovery_threshold" in kwargs:
|
||||
func(network, args, recovery_threshold=kwargs["recovery_threshold"])
|
||||
else:
|
||||
func(network, args, **kwargs)
|
||||
|
||||
for m, share_before in saved_recovery_shares.items():
|
||||
if are_shared_updated:
|
||||
assert share_before != m.get_and_decrypt_recovery_share(primary)
|
||||
else:
|
||||
assert share_before == m.get_and_decrypt_recovery_share(primary)
|
||||
|
||||
|
||||
def service_startups(args):
|
||||
LOG.info("Starting service with insufficient number of recovery members")
|
||||
args.initial_member_count = 2
|
||||
args.initial_recovery_member_count = 0
|
||||
args.initial_operator_count = 1
|
||||
with infra.network.network(args.nodes, args.binary_dir, pdb=args.pdb) as network:
|
||||
try:
|
||||
network.start_and_join(args)
|
||||
assert False, "Service cannot be opened with no recovery members"
|
||||
except infra.proposal.ProposalNotAccepted:
|
||||
LOG.success(
|
||||
"Service could not be opened with insufficient number of recovery mmebers"
|
||||
)
|
||||
|
||||
LOG.info(
|
||||
"Starting service with a recovery operator member, a non-recovery operator member and a non-recovery non-operator member"
|
||||
)
|
||||
args.initial_member_count = 3
|
||||
args.initial_recovery_member_count = 1
|
||||
args.initial_operator_count = 2
|
||||
with infra.network.network(args.nodes, args.binary_dir, pdb=args.pdb) as network:
|
||||
network.start_and_join(args)
|
||||
|
||||
LOG.info(
|
||||
"Starting service with a recovery operator member, a recovery non-operator member and a non-recovery non-operator member"
|
||||
)
|
||||
args.initial_member_count = 3
|
||||
args.initial_recovery_member_count = 2
|
||||
args.initial_operator_count = 1
|
||||
with infra.network.network(args.nodes, args.binary_dir, pdb=args.pdb) as network:
|
||||
network.start_and_join(args)
|
||||
|
||||
|
||||
def recovery_shares_scenario(args):
|
||||
# Members 0 and 1 are recovery members, member 2 isn't
|
||||
args.initial_member_count = 3
|
||||
args.initial_recovery_member_count = 2
|
||||
non_recovery_member_id = 2
|
||||
|
||||
# Recovery threshold is initially set to number of recovery members (2)
|
||||
with infra.network.network(
|
||||
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
||||
) as network:
|
||||
network.start_and_join(args)
|
||||
|
||||
# Membership changes trigger re-sharing and re-keying and are
|
||||
# only supported with CFT
|
||||
if args.consensus != "cft":
|
||||
LOG.warning("Skipping test recovery threshold as consensus is not CFT")
|
||||
return
|
||||
|
||||
LOG.info("Update recovery shares")
|
||||
assert_recovery_shares_update(True, test_update_recovery_shares, network, args)
|
||||
|
||||
LOG.info("Non-recovery member does not have a recovery share")
|
||||
primary, _ = network.find_primary()
|
||||
with primary.client(f"member{non_recovery_member_id}") as mc:
|
||||
r = mc.get("/gov/recovery_share")
|
||||
assert r.status_code == http.HTTPStatus.NOT_FOUND.value
|
||||
assert (
|
||||
r.body.text()
|
||||
== f"Recovery share not found for member {non_recovery_member_id}"
|
||||
)
|
||||
|
||||
# Retiring a recovery number is not possible as the number of recovery
|
||||
# members would be under recovery threshold (2)
|
||||
LOG.info("Retiring a recovery member should not be possible")
|
||||
try:
|
||||
test_retire_member(network, args, recovery_member=True)
|
||||
assert False, "Retiring a recovery member should not be possible"
|
||||
except infra.proposal.ProposalNotAccepted as e:
|
||||
assert e.proposal.state == infra.proposal.ProposalState.Failed
|
||||
|
||||
# However, retiring a non-recovery member is allowed
|
||||
LOG.info("Retiring a non-recovery member is still possible")
|
||||
member_to_retire = network.consortium.get_member_by_id(non_recovery_member_id)
|
||||
test_retire_member(network, args, member_to_retire=member_to_retire)
|
||||
|
||||
LOG.info("Adding one non-recovery member")
|
||||
assert_recovery_shares_update(
|
||||
False, test_add_member, network, args, recovery_member=False
|
||||
)
|
||||
LOG.info("Adding one recovery member")
|
||||
assert_recovery_shares_update(
|
||||
True, test_add_member, network, args, recovery_member=True
|
||||
)
|
||||
LOG.info("Retiring one non-recovery member")
|
||||
assert_recovery_shares_update(
|
||||
False, test_retire_member, network, args, recovery_member=False
|
||||
)
|
||||
LOG.info("Retiring one recovery member")
|
||||
assert_recovery_shares_update(
|
||||
True, test_retire_member, network, args, recovery_member=True
|
||||
)
|
||||
|
||||
LOG.info("Reduce recovery threshold")
|
||||
assert_recovery_shares_update(
|
||||
True,
|
||||
test_set_recovery_threshold,
|
||||
network,
|
||||
args,
|
||||
recovery_threshold=network.consortium.recovery_threshold - 1,
|
||||
)
|
||||
|
||||
# Retiring a recovery member now succeeds
|
||||
LOG.info("Retiring one recovery member")
|
||||
assert_recovery_shares_update(
|
||||
True, test_retire_member, network, args, recovery_member=True
|
||||
)
|
||||
|
||||
LOG.info("Set recovery threshold to 0 is impossible")
|
||||
try:
|
||||
test_set_recovery_threshold(network, args, recovery_threshold=0)
|
||||
assert False, "Setting recovery threshold to 0 should not be possible"
|
||||
except infra.proposal.ProposalNotAccepted as e:
|
||||
assert e.proposal.state == infra.proposal.ProposalState.Failed
|
||||
|
||||
LOG.info(
|
||||
"Set recovery threshold to more that number of active recovery members is impossible"
|
||||
)
|
||||
try:
|
||||
test_set_recovery_threshold(
|
||||
network,
|
||||
args,
|
||||
recovery_threshold=len(network.consortium.get_active_recovery_members())
|
||||
+ 1,
|
||||
)
|
||||
assert (
|
||||
False
|
||||
), "Setting recovery threshold to more than number of active recovery members should not be possible"
|
||||
except infra.proposal.ProposalNotAccepted as e:
|
||||
assert e.proposal.state == infra.proposal.ProposalState.Failed
|
||||
|
||||
LOG.info(
|
||||
"Setting recovery threshold to current threshold does not update shares"
|
||||
)
|
||||
assert_recovery_shares_update(
|
||||
False,
|
||||
test_set_recovery_threshold,
|
||||
network,
|
||||
args,
|
||||
recovery_threshold=network.consortium.recovery_threshold,
|
||||
)
|
||||
|
||||
|
||||
def run(args):
|
||||
service_startups(args)
|
||||
recovery_shares_scenario(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = infra.e2e_args.cli_args()
|
||||
args.package = args.app_script or "liblogging"
|
||||
|
||||
# Fast test
|
||||
args.nodes = infra.e2e_args.min_nodes(args, f=0)
|
||||
args.initial_user_count = 0
|
||||
run(args)
|
|
@ -80,14 +80,14 @@ def at_least_n_nodes(n):
|
|||
return ensure_reqs(check)
|
||||
|
||||
|
||||
def sufficient_member_count():
|
||||
def sufficient_recovery_member_count():
|
||||
def check(network, args, *nargs, **kwargs):
|
||||
if (
|
||||
len(network.consortium.get_active_members())
|
||||
len(network.consortium.get_active_recovery_members())
|
||||
<= network.consortium.recovery_threshold
|
||||
):
|
||||
raise TestRequirementsNotMet(
|
||||
"Cannot retire a member since number of active members"
|
||||
"Cannot retire recovery member since number of active recovery members"
|
||||
f" ({len(network.consortium.get_active_members()) - 1}) would be less than"
|
||||
f" the recovery threshold ({network.consortium.recovery_threshold})"
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ import recovery
|
|||
import rekey
|
||||
import election
|
||||
import code_update
|
||||
import membership
|
||||
|
||||
from inspect import signature, Parameter
|
||||
|
||||
|
@ -29,13 +30,13 @@ suites["rekey_recovery"] = suite_rekey_recovery
|
|||
|
||||
# This suite tests that membership changes and recoveries can be interleaved
|
||||
suite_membership_recovery = [
|
||||
memberclient.test_add_member,
|
||||
membership.test_add_member,
|
||||
recovery.test,
|
||||
memberclient.test_retire_member,
|
||||
membership.test_retire_member,
|
||||
recovery.test,
|
||||
memberclient.test_set_recovery_threshold,
|
||||
membership.test_set_recovery_threshold,
|
||||
recovery.test,
|
||||
memberclient.test_update_recovery_shares,
|
||||
membership.test_update_recovery_shares,
|
||||
recovery.test,
|
||||
]
|
||||
suites["membership_recovery"] = suite_membership_recovery
|
||||
|
@ -78,11 +79,13 @@ all_tests_suite = [
|
|||
e2e_logging.test_user_data_ACL,
|
||||
e2e_logging.test_view_history,
|
||||
e2e_logging.test_tx_statuses,
|
||||
# membership:
|
||||
membership.test_set_recovery_threshold,
|
||||
membership.test_add_member,
|
||||
membership.test_retire_member,
|
||||
membership.test_retire_member,
|
||||
membership.test_update_recovery_shares,
|
||||
# memberclient:
|
||||
memberclient.test_set_recovery_threshold,
|
||||
memberclient.test_add_member,
|
||||
memberclient.test_retire_member,
|
||||
memberclient.test_update_recovery_shares,
|
||||
memberclient.test_missing_signature,
|
||||
# receipts:
|
||||
receipts.test,
|
||||
|
|
Загрузка…
Ссылка в новой задаче