This commit is contained in:
Julien Maffre 2020-11-10 15:34:58 +00:00 коммит произвёл GitHub
Родитель 82e4d007fd
Коммит 7afef2cc2b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
30 изменённых файлов: 805 добавлений и 492 удалений

Просмотреть файл

@ -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 members 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 members 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 members 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()

259
tests/membership.py Normal file
Просмотреть файл

@ -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,