зеркало из https://github.com/microsoft/CCF.git
Dynamic vote (#370)
This commit is contained in:
Родитель
0b62318f90
Коммит
f71a103f41
|
@ -3,32 +3,27 @@ Governance
|
|||
|
||||
A trusted set of members is in charge of governing a given CCF network. For transparency and auditability, all governance operations are recorded in plaintext in the ledger and members are required to sign their requests.
|
||||
|
||||
One member (proposer) can submit a new proposal. Once they have done this, other members can vote for the proposal using its unique proposal ID. Proposals are executed once a :term:`quorum` of members have accepted it.
|
||||
Any member (proposer) can submit a new proposal. Other members can then vote on this proposal using its unique proposal ID. Votes for the proposal are evaluated by the constitution's `pass` function.
|
||||
If the `pass` function returns true, the vote is passed, and its consequences are applied to the KV in a transaction.
|
||||
|
||||
The quorum is defined as a Lua script in the genesis transaction (see for example `the default quorum script`_).
|
||||
This `simple constitution`_ implements a "one-member, one-vote" constitution, with a majority rule. Votes on so-called sensitive tables, such as the one containing the constitution itself, require unanimity.
|
||||
|
||||
.. note:: A proposal can be a Lua script defined by the proposer member or a static function defined by CCF (e.g. ``new_member``).
|
||||
|
||||
.. _`the default quorum script`: https://github.com/microsoft/CCF/blob/master/src/runtime_config/gov.lua
|
||||
|
||||
|
||||
Common governance operations
|
||||
----------------------------
|
||||
|
||||
Common member governance operations include:
|
||||
Operations
|
||||
----------
|
||||
|
||||
- :ref:`Adding users`
|
||||
- :ref:`Opening a network`
|
||||
- Adding members
|
||||
- :ref:`Updating trusted enclave code versions`
|
||||
- :ref:`Opening a network`
|
||||
- :ref:`Updating code`
|
||||
- Accepting a new node to the network
|
||||
- Retiring an existing node
|
||||
- Accepting :ref:`catastrophic recovery`
|
||||
- Accept :ref:`Catastrophic Recovery`
|
||||
|
||||
Submitting a new proposal
|
||||
-------------------------
|
||||
`````````````````````````
|
||||
|
||||
Assuming that 3 members (``member1``, ``member2`` and ``member3``) are already registered in the CCF network and that the quorum is defined as a strict majority of members, a member can submit a new proposal using the ``memberclient`` command-line utility (see :ref:`Member methods` for equivalent JSON-RPC API).
|
||||
Assuming that 3 members (``member1``, ``member2`` and ``member3``) are already registered in the CCF network and that the sample constitution is used, a member can submit a new proposal using the ``memberclient`` command-line utility (see :ref:`Member methods` for equivalent JSON-RPC API).
|
||||
|
||||
For example, ``member1`` may submit a proposal to add a new member (``member4``) to the consortium:
|
||||
|
||||
|
@ -48,11 +43,11 @@ In this case, a new proposal with id ``1`` has successfully been created and the
|
|||
{"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
|
||||
// As a majority of members have accepted the proposal, member4 is added to the consortium
|
||||
$ memberclient --rpc-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``.
|
||||
As soon as ``member3`` accepts the proposal, a majority (2 out of 3) of members has been reached and the proposal completes, successfully adding ``member4``.
|
||||
|
||||
.. note:: Once a new member has been accepted to the consortium, the new member must acknowledge that it is active:
|
||||
|
||||
|
@ -63,7 +58,7 @@ As soon as ``member3`` accepts the proposal, a quorum (2 out of 3) of members ha
|
|||
|
||||
|
||||
Displaying proposals
|
||||
--------------------
|
||||
````````````````````
|
||||
|
||||
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:
|
||||
|
||||
|
@ -96,8 +91,8 @@ The details of pending proposals, including the proposer member ID, proposal scr
|
|||
|
||||
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. Two votes have been cast: ``member1`` (proposer) has voted for the proposal, while ``member2`` (``id`` is 1) has voted against it.
|
||||
|
||||
Withdrawing proposals
|
||||
---------------------
|
||||
Withdrawing a proposal
|
||||
``````````````````````
|
||||
|
||||
At any stage during the voting process and before the proposal is completed, the proposing member may decide to withdraw a pending proposal:
|
||||
|
||||
|
@ -108,8 +103,8 @@ At any stage during the voting process and before the proposal is completed, the
|
|||
|
||||
This means future votes will be ignored, and the proposal will never be accepted. However it will remain visible as a proposal so members can easily audit historic proposals.
|
||||
|
||||
Updating trusted enclave code versions
|
||||
--------------------------------------
|
||||
Updating code
|
||||
`````````````
|
||||
|
||||
For new nodes to be able to join the network, the version of the code they run (as specified by the ``--enclave-file``) should be first trusted by the consortium of members.
|
||||
|
||||
|
@ -123,4 +118,54 @@ Once the proposal has been accepted, nodes running the new code are authorised j
|
|||
|
||||
.. note:: It is important to keep the code compatible with the previous version, since there will be a point in time in which the new code is running on at least one node, while the other version is running on a different node.
|
||||
|
||||
.. note:: The safest way to restart or replace nodes is by stopping a single node running the old version and starting a node running the new version as a sequence of operations, in order to avoid a situation in which most nodes have been stopped, and new nodes will not be able to join since it would be impossible to reach a majority of nodes agreeing to accept new nodes (this restriction is imposed by the consensus algorithm).
|
||||
.. note:: The safest way to restart or replace nodes is by stopping a single node running the old version and starting a node running the new version as a sequence of operations, in order to avoid a situation in which most nodes have been stopped, and new nodes will not be able to join since it would be impossible to reach a majority of nodes agreeing to accept new nodes (this restriction is imposed by the consensus algorithm).
|
||||
|
||||
|
||||
Models
|
||||
------
|
||||
|
||||
The operators of a CCF network do not necessarily overlap with the members of that network. Although the scriptability of the governance model effectively allows a large number of possible arrangements, the following two schemes seem most likely:
|
||||
|
||||
Non-member operators
|
||||
````````````````````
|
||||
|
||||
It is possible for a set of operators to host a CCF network without being members. These operators could:
|
||||
|
||||
- Start the network
|
||||
- Hand it over to the members for them to Open (see :ref:`Opening a network`)
|
||||
|
||||
In case of catastrophic failure, operators could also:
|
||||
|
||||
- Start a network in recovery mode from the ledger
|
||||
- Hand it over to the members for them to Open (see :ref:`Catastrophic Recovery`)
|
||||
|
||||
Finally, operators could:
|
||||
- Propose new nodes (TR, Section IV D)
|
||||
- Notify the members, who would have to review and vote on the proposal
|
||||
|
||||
Operators would not be able to add or remove members or users to the service. They would not be able to update the code of the service (and therefore apply security patches). Because they could propose new nodes, but would require member votes before nodes are allows to join, the operators' ability to mitigate node failures may be limited and delayed.
|
||||
|
||||
This model keeps operators out of the trust boundary for the service.
|
||||
|
||||
Operating members
|
||||
`````````````````
|
||||
|
||||
If network operators are made members, they could have the ability to:
|
||||
|
||||
- Update code (in particular, apply security patches)
|
||||
- Add and remove nodes to and from the network
|
||||
|
||||
Essentially, operators gain the ability to fix security issues and mitigate service degradation for the network. In this situation however, the operator is inside the trust boundary.
|
||||
|
||||
The constitution can limit or remove the operating members' ability to:
|
||||
|
||||
- Add and remove members and users
|
||||
- Complete a recovery
|
||||
|
||||
.. note:: These limits are weakened by the operators' ability to update the code. A code update could contain changes that allow the operator to bypass constitution restrictions. Work is in progress to propose a service that would effectively mitigate this problem. In the absence of code updates however, other members of the service could trust that the operating members have not added or removed members and users, and have not executed a recovery.
|
||||
|
||||
This `operating member constitution`_ shows how some members can be made operators.
|
||||
|
||||
.. _`simple constitution`_: https://github.com/microsoft/CCF/blob/master/src/runtime_config/gov.lua
|
||||
|
||||
.. _`operating member constitution`_: https://github.com/microsoft/CCF/blob/master/src/runtime_config/operator_gov.lua
|
|
@ -113,7 +113,7 @@ namespace ccf
|
|||
auto proposals = tx.get_view(this->network.proposals);
|
||||
auto proposal = proposals->get(id);
|
||||
if (!proposal)
|
||||
throw std::logic_error(fmt::format("No proposal {}", id));
|
||||
throw std::logic_error(fmt::format("No such proposal: {}", id));
|
||||
|
||||
if (proposal->state != ProposalState::OPEN)
|
||||
throw std::logic_error(fmt::format(
|
||||
|
@ -130,44 +130,35 @@ namespace ccf
|
|||
// vvv arguments to script vvv
|
||||
proposal->parameter);
|
||||
|
||||
// pass the effects to the quorum script
|
||||
const auto quorum = tsr.run<int>(
|
||||
tx,
|
||||
{get_script(tx, GovScriptIds::QUORUM),
|
||||
{}, // can't write
|
||||
WlIds::MEMBER_CAN_READ,
|
||||
{}},
|
||||
// vvv arguments to script vvv
|
||||
proposed_calls);
|
||||
|
||||
/* count the votes
|
||||
*/
|
||||
uint64_t pro = 0, con = 0;
|
||||
const uint64_t total = proposal->votes.size();
|
||||
nlohmann::json votes;
|
||||
// Collect all member votes
|
||||
for (const auto& vote : proposal->votes)
|
||||
{
|
||||
// can the proposal still succeed?
|
||||
if (total - con < quorum)
|
||||
return false;
|
||||
|
||||
// valid voter
|
||||
if (!check_member_active(tx, vote.first))
|
||||
continue;
|
||||
|
||||
// does the voter agree?
|
||||
if (tsr.run<bool>(
|
||||
tx,
|
||||
{vote.second,
|
||||
{}, // can't write
|
||||
WlIds::MEMBER_CAN_READ,
|
||||
{}},
|
||||
proposed_calls))
|
||||
pro++;
|
||||
else
|
||||
con++;
|
||||
votes[std::to_string(vote.first)] = tsr.run<bool>(
|
||||
tx,
|
||||
{vote.second,
|
||||
{}, // can't write
|
||||
WlIds::MEMBER_CAN_READ,
|
||||
{}},
|
||||
proposed_calls);
|
||||
}
|
||||
|
||||
if (pro < quorum)
|
||||
const auto pass = tsr.run<bool>(
|
||||
tx,
|
||||
{get_script(tx, GovScriptIds::PASS),
|
||||
{}, // can't write
|
||||
WlIds::MEMBER_CAN_READ,
|
||||
{}},
|
||||
// vvv arguments to script vvv
|
||||
proposed_calls,
|
||||
votes);
|
||||
|
||||
if (!pass)
|
||||
return false;
|
||||
|
||||
// execute proposed calls
|
||||
|
|
|
@ -44,6 +44,8 @@ string get_script_path(string name)
|
|||
return ss.str();
|
||||
}
|
||||
const auto gov_script_file = files::slurp_string(get_script_path("gov.lua"));
|
||||
const auto operator_gov_script_file =
|
||||
files::slurp_string(get_script_path("operator_gov.lua"));
|
||||
|
||||
template <typename T>
|
||||
auto mpack(T&& a)
|
||||
|
@ -590,7 +592,8 @@ TEST_CASE("Accept node")
|
|||
// node to be tested
|
||||
// new node certificate
|
||||
auto new_ca = new_kp->self_sign("CN=new node");
|
||||
NodeInfo ni = {"", "", "", "", new_ca, {}};
|
||||
NodeInfo ni;
|
||||
ni.cert = new_ca;
|
||||
gen.add_node(ni);
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(lua::Interpreter().invoke<json>(gov_script_file));
|
||||
|
@ -1008,6 +1011,350 @@ TEST_CASE("Add user via proposed call")
|
|||
CHECK(*uid1 == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Passing members ballot with operator")
|
||||
{
|
||||
// Members pass a ballot with a constitution that includes an operator
|
||||
// Operator votes, but is _not_ taken into consideration
|
||||
NetworkTables network;
|
||||
GenesisGenerator gen(network);
|
||||
gen.init_values();
|
||||
|
||||
// Operating member, as set in operator_gov.lua
|
||||
const auto operator_cert = get_cert_data(0, kp);
|
||||
const auto operator_id = gen.add_member(operator_cert, MemberStatus::ACTIVE);
|
||||
|
||||
// Non-operating members
|
||||
std::map<size_t, ccf::Cert> members;
|
||||
for (size_t i = 1; i < 4; i++)
|
||||
{
|
||||
auto cert = get_cert_data(i, kp);
|
||||
members[gen.add_member(cert, MemberStatus::ACTIVE)] = cert;
|
||||
}
|
||||
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(
|
||||
lua::Interpreter().invoke<json>(operator_gov_script_file));
|
||||
gen.finalize();
|
||||
|
||||
StubNodeState node;
|
||||
MemberCallRpcFrontend frontend(network, node);
|
||||
|
||||
size_t proposal_id;
|
||||
size_t proposer_id = 1;
|
||||
size_t voter_id = 2;
|
||||
|
||||
const ccf::Script vote_for("return true");
|
||||
const ccf::Script vote_against("return false");
|
||||
{
|
||||
INFO("Propose and vote for");
|
||||
|
||||
const auto proposed_member = get_cert_data(4, kp);
|
||||
|
||||
Script proposal(R"xxx(
|
||||
tables, member_cert = ...
|
||||
return Calls:call("new_member", member_cert)
|
||||
)xxx");
|
||||
const auto proposej = create_json_req(
|
||||
Propose::In{proposal, proposed_member, vote_for}, "propose");
|
||||
enclave::RPCContext rpc_ctx(proposer_id, members[proposer_id]);
|
||||
|
||||
Store::Tx tx;
|
||||
ccf::SignedReq sr(proposej);
|
||||
Response<Propose::Out> r =
|
||||
frontend.process_json(rpc_ctx, tx, proposer_id, proposej, sr).value();
|
||||
|
||||
CHECK(r.result.completed == false);
|
||||
|
||||
proposal_id = r.result.id;
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Operator votes, but without effect");
|
||||
|
||||
const auto votej =
|
||||
create_json_req_signed(Vote{proposal_id, vote_for}, "vote", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(operator_id, operator_cert);
|
||||
ccf::SignedReq sr(votej);
|
||||
Response<bool> r =
|
||||
frontend.process_json(rpc_ctx, tx, operator_id, votej["req"], sr).value();
|
||||
|
||||
CHECK(r.result == false);
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Second member votes for proposal, which passes");
|
||||
|
||||
const auto votej =
|
||||
create_json_req_signed(Vote{proposal_id, vote_for}, "vote", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(voter_id, members[voter_id]);
|
||||
ccf::SignedReq sr(votej);
|
||||
Response<bool> r =
|
||||
frontend.process_json(rpc_ctx, tx, voter_id, votej["req"], sr).value();
|
||||
|
||||
CHECK(r.result == true);
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Validate vote tally");
|
||||
|
||||
const auto readj = create_json_req_signed(
|
||||
read_params(proposal_id, Tables::PROPOSALS), "read", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(proposer_id, members[proposer_id]);
|
||||
const Response<Proposal> proposal =
|
||||
get_proposal(rpc_ctx, frontend, proposal_id, proposer_id);
|
||||
|
||||
const auto& votes = proposal.result.votes;
|
||||
CHECK(votes.size() == 3);
|
||||
|
||||
const auto operator_vote = votes.find(operator_id);
|
||||
CHECK(operator_vote != votes.end());
|
||||
CHECK(operator_vote->second == vote_for);
|
||||
|
||||
const auto proposer_vote = votes.find(proposer_id);
|
||||
CHECK(proposer_vote != votes.end());
|
||||
CHECK(proposer_vote->second == vote_for);
|
||||
|
||||
const auto voter_vote = votes.find(voter_id);
|
||||
CHECK(voter_vote != votes.end());
|
||||
CHECK(voter_vote->second == vote_for);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Passing operator vote")
|
||||
{
|
||||
// Operator issues a proposal that only requires its own vote
|
||||
// and gets it through without member votes
|
||||
NetworkTables network;
|
||||
GenesisGenerator gen(network);
|
||||
gen.init_values();
|
||||
auto new_kp = tls::make_key_pair();
|
||||
auto new_ca = new_kp->self_sign("CN=new node");
|
||||
NodeInfo ni;
|
||||
ni.cert = new_ca;
|
||||
gen.add_node(ni);
|
||||
|
||||
// Operating member, as set in operator_gov.lua
|
||||
const auto operator_cert = get_cert_data(0, kp);
|
||||
const auto operator_id = gen.add_member(operator_cert, MemberStatus::ACTIVE);
|
||||
|
||||
// Non-operating members
|
||||
std::map<size_t, ccf::Cert> members;
|
||||
for (size_t i = 1; i < 4; i++)
|
||||
{
|
||||
auto cert = get_cert_data(i, kp);
|
||||
members[gen.add_member(cert, MemberStatus::ACTIVE)] = cert;
|
||||
}
|
||||
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(
|
||||
lua::Interpreter().invoke<json>(operator_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");
|
||||
|
||||
auto node_id = 0;
|
||||
{
|
||||
INFO("Check node exists with status pending");
|
||||
Store::Tx tx;
|
||||
auto read_values_j =
|
||||
create_json_req(read_params<int>(node_id, Tables::NODES), "read");
|
||||
ccf::SignedReq sr(read_values_j);
|
||||
|
||||
enclave::RPCContext rpc_ctx(operator_id, operator_cert);
|
||||
Response<NodeInfo> r =
|
||||
frontend.process_json(rpc_ctx, tx, operator_id, read_values_j, sr)
|
||||
.value();
|
||||
CHECK(r.result.status == NodeStatus::PENDING);
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Operator proposes and votes for node");
|
||||
Script proposal(R"xxx(
|
||||
local tables, node_id = ...
|
||||
return Calls:call("accept_node", node_id)
|
||||
)xxx");
|
||||
|
||||
json proposej =
|
||||
create_json_req(Propose::In{proposal, node_id, vote_for}, "propose");
|
||||
ccf::SignedReq sr(proposej);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(operator_id, operator_cert);
|
||||
Response<Propose::Out> r =
|
||||
frontend.process_json(rpc_ctx, tx, operator_id, proposej, sr).value();
|
||||
|
||||
CHECK(r.result.completed);
|
||||
proposal_id = r.result.id;
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Validate vote tally");
|
||||
|
||||
const auto readj = create_json_req_signed(
|
||||
read_params(proposal_id, Tables::PROPOSALS), "read", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(operator_id, operator_cert);
|
||||
const Response<Proposal> proposal =
|
||||
get_proposal(rpc_ctx, frontend, proposal_id, 1);
|
||||
|
||||
const auto& votes = proposal.result.votes;
|
||||
CHECK(votes.size() == 1);
|
||||
|
||||
const auto proposer_vote = votes.find(operator_id);
|
||||
CHECK(proposer_vote != votes.end());
|
||||
CHECK(proposer_vote->second == vote_for);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Members passing an operator vote")
|
||||
{
|
||||
// Operator proposes a vote, but does not vote for it
|
||||
// A majority of members pass the vote
|
||||
NetworkTables network;
|
||||
GenesisGenerator gen(network);
|
||||
gen.init_values();
|
||||
auto new_kp = tls::make_key_pair();
|
||||
auto new_ca = new_kp->self_sign("CN=new node");
|
||||
NodeInfo ni;
|
||||
ni.cert = new_ca;
|
||||
gen.add_node(ni);
|
||||
|
||||
// Operating member, as set in operator_gov.lua
|
||||
const auto operator_cert = get_cert_data(0, kp);
|
||||
const auto operator_id = gen.add_member(operator_cert, MemberStatus::ACTIVE);
|
||||
|
||||
// Non-operating members
|
||||
std::map<size_t, ccf::Cert> members;
|
||||
for (size_t i = 1; i < 4; i++)
|
||||
{
|
||||
auto cert = get_cert_data(i, kp);
|
||||
members[gen.add_member(cert, MemberStatus::ACTIVE)] = cert;
|
||||
}
|
||||
|
||||
set_whitelists(gen);
|
||||
gen.set_gov_scripts(
|
||||
lua::Interpreter().invoke<json>(operator_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");
|
||||
|
||||
auto node_id = 0;
|
||||
{
|
||||
INFO("Check node exists with status pending");
|
||||
Store::Tx tx;
|
||||
auto read_values_j =
|
||||
create_json_req(read_params<int>(node_id, Tables::NODES), "read");
|
||||
ccf::SignedReq sr(read_values_j);
|
||||
|
||||
enclave::RPCContext rpc_ctx(operator_id, operator_cert);
|
||||
Response<NodeInfo> r =
|
||||
frontend.process_json(rpc_ctx, tx, operator_id, read_values_j, sr)
|
||||
.value();
|
||||
CHECK(r.result.status == NodeStatus::PENDING);
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Operator proposes and votes against adding node");
|
||||
Script proposal(R"xxx(
|
||||
local tables, node_id = ...
|
||||
return Calls:call("accept_node", node_id)
|
||||
)xxx");
|
||||
|
||||
json proposej =
|
||||
create_json_req(Propose::In{proposal, node_id, vote_against}, "propose");
|
||||
ccf::SignedReq sr(proposej);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(operator_id, operator_cert);
|
||||
Response<Propose::Out> r =
|
||||
frontend.process_json(rpc_ctx, tx, operator_id, proposej, sr).value();
|
||||
|
||||
CHECK(!r.result.completed);
|
||||
proposal_id = r.result.id;
|
||||
}
|
||||
|
||||
size_t first_voter_id = 1;
|
||||
size_t second_voter_id = 2;
|
||||
|
||||
{
|
||||
INFO("First 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(first_voter_id, members[first_voter_id]);
|
||||
ccf::SignedReq sr(votej);
|
||||
Response<bool> r =
|
||||
frontend.process_json(rpc_ctx, tx, first_voter_id, votej["req"], sr)
|
||||
.value();
|
||||
|
||||
CHECK(r.result == false);
|
||||
}
|
||||
|
||||
{
|
||||
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(second_voter_id, members[second_voter_id]);
|
||||
ccf::SignedReq sr(votej);
|
||||
Response<bool> r =
|
||||
frontend.process_json(rpc_ctx, tx, second_voter_id, votej["req"], sr)
|
||||
.value();
|
||||
|
||||
CHECK(r.result == true);
|
||||
}
|
||||
|
||||
{
|
||||
INFO("Validate vote tally");
|
||||
|
||||
const auto readj = create_json_req_signed(
|
||||
read_params(proposal_id, Tables::PROPOSALS), "read", kp);
|
||||
|
||||
Store::Tx tx;
|
||||
enclave::RPCContext rpc_ctx(operator_id, operator_cert);
|
||||
const Response<Proposal> proposal =
|
||||
get_proposal(rpc_ctx, frontend, proposal_id, 1);
|
||||
|
||||
const auto& votes = proposal.result.votes;
|
||||
CHECK(votes.size() == 3);
|
||||
|
||||
const auto proposer_vote = votes.find(operator_id);
|
||||
CHECK(proposer_vote != votes.end());
|
||||
CHECK(proposer_vote->second == vote_against);
|
||||
|
||||
const auto first_vote = votes.find(first_voter_id);
|
||||
CHECK(first_vote != votes.end());
|
||||
CHECK(first_vote->second == vote_for);
|
||||
|
||||
const auto second_vote = votes.find(second_voter_id);
|
||||
CHECK(second_vote != votes.end());
|
||||
CHECK(second_vote->second == vote_for);
|
||||
}
|
||||
}
|
||||
|
||||
// We need an explicit main to initialize kremlib and EverCrypt
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
|
|
|
@ -11,12 +11,12 @@ namespace ccf
|
|||
|
||||
struct GovScriptIds
|
||||
{
|
||||
//! script that decides if the required quorum for a proposal
|
||||
static auto constexpr QUORUM = "quorum";
|
||||
//! script that applies an accepted "raw puts" proposal
|
||||
static auto constexpr RAW_PUTS = "raw_puts";
|
||||
//! script that sets the environment for a proposal script
|
||||
static auto constexpr ENV_PROPOSAL = "environment_proposal";
|
||||
//! script that decides if a proposal has been accepted
|
||||
static auto constexpr PASS = "pass";
|
||||
};
|
||||
|
||||
struct UserScriptIds
|
||||
|
|
|
@ -2,32 +2,48 @@
|
|||
-- Licensed under the Apache 2.0 License.
|
||||
|
||||
-- This file defines the default initial contents (ie, Lua scripts) of the gov_scipts table.
|
||||
return {
|
||||
quorum = [[
|
||||
tables, calls = ...
|
||||
return {
|
||||
pass = [[
|
||||
tables, calls, votes = ...
|
||||
|
||||
member_votes = 0
|
||||
|
||||
for member, vote in pairs(votes) do
|
||||
if vote then
|
||||
member_votes = member_votes + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- count active members
|
||||
n_active = 0
|
||||
members_active = 0
|
||||
STATE_ACTIVE = 1
|
||||
tables["ccf.members"]:foreach(function(k, v)
|
||||
if v["status"] == STATE_ACTIVE then
|
||||
n_active = n_active + 1
|
||||
|
||||
tables["ccf.members"]:foreach(function(member, details)
|
||||
if details["status"] == STATE_ACTIVE then
|
||||
members_active = members_active + 1
|
||||
end
|
||||
end)
|
||||
|
||||
-- check for raw_puts to sensitive tables
|
||||
SENSITIVE_TABLES = {"ccf.whitelists", "ccf.gov_scripts"}
|
||||
for _,call in pairs(calls) do
|
||||
for _, call in pairs(calls) do
|
||||
if call.func == "raw_puts" then
|
||||
for _,sensitive_table in pairs(SENSITIVE_TABLES) do
|
||||
for _, sensitive_table in pairs(SENSITIVE_TABLES) do
|
||||
if call.args[sensitive_table] then
|
||||
-- require unanimity
|
||||
return n_active
|
||||
return member_votes == members_active
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return math.floor(n_active / 2 + 1)]],
|
||||
|
||||
-- a majority of members can pass votes
|
||||
if member_votes > math.floor(members_active / 2) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false]],
|
||||
|
||||
environment_proposal = [[
|
||||
__Puts = {}
|
||||
function __Puts:new(o)
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
-- Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
-- Licensed under the Apache 2.0 License.
|
||||
|
||||
-- This file defines the default initial contents (ie, Lua scripts) of the gov_scipts table.
|
||||
return {
|
||||
pass = [[
|
||||
tables, calls, votes = ...
|
||||
|
||||
-- defines which of the members are operators
|
||||
function is_operator(member)
|
||||
return member == "0"
|
||||
end
|
||||
|
||||
-- defines calls that can be passed with sole operator input
|
||||
operator_calls = {
|
||||
accept_node=true,
|
||||
retire_node=true,
|
||||
new_code=true
|
||||
}
|
||||
|
||||
operator_votes = 0
|
||||
member_votes = 0
|
||||
|
||||
for member, vote in pairs(votes) do
|
||||
if vote then
|
||||
if is_operator(member) then
|
||||
operator_votes = operator_votes + 1
|
||||
else
|
||||
member_votes = member_votes + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- count active members, excluding operators
|
||||
members_active = 0
|
||||
STATE_ACTIVE = 1
|
||||
|
||||
tables["ccf.members"]:foreach(function(member, details)
|
||||
if details["status"] == STATE_ACTIVE and not is_operator(tostring(member)) then
|
||||
members_active = members_active + 1
|
||||
end
|
||||
end)
|
||||
|
||||
-- check for raw_puts to sensitive tables
|
||||
SENSITIVE_TABLES = {"ccf.whitelists", "ccf.gov_scripts"}
|
||||
for _, call in pairs(calls) do
|
||||
if call.func == "raw_puts" then
|
||||
for _, sensitive_table in pairs(SENSITIVE_TABLES) do
|
||||
if call.args[sensitive_table] then
|
||||
-- require unanimity of non-operating members
|
||||
return member_votes == members_active
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- a vote is an operator vote if it's only making operator calls
|
||||
operator_vote = true
|
||||
for _, call in pairs(calls) do
|
||||
if not operator_calls[call.func] then
|
||||
operator_vote = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- a majority of members can always pass votes
|
||||
if member_votes > math.floor(members_active / 2) then
|
||||
return true
|
||||
end
|
||||
|
||||
-- a single operator can pass an operator vote
|
||||
if operator_vote then
|
||||
return operator_votes > 0
|
||||
end
|
||||
|
||||
return false]],
|
||||
|
||||
environment_proposal = [[
|
||||
__Puts = {}
|
||||
function __Puts:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
function __Puts:put(t, key, value)
|
||||
self[t] = self[t] or {}
|
||||
table.insert(self[t], {k = key, v = value})
|
||||
return self
|
||||
end
|
||||
-- create a frontend for __Puts that hides function entries
|
||||
Puts = setmetatable({}, {__index = __Puts})
|
||||
|
||||
__Calls = {}
|
||||
function __Calls:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
function __Calls:call(_func, _args)
|
||||
table.insert(self, {func=_func, args=_args})
|
||||
return self
|
||||
end
|
||||
Calls = setmetatable({}, {__index = __Calls})
|
||||
]],
|
||||
|
||||
-- scripts that can be proposed to be called
|
||||
|
||||
raw_puts = [[
|
||||
tables, puts = ...
|
||||
for table_name, entries in pairs(puts) do
|
||||
t = tables[table_name]
|
||||
for _,entry in pairs(entries) do
|
||||
t:put(entry.k, entry.v)
|
||||
end
|
||||
end
|
||||
return true]],
|
||||
|
||||
new_user = [[
|
||||
tables, cert = ...
|
||||
if tables["ccf.user_certs"]:get(cert) then return end
|
||||
NEXT_USER_ID = 1
|
||||
user_id = tables["ccf.values"]:get(NEXT_USER_ID)
|
||||
tables["ccf.values"]:put(NEXT_USER_ID, user_id + 1)
|
||||
tables["ccf.user_certs"]:put(cert, user_id)
|
||||
]]
|
||||
}
|
Загрузка…
Ссылка в новой задаче