зеркало из https://github.com/microsoft/CCF.git
Make implicit proposer vote explicit (#339)
* Proposer sends ballot * Remove unnecessary error log line * Add test of proposer initially voting against * Throw errors, not strings * Update memberclient documentation * --standardize-long-options * Remove final references to nodes.json * Modified propose_params * Python wrappers use --updated-option-names * --member-cert * '=' != ' '
This commit is contained in:
Родитель
65bde18079
Коммит
c9b0f7ef5f
|
@ -20,7 +20,7 @@ For example, ``member1`` may submit a proposal to add a new member (``member4``)
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ memberclient add_member --ca=networkcert.pem --member_cert=member4_cert.pem --cert=member1_cert.pem --privk=member1_privk.pem --host=10.1.0.4 --port=25000
|
||||
$ memberclient --server-address 127.83.203.69:55526 --cert member1_cert.pem --privk member1_privk.pem --ca networkcert.pem add_member --member-cert member4_cert.pem
|
||||
{"commit":100,"global_commit":99,"id":0,"jsonrpc":"2.0","result":{"completed":false,"id":1},"term":2}
|
||||
|
||||
In this case, a new proposal with id ``1`` has successfully been created and the proposer member has automatically accepted it. Other members can then accept or reject the proposal:
|
||||
|
@ -30,12 +30,12 @@ In this case, a new proposal with id ``1`` has successfully been created and the
|
|||
// Proposal 1 is already created by member 1 (votes: 1/3)
|
||||
|
||||
// Member 2 rejects the proposal (votes: 1/3)
|
||||
$ memberclient vote --reject --id=1 --cert=member2_cert.pem --privk=member2_privk.pem --host=10.1.0.4 --port=25000 --ca=networkcert.pem --sign
|
||||
$ memberclient --server-address 127.83.203.69:55526 --cert member2_cert.pem --privk member2_privk.pem --ca networkcert.pem vote --reject --proposal-id 1
|
||||
{"commit":104,"global_commit":103,"id":0,"jsonrpc":"2.0","result":false,"term":2}
|
||||
|
||||
// Member 3 accepts the proposal (votes: 2/3)
|
||||
// As a quorum of members have accepted the proposal, member4 is added to the consortium
|
||||
$ memberclient vote --accept --id=1 --cert=member3_cert.pem --privk=member3_privk.pem --host=10.1.0.4 --port=25000 --ca=networkcert.pem --sign
|
||||
$ memberclient --server-address 127.83.203.69:55526 --cert member3_cert.pem --privk member3_privk.pem --ca networkcert.pem vote --accept --proposal-id 1
|
||||
{"commit":106,"global_commit":105,"id":0,"jsonrpc":"2.0","result":true,"term":2}
|
||||
|
||||
As soon as ``member3`` accepts the proposal, a quorum (2 out of 3) of members has been reached and the proposal completes, successfully adding ``member4``.
|
||||
|
@ -44,34 +44,50 @@ As soon as ``member3`` accepts the proposal, a quorum (2 out of 3) of members ha
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ ../build/memberclient ack --cert=member4_cert.pem --privk=member4_privk.pem --host=10.1.0.4 --port=25000 --ca=networkcert.pem --sign
|
||||
$ memberclient --server-address 127.83.203.69:55526 --cert member4_cert.pem --privk member4_privk.pem --ca networkcert.pem ack
|
||||
{"commit":108,"global_commit":107,"id":2,"jsonrpc":"2.0","result":true,"term":2}
|
||||
|
||||
|
||||
Displaying proposals
|
||||
--------------------
|
||||
|
||||
The details of pending proposals, including the proposer member ID, proposal script, parameters and votes, can be displayed with the ``proposal_display`` option of the ``memberclient`` utility. For example:
|
||||
The details of pending proposals, including the proposer member ID, proposal script, parameters and votes, can be displayed with the ``proposal_display`` sub command of the ``memberclient`` utility. For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ memberclient proposal_display --cert=member1_cert.pem --privk=member1_privk.pem --host=10.1.0.4 --port=25000 --ca=networkcert.pem
|
||||
------ Proposal ------
|
||||
-- Proposal id: 1
|
||||
-- Proposer id: 0
|
||||
-- Script: {"text":"\n tables, member_cert = ...\n return Calls:call(\"new_member\", member_cert)\n "}
|
||||
-- Parameter: [<member_cert>]
|
||||
-- Votes: [[1,{"text":"\n tables, changes = ...\n return false"}]]
|
||||
----------------------
|
||||
$ memberclient --server-address 127.83.203.69:55526 --cert member1_cert.pem --privk member1_privk.pem --ca networkcert.pem proposal_display
|
||||
{
|
||||
"1": {
|
||||
"parameter": [...],
|
||||
"proposer": 0,
|
||||
"script": {
|
||||
"text": "tables, member_cert = ...\n return Calls:call(\"new_member\", member_cert)"
|
||||
},
|
||||
"votes": [
|
||||
[
|
||||
0,
|
||||
{
|
||||
"text": "return true"
|
||||
}
|
||||
],
|
||||
[
|
||||
1,
|
||||
{
|
||||
"text": "return false"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
In this case, there is one pending proposal (``id`` is 1), proposed by the first member (``member1``, ``id`` is 0) and which will call the ``new_member`` function with the new member's certificate as a parameter. Only one vote has been cast by ``member2`` (``id`` is 1) to reject the proposal while ``member1`` (proposer) has already implicitly accepted it.
|
||||
In this case, there is one pending proposal (``id`` is 1), proposed by the first member (``member1``, ``id`` is 0) and which will call the ``accept_node`` function with the new member's certificate as a parameter. Two votes have been cast: ``member1`` (proposer) has voted for the proposal, while ``member2`` (``id`` is 1) has voted against it.
|
||||
|
||||
Removing proposals
|
||||
------------------
|
||||
|
||||
At any stage during the voting process and before the proposal is completed, the proposer member may decide to remove a pending proposal:
|
||||
At any stage during the voting process and before the proposal is completed, the proposing member may decide to remove a pending proposal:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ memberclient removal --id=1 --cert=member1_cert.pem --privk=member1_privk.pem --host=10.1.0.4 --port=25000 --ca=networkcert.pem --sign
|
||||
$ memberclient --server-address 127.83.203.69:55526 --cert member1_cert.pem --privk member1_privk.pem --ca networkcert.pem removal --proposal-id 0
|
||||
{"commit":110,"global_commit":109,"id":0,"jsonrpc":"2.0","result":true,"term":4}
|
||||
|
|
|
@ -22,11 +22,11 @@ To initiate the first phase of the recovery protocol, one or several nodes shoul
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cchost --enclave-file=/path/to/enclave_library --enclave-type=debug --node-address=node_ip:node_port
|
||||
--rpc-address=rpc_ip:rpc_port --public-rpc-address=public_rpc_ip:public_rpc_port
|
||||
--ledger-file=/path/to/ledger/to/recover
|
||||
--node-cert-file=/path/to/node_certificate --quote-file=/path/to/quote
|
||||
recover --network-cert-file=/path/to/network_certificate
|
||||
$ cchost --enclave-file /path/to/enclave_library --enclave-type debug --node-address node_ip:node_port
|
||||
--rpc-address rpc_ip:rpc_port --public-rpc-address public_rpc_ip:public_rpc_port
|
||||
--ledger-file /path/to/ledger/to/recover
|
||||
--node-cert-file /path/to/node_certificate --quote-file /path/to/quote
|
||||
recover --network-cert-file /path/to/network_certificate
|
||||
|
||||
Each node will then immediately restore the public entries of its ledger (``--ledger-file``). Because deserialising the public entries present in the ledger may take some time, operators can query the progress of the public recovery by running the ``getSignedIndex`` JSON-RPC which returns the version of the last signed recovered ledger entry. Once the public ledger is fully recovered, the recovered node automatically becomes part of the public network, allowing other nodes to join the network.
|
||||
|
||||
|
@ -67,17 +67,17 @@ Once the public crash-fault tolerant network is established, members are allowed
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ memberclient --cert=/path/to/member1/certificate --privk=/path/to/member1/private/key
|
||||
--rpc-address=node2_rpc_ip:node2_rpc_port --ca=/path/to/new/network/certificate
|
||||
accept_recovery --sealed-secrets=/path/to/sealed/secrets/file
|
||||
$ memberclient --cert /path/to/member1/certificate --privk /path/to/member1/private/key
|
||||
--rpc-address node2_rpc_ip:node2_rpc_port --ca /path/to/new/network/certificate
|
||||
accept_recovery --sealed-secrets /path/to/sealed/secrets/file
|
||||
|
||||
If successful, this commands returns the proposal id that can be used by other members to submit their votes:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ memberclient --cert=/path/to/member2/certificate --privk=/path/to/member2/private/key
|
||||
--rpc-address=node2_rpc_ip:node2_rpc_port --ca=/path/to/new/network/certificate
|
||||
vote --accept --proposal-id=proposal_id
|
||||
$ memberclient --cert /path/to/member2/certificate --privk /path/to/member2/private/key
|
||||
--rpc-address node2_rpc_ip:node2_rpc_port --ca /path/to/new/network/certificate
|
||||
vote --accept --proposal-id proposal_id
|
||||
|
||||
Once a :term:`quorum` of members have agreed to recover the network, the network secrets are unsealed and each node begins recovery of the private ledger entries.
|
||||
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"ballot": {
|
||||
"properties": {
|
||||
"bytecode": {
|
||||
"items": {
|
||||
"maximum": 255,
|
||||
"minimum": 0,
|
||||
"type": "number"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"parameter": {},
|
||||
"script": {
|
||||
"properties": {
|
||||
|
|
|
@ -8,24 +8,24 @@ To start up a network, the first node of the network should be started with the
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cchost --enclave-file=/path/to/enclave_library --enclave-type=debug
|
||||
--node-address=node_ip:node_port --rpc-address=rpc_ip:rpc_port
|
||||
--public-rpc-address=public_rpc_ip:public_rpc_port --ledger-file=/path/to/ledger
|
||||
--node-cert-file=/path/to/node_certificate --quote-file=/path/to/quote
|
||||
start --network-cert-file=/path/to/network_certificate --gov-script=/path/to/lua/governance_script
|
||||
--member-certs=member_certificates_glob --user-certs=user_certificates_glob
|
||||
$ cchost --enclave-file /path/to/enclave_library --enclave-type debug
|
||||
--node-address node_ip:node_port --rpc-address rpc_ip:rpc_port
|
||||
--public-rpc-address public_rpc_ip:public_rpc_port --ledger-file /path/to/ledger
|
||||
--node-cert-file /path/to/node_certificate --quote-file /path/to/quote
|
||||
start --network-cert-file /path/to/network_certificate --gov-script /path/to/lua/governance_script
|
||||
--member-certs member_certificates_glob --user-certs user_certificates_glob
|
||||
|
||||
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``. A quote file, required for remote attestation, is also output at the location specified by ``--quote-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 used by users and members as the certificate authority (CA) when establishing a TLS connection with any of the nodes part of the CCF network. For the ``client`` and ``memberclient`` utilities, ``--ca=/path/to/network_certificate`` should always be specified.
|
||||
.. note:: The network certificate should be used by users and members as the certificate authority (CA) when establishing a TLS connection with any of the nodes part of the CCF network. For the ``client`` and ``memberclient`` utilities, ``--ca /path/to/network_certificate`` should always be specified.
|
||||
|
||||
The :ref:`governance` rules are defined as a Lua script passed via the ``--gov-script`` option. For example, a default set of `governance rules <https://github.com/microsoft/CCF/blob/master/src/runtime_config/gov.lua>`_ can be used to define a majority of members as the :term:`quorum` of the consortium.
|
||||
|
||||
The identities of members and users are specified as `glob patterns <https://en.wikipedia.org/wiki/Glob_(programming)>`_ via the ``--member-certs`` and ``--user-certs`` option, respectively. For example, if 2 members (``member1_cert.pem`` and ``member2_cert.pem``) and 3 users (``user1_cert.pem``, ``user2_cert.pem`` and ``user3_cert.pem``) should be added to CCF, operators should specify ``--member-certs=member*_cert.pem`` and ``--user-certs=user*_cert.pem``.
|
||||
The identities of members and users are specified as `glob patterns <https://en.wikipedia.org/wiki/Glob_(programming)>`_ via the ``--member-certs`` and ``--user-certs`` option, respectively. For example, if 2 members (``member1_cert.pem`` and ``member2_cert.pem``) and 3 users (``user1_cert.pem``, ``user2_cert.pem`` and ``user3_cert.pem``) should be added to CCF, operators should specify ``--member-certs member*_cert.pem`` and ``--user-certs user*_cert.pem``.
|
||||
|
||||
.. note:: Once a CCF network is started, members can add other members and users via governance. See :ref:`Submitting a new proposal` for more information.
|
||||
|
||||
When CCF is used to run a custom Lua application, the starting node should also be started with the ``--app-script=/path/to/lua/application_script`` (see the `samples folder <https://github.com/microsoft/CCF/tree/master/samples/apps>`_ for example of Lua applications).
|
||||
When CCF is used to run a custom Lua application, the starting node should also be started with the ``--app-script /path/to/lua/application_script`` (see the `samples folder <https://github.com/microsoft/CCF/tree/master/samples/apps>`_ for example of Lua applications).
|
||||
|
||||
Joining an existing network
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -34,11 +34,11 @@ To join an existing network, other nodes should be started with the ``join`` opt
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cchost --enclave-file=/path/to/enclave_library --enclave-type=debug
|
||||
--node-address=node_ip:node_port --rpc-address=rpc_ip:rpc_port
|
||||
--public-rpc-address=public_rpc_ip:public_rpc_port --ledger-file=/path/to/ledger
|
||||
--node-cert-file=/path/to/node_certificate --quote-file=/path/to/quote
|
||||
join --network-cert-file=/path/to/existing/network_certificate --target-rpc-address=target_rpc_ip:target_rpc_port
|
||||
$ cchost --enclave-file /path/to/enclave_library --enclave-type debug
|
||||
--node-address node_ip:node_port --rpc-address rpc_ip:rpc_port
|
||||
--public-rpc-address public_rpc_ip:public_rpc_port --ledger-file /path/to/ledger
|
||||
--node-cert-file /path/to/node_certificate --quote-file /path/to/quote
|
||||
join --network-cert-file /path/to/existing/network_certificate --target-rpc-address target_rpc_ip:target_rpc_port
|
||||
|
||||
The node takes the certificate of the existing network to join via ``--network-cert-file`` and initiates an enclave-to-enclave TLS connection to an existing node of the network as specified by ``--target-rpc-address``. Once the join protocol [#remote_attestation]_ completes, the joining node becomes part of the network as a backup (see :ref:`Ledger replication` for more details on consensus protocols).
|
||||
|
||||
|
|
|
@ -166,12 +166,6 @@ int main(int argc, char** argv)
|
|||
|
||||
app.require_subcommand(1, 1);
|
||||
|
||||
std::string nodes_file = "nodes.json";
|
||||
size_t node_index = 0;
|
||||
auto nodes_opt = app.add_option("--nodes", nodes_file, "Nodes file", true);
|
||||
app.add_option(
|
||||
"--host-node-index", node_index, "Index of host in nodes file", true);
|
||||
|
||||
bool pretty_print = false;
|
||||
app.add_flag(
|
||||
"--pretty-print",
|
||||
|
@ -183,11 +177,10 @@ int main(int argc, char** argv)
|
|||
|
||||
cli::ParsedAddress server_address;
|
||||
auto server_addr_opt = cli::add_address_option(
|
||||
app,
|
||||
server_address,
|
||||
"--rpc-address",
|
||||
"Remote node JSON-RPC server address")
|
||||
->excludes(nodes_opt);
|
||||
app,
|
||||
server_address,
|
||||
"--rpc-address",
|
||||
"Remote node JSON-RPC server address");
|
||||
app.add_option("--ca", ca_file, "Network CA", true);
|
||||
|
||||
auto member_rpc = app.add_subcommand("memberrpc", "Member RPC");
|
||||
|
@ -222,34 +215,8 @@ int main(int argc, char** argv)
|
|||
|
||||
try
|
||||
{
|
||||
// If host connection has not been set explicitly by options then load from
|
||||
// nodes file
|
||||
if (!*server_addr_opt)
|
||||
{
|
||||
const auto j_nodes = files::slurp_json(nodes_file);
|
||||
|
||||
if (!j_nodes.is_array())
|
||||
{
|
||||
throw logic_error("Expected " + nodes_file + " to contain array");
|
||||
}
|
||||
|
||||
if (node_index >= j_nodes.size())
|
||||
{
|
||||
throw logic_error(
|
||||
"Expected node data at index " + to_string(node_index) + ", but " +
|
||||
nodes_file + " defines only " + to_string(j_nodes.size()) + " files");
|
||||
}
|
||||
|
||||
const auto& j_node = j_nodes[node_index];
|
||||
|
||||
host = j_node["pubhost"];
|
||||
port = j_node["rpcport"];
|
||||
}
|
||||
else
|
||||
{
|
||||
host = server_address.hostname;
|
||||
port = server_address.port;
|
||||
}
|
||||
host = server_address.hostname;
|
||||
port = server_address.port;
|
||||
|
||||
nlohmann::json response;
|
||||
|
||||
|
|
|
@ -288,13 +288,13 @@ int main(int argc, char** argv)
|
|||
auto add_member = app.add_subcommand("add_member", "Add a new member");
|
||||
string member_cert_file;
|
||||
add_member
|
||||
->add_option("--member_cert", member_cert_file, "New member certificate")
|
||||
->add_option("--member-cert", member_cert_file, "New member certificate")
|
||||
->required(true)
|
||||
->check(CLI::ExistingFile);
|
||||
|
||||
auto add_user = app.add_subcommand("add_user", "Add a new user");
|
||||
string user_cert_file;
|
||||
add_user->add_option("--user_cert", user_cert_file, "New user certificate")
|
||||
add_user->add_option("--user-cert", user_cert_file, "New user certificate")
|
||||
->required(true)
|
||||
->check(CLI::ExistingFile);
|
||||
|
||||
|
@ -327,14 +327,14 @@ int main(int argc, char** argv)
|
|||
auto add_node = app.add_subcommand("add_node", "Add a node");
|
||||
add_node
|
||||
->add_option(
|
||||
"--nodes_to_add", nodes_file, "The file containing the nodes to be added")
|
||||
"--nodes-to-add", nodes_file, "The file containing the nodes to be added")
|
||||
->required(true);
|
||||
|
||||
std::string new_code_id;
|
||||
auto add_code = app.add_subcommand("add_code", "Support executing new code");
|
||||
add_code
|
||||
->add_option(
|
||||
"--new_code_id",
|
||||
"--new-code-id",
|
||||
new_code_id,
|
||||
"The new code id (a 64 character string representing a 32 byte hash "
|
||||
"value in hex format)")
|
||||
|
@ -361,7 +361,8 @@ int main(int argc, char** argv)
|
|||
->check(CLI::ExistingFile);
|
||||
|
||||
auto removal = app.add_subcommand("removal", "Remove a proposal");
|
||||
removal->add_option("--id", proposal_id, "The proposal id")->required(true);
|
||||
removal->add_option("--proposal-id", proposal_id, "The proposal id")
|
||||
->required(true);
|
||||
|
||||
auto accept_recovery =
|
||||
app.add_subcommand("accept_recovery", "Accept to recover network");
|
||||
|
|
|
@ -66,12 +66,12 @@ public:
|
|||
auto err = mbedtls_ctr_drbg_seed(
|
||||
&ctr_drbg, mbedtls_entropy_func, &entropy, nullptr, 0);
|
||||
if (err)
|
||||
throw tls::error_string(err);
|
||||
throw std::logic_error(tls::error_string(err));
|
||||
|
||||
err = mbedtls_net_connect(
|
||||
&server_fd, host.c_str(), port.c_str(), MBEDTLS_NET_PROTO_TCP);
|
||||
if (err)
|
||||
throw tls::error_string(err);
|
||||
throw std::logic_error(tls::error_string(err));
|
||||
|
||||
err = mbedtls_ssl_config_defaults(
|
||||
&conf,
|
||||
|
@ -79,7 +79,7 @@ public:
|
|||
MBEDTLS_SSL_TRANSPORT_STREAM,
|
||||
MBEDTLS_SSL_PRESET_DEFAULT);
|
||||
if (err)
|
||||
throw tls::error_string(err);
|
||||
throw std::logic_error(tls::error_string(err));
|
||||
|
||||
if (cert != nullptr)
|
||||
cert->use(&ssl, &conf);
|
||||
|
@ -92,11 +92,11 @@ public:
|
|||
|
||||
err = mbedtls_ssl_setup(&ssl, &conf);
|
||||
if (err)
|
||||
throw tls::error_string(err);
|
||||
throw std::logic_error(tls::error_string(err));
|
||||
|
||||
err = mbedtls_ssl_set_hostname(&ssl, sni.c_str());
|
||||
if (err)
|
||||
throw tls::error_string(err);
|
||||
throw std::logic_error(tls::error_string(err));
|
||||
|
||||
mbedtls_ssl_set_bio(
|
||||
&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, nullptr);
|
||||
|
@ -109,7 +109,7 @@ public:
|
|||
if (
|
||||
(err != MBEDTLS_ERR_SSL_WANT_READ) &&
|
||||
(err != MBEDTLS_ERR_SSL_WANT_WRITE))
|
||||
throw tls::error_string(err);
|
||||
throw std::logic_error(tls::error_string(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ public:
|
|||
if (ret > 0)
|
||||
written += ret;
|
||||
else
|
||||
throw tls::error_string(ret);
|
||||
throw std::logic_error(tls::error_string(ret));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,7 +149,7 @@ public:
|
|||
else if (ret == 0)
|
||||
throw std::logic_error("Underlying transport closed");
|
||||
else
|
||||
throw tls::error_string(ret);
|
||||
throw std::logic_error(tls::error_string(ret));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,8 @@ namespace ccf
|
|||
Script script;
|
||||
//! fixed parameter for the script
|
||||
nlohmann::json parameter;
|
||||
//! vote ballot of proposer
|
||||
Script ballot = {"return true"};
|
||||
};
|
||||
|
||||
//! results from propose RPC
|
||||
|
@ -68,8 +70,9 @@ namespace ccf
|
|||
bool completed;
|
||||
};
|
||||
};
|
||||
DECLARE_JSON_TYPE(Proposal::In)
|
||||
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Proposal::In)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(Proposal::In, script, parameter)
|
||||
DECLARE_JSON_OPTIONAL_FIELDS(Proposal::In, ballot)
|
||||
DECLARE_JSON_TYPE(Proposal::Out)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(Proposal::Out, id, completed)
|
||||
|
||||
|
|
|
@ -128,23 +128,15 @@ namespace ccf
|
|||
proposed_calls);
|
||||
|
||||
/* count the votes
|
||||
* if the proposer hasn't explicitly voted and is still active,
|
||||
* an implicit pro vote is assumed.
|
||||
*/
|
||||
bool explicit_proposer_vote = false;
|
||||
uint64_t pro = 0, con = 0;
|
||||
const uint64_t total = proposal->votes.size();
|
||||
for (const auto& vote : proposal->votes)
|
||||
{
|
||||
// can the proposal still succeed? If we haven't seen the proposer's
|
||||
// vote yet, assume it to be pro.
|
||||
if (total - con + (explicit_proposer_vote ? 0 : 1) < quorum)
|
||||
// can the proposal still succeed?
|
||||
if (total - con < quorum)
|
||||
return false;
|
||||
|
||||
// is this an explicit proposer vote?
|
||||
if (vote.first == proposal->proposer)
|
||||
explicit_proposer_vote = true;
|
||||
|
||||
// valid voter
|
||||
if (!check_member_active(tx, vote.first))
|
||||
continue;
|
||||
|
@ -161,9 +153,6 @@ namespace ccf
|
|||
else
|
||||
con++;
|
||||
}
|
||||
if (
|
||||
!explicit_proposer_vote && check_member_active(tx, proposal->proposer))
|
||||
pro++;
|
||||
|
||||
if (pro < quorum)
|
||||
return false;
|
||||
|
@ -284,8 +273,10 @@ namespace ccf
|
|||
const auto in = args.params.get<Proposal::In>();
|
||||
const auto proposal_id = get_next_id(
|
||||
args.tx.get_view(this->network.values), ValueIds::NEXT_PROPOSAL_ID);
|
||||
const OpenProposal proposal(in.script, in.parameter, args.caller_id);
|
||||
args.tx.get_view(this->network.proposals)->put(proposal_id, proposal);
|
||||
OpenProposal proposal(in.script, in.parameter, args.caller_id);
|
||||
auto proposals = args.tx.get_view(this->network.proposals);
|
||||
proposal.votes[args.caller_id] = in.ballot;
|
||||
proposals->put(proposal_id, proposal);
|
||||
const bool completed = complete_proposal(args.tx, proposal_id);
|
||||
return jsonrpc::success<Proposal::Out>({proposal_id, completed});
|
||||
};
|
||||
|
|
|
@ -283,6 +283,108 @@ TEST_CASE("Member query/read")
|
|||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Proposer ballot")
|
||||
{
|
||||
NetworkTables network;
|
||||
GenesisGenerator gen(network);
|
||||
gen.init_values();
|
||||
|
||||
const auto proposer_cert = get_cert_data(0, kp);
|
||||
const auto proposer_id = gen.add_member(proposer_cert, MemberStatus::ACTIVE);
|
||||
const auto voter_cert = get_cert_data(1, kp);
|
||||
const auto voter_id = gen.add_member(voter_cert, MemberStatus::ACTIVE);
|
||||
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(lua::Interpreter().invoke<json>(gov_script_file));
|
||||
gen.finalize();
|
||||
|
||||
StubNodeState node;
|
||||
MemberCallRpcFrontend frontend(network, node);
|
||||
|
||||
size_t proposal_id;
|
||||
|
||||
const ccf::Script vote_for("return true");
|
||||
const ccf::Script vote_against("return false");
|
||||
{
|
||||
INFO("Propose, initially voting against");
|
||||
|
||||
const auto proposed_member = get_cert_data(2, kp);
|
||||
|
||||
Script proposal(R"xxx(
|
||||
tables, member_cert = ...
|
||||
return Calls:call("new_member", member_cert)
|
||||
)xxx");
|
||||
const auto proposej = create_json_req(
|
||||
Proposal::In{proposal, proposed_member, vote_against}, "propose");
|
||||
enclave::RPCContext rpc_ctx(proposer_id, proposer_cert);
|
||||
|
||||
Store::Tx tx;
|
||||
ccf::SignedReq sr(proposej);
|
||||
Response<Proposal::Out> r =
|
||||
frontend.process_json(rpc_ctx, tx, proposer_id, proposej, sr).value();
|
||||
|
||||
// the proposal should be accepted, but not succeed immediately
|
||||
CHECK(r.result.completed == false);
|
||||
|
||||
proposal_id = r.result.id;
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Second member votes for proposal");
|
||||
|
||||
const auto votej =
|
||||
create_json_req_signed(Vote{proposal_id, vote_for}, "vote", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(voter_id, voter_cert);
|
||||
ccf::SignedReq sr(votej);
|
||||
Response<bool> r =
|
||||
frontend.process_json(rpc_ctx, tx, voter_id, votej["req"], sr).value();
|
||||
|
||||
// The vote should not yet succeed
|
||||
CHECK(r.result == false);
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Read current votes");
|
||||
|
||||
const auto readj = create_json_req_signed(
|
||||
read_params(proposal_id, Tables::PROPOSALS), "read", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(proposer_id, proposer_cert);
|
||||
const Response<OpenProposal> proposal =
|
||||
get_proposal(rpc_ctx, frontend, proposal_id, proposer_id);
|
||||
|
||||
const auto& votes = proposal.result.votes;
|
||||
CHECK(votes.size() == 2);
|
||||
|
||||
const auto proposer_vote = votes.find(proposer_id);
|
||||
CHECK(proposer_vote != votes.end());
|
||||
CHECK(proposer_vote->second == vote_against);
|
||||
|
||||
const auto voter_vote = votes.find(voter_id);
|
||||
CHECK(voter_vote != votes.end());
|
||||
CHECK(voter_vote->second == vote_for);
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Proposer votes for");
|
||||
|
||||
const auto votej =
|
||||
create_json_req_signed(Vote{proposal_id, vote_for}, "vote", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(proposer_id, proposer_cert);
|
||||
ccf::SignedReq sr(votej);
|
||||
Response<bool> r =
|
||||
frontend.process_json(rpc_ctx, tx, proposer_id, votej["req"], sr).value();
|
||||
|
||||
// The vote should now succeed
|
||||
CHECK(r.result == true);
|
||||
}
|
||||
}
|
||||
|
||||
struct NewMember
|
||||
{
|
||||
MemberId id;
|
||||
|
|
|
@ -35,6 +35,11 @@ namespace ccf
|
|||
return bytecode == other.bytecode && text == other.text;
|
||||
}
|
||||
|
||||
bool operator!=(const Script& other) const
|
||||
{
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
MSGPACK_DEFINE(bytecode, text);
|
||||
};
|
||||
|
||||
|
|
|
@ -350,7 +350,7 @@ class Network:
|
|||
|
||||
def propose_add_member(self, member_id, remote_node, new_member_cert):
|
||||
return self.propose(
|
||||
member_id, remote_node, "add_member", f"--member_cert={new_member_cert}"
|
||||
member_id, remote_node, "add_member", f"--member-cert={new_member_cert}"
|
||||
)
|
||||
|
||||
def stop_all_nodes(self):
|
||||
|
@ -693,7 +693,6 @@ class Node:
|
|||
)
|
||||
|
||||
def management_client(self, **kwargs):
|
||||
LOG.error(f"{self.local_node_id}.pem")
|
||||
return infra.jsonrpc.client(
|
||||
self.host,
|
||||
self.rpc_port,
|
||||
|
|
|
@ -83,7 +83,7 @@ def run(args):
|
|||
result = network.vote(1, primary, proposal_id, True)
|
||||
assert result[0] and not result[1]
|
||||
|
||||
# result is true with just 2 votes because proposer implicit pro vote is assumed
|
||||
# result is true with 3 votes (proposer, member 1, and member 2)
|
||||
result = network.vote(2, primary, proposal_id, True)
|
||||
assert result[0] and result[1]
|
||||
|
||||
|
@ -94,7 +94,9 @@ def run(args):
|
|||
assert not result[1]["completed"]
|
||||
assert proposal_id == 2
|
||||
|
||||
j_result = network.member_client_rpc_as_json(4, primary, "removal", "--id=2")
|
||||
j_result = network.member_client_rpc_as_json(
|
||||
4, primary, "removal", "--proposal-id=2"
|
||||
)
|
||||
assert j_result["result"]
|
||||
|
||||
# member 4 proposes to inactivate member 1 and other members vote yes
|
||||
|
@ -122,7 +124,7 @@ def run(args):
|
|||
assert result[1]["code"] == infra.jsonrpc.ErrorCode.INSUFFICIENT_RIGHTS.value
|
||||
|
||||
# member 4 proposes to add member 3 as user
|
||||
result = network.propose(4, primary, "add_user", "--user_cert=member3_cert.pem")
|
||||
result = network.propose(4, primary, "add_user", "--user-cert=member3_cert.pem")
|
||||
assert not result[1]["completed"]
|
||||
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче