This commit is contained in:
Amaury Chamayou 2019-09-17 16:47:34 +01:00 коммит произвёл GitHub
Родитель 0b62318f90
Коммит f71a103f41
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 597 добавлений и 68 удалений

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

@ -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)
]]
}