Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic member_data to simplify operator governance #1657

Merged
merged 12 commits into from
Sep 28, 2020
16 changes: 14 additions & 2 deletions python/ccf/proposal_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ def cli_proposal(func):


@cli_proposal
def new_member(member_cert_path: str, member_enc_pubk_path: str, **kwargs):
def new_member(
member_cert_path: str, member_enc_pubk_path: str, member_data: Any = None, **kwargs
):
LOG.debug("Generating new_member proposal")

# Read certs
Expand All @@ -193,7 +195,11 @@ def new_member(member_cert_path: str, member_enc_pubk_path: str, **kwargs):

# Proposal object (request body for POST /gov/proposals) containing this member's info as parameter
proposal = {
"parameter": {"cert": member_cert, "keyshare": member_keyshare_encryptor},
"parameter": {
"cert": member_cert,
"keyshare": member_keyshare_encryptor,
"member_data": member_data,
},
"script": {"text": proposal_script_text},
}

Expand Down Expand Up @@ -241,6 +247,12 @@ def retire_member(member_id: int, **kwargs):
return build_proposal("retire_member", member_id, **kwargs)


@cli_proposal
def set_member_data(member_id: int, member_data: Any, **kwargs):
proposal_args = {"member_id": member_id, "member_data": member_data}
return build_proposal("set_member_data", proposal_args, **kwargs)


@cli_proposal
def new_user(user_cert_path: str, user_data: Any = None, **kwargs):
user_info = {"cert": open(user_cert_path).read()}
Expand Down
17 changes: 13 additions & 4 deletions src/apps/lua_generic/test/lua_generic_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,15 +310,24 @@ 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_key_share, MemberStatus::ACCEPTED}},
{"1", {active_members[1], dummy_key_share, MemberStatus::ACCEPTED}},
{"2", {active_members[2], dummy_key_share, MemberStatus::ACCEPTED}}};
{"0",
{active_members[0], dummy_key_share, nullptr, MemberStatus::ACCEPTED}},
{"1",
{active_members[1],
dummy_key_share,
"Some interesting member data",
MemberStatus::ACCEPTED}},
{"2",
{active_members[2],
dummy_key_share,
{{"structured", "data"}, {"nested", "here"}},
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{{}, {}, MemberStatus::ACCEPTED}}});
{{"k", 99}, {"v", MemberInfo{{}, {}, nullptr, 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);
Expand Down
4 changes: 3 additions & 1 deletion src/host/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,9 @@ int main(int argc, char** argv)
for (auto const& m_info : members_info)
{
ccf_config.genesis.members_info.emplace_back(
files::slurp(m_info.cert_file), files::slurp(m_info.keyshare_pub_file));
files::slurp(m_info.cert_file),
files::slurp(m_info.keyshare_pub_file),
nullptr);
}
ccf_config.genesis.gov_script = files::slurp_string(gov_script);
ccf_config.genesis.recovery_threshold = recovery_threshold.value();
Expand Down
15 changes: 13 additions & 2 deletions src/node/genesis_gen.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ namespace ccf
}

auto add_member(
const tls::Pem& member_cert, const tls::Pem& member_keyshare_pub)
const tls::Pem& member_cert,
const tls::Pem& member_keyshare_pub,
const nlohmann::json& member_data = nullptr)
{
auto [m, mc, v, ma, sig] = tx.get_view(
tables.members,
Expand All @@ -110,7 +112,11 @@ namespace ccf
const auto id = get_next_id(v, ValueIds::NEXT_MEMBER_ID);
m->put(
id,
MemberInfo(member_cert, member_keyshare_pub, MemberStatus::ACCEPTED));
MemberInfo(
member_cert,
member_keyshare_pub,
member_data,
MemberStatus::ACCEPTED));
mc->put(member_cert_der, id);

auto s = sig->get(0);
Expand All @@ -125,6 +131,11 @@ namespace ccf
return id;
}

auto add_member(const MemberPubInfo& info)
{
return add_member(info.cert, info.keyshare, info.member_data);
}

void activate_member(MemberId member_id)
{
auto members = tx.get_view(tables.members);
Expand Down
41 changes: 29 additions & 12 deletions src/node/members.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,39 @@ namespace ccf
{
tls::Pem cert;
tls::Pem keyshare;
nlohmann::json member_data = nullptr;

MemberPubInfo() {}

MemberPubInfo(const tls::Pem& cert_, const tls::Pem& keyshare_) :
MemberPubInfo(
const tls::Pem& cert_,
const tls::Pem& keyshare_,
const nlohmann::json& member_data_) :
cert(cert_),
keyshare(keyshare_)
keyshare(keyshare_),
member_data(member_data_)
{}

MemberPubInfo(
std::vector<uint8_t>&& cert_, std::vector<uint8_t>&& keyshare_) :
std::vector<uint8_t>&& cert_,
std::vector<uint8_t>&& keyshare_,
nlohmann::json&& member_data_) :
cert(std::move(cert_)),
keyshare(std::move(keyshare_))
keyshare(std::move(keyshare_)),
member_data(std::move(member_data_))
{}

MSGPACK_DEFINE(cert, keyshare);
bool operator==(const MemberPubInfo& rhs) const
{
return cert == rhs.cert && keyshare == rhs.keyshare &&
member_data == rhs.member_data;
}

MSGPACK_DEFINE(cert, keyshare, member_data);
};
DECLARE_JSON_TYPE(MemberPubInfo)
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(MemberPubInfo)
DECLARE_JSON_REQUIRED_FIELDS(MemberPubInfo, cert, keyshare)
DECLARE_JSON_OPTIONAL_FIELDS(MemberPubInfo, member_data)

struct MemberInfo : public MemberPubInfo
{
Expand All @@ -63,21 +78,23 @@ namespace ccf
MemberInfo() {}

MemberInfo(
const tls::Pem& cert_, const tls::Pem& keyshare_, MemberStatus status_) :
MemberPubInfo(cert_, keyshare_),
const tls::Pem& cert_,
const tls::Pem& keyshare_,
const nlohmann::json& member_data_,
MemberStatus status_) :
MemberPubInfo(cert_, keyshare_, member_data_),
status(status_)
{}

bool operator==(const MemberInfo& rhs) const
{
return cert == rhs.cert && keyshare == rhs.keyshare &&
status == rhs.status;
return MemberPubInfo::operator==(rhs) && status == rhs.status;
}

MSGPACK_DEFINE(MSGPACK_BASE(MemberPubInfo), status);
};
DECLARE_JSON_TYPE(MemberInfo)
DECLARE_JSON_REQUIRED_FIELDS(MemberInfo, cert, keyshare, status)
DECLARE_JSON_TYPE_WITH_BASE(MemberInfo, MemberPubInfo)
DECLARE_JSON_REQUIRED_FIELDS(MemberInfo, status)
using Members = kv::Map<MemberId, MemberInfo>;

/** Records a signed signature containing the last state digest and the next
Expand Down
33 changes: 30 additions & 3 deletions src/node/rpc/member_frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ namespace ccf
MemberTsr(NetworkTables& network) : TxScriptRunner(network) {}
};

struct SetMemberData
{
MemberId member_id;
nlohmann::json member_data = nullptr;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(SetMemberData)
DECLARE_JSON_REQUIRED_FIELDS(SetMemberData, member_id)
DECLARE_JSON_OPTIONAL_FIELDS(SetMemberData, member_data)

struct SetUserData
{
UserId user_id;
Expand Down Expand Up @@ -287,7 +296,7 @@ namespace ccf
[this](ObjectId, kv::Tx& tx, const nlohmann::json& args) {
const auto parsed = args.get<MemberPubInfo>();
GenesisGenerator g(this->network, tx);
g.add_member(parsed.cert, parsed.keyshare);
g.add_member(parsed);

return true;
}},
Expand Down Expand Up @@ -321,6 +330,24 @@ namespace ccf
}
}

return true;
}},
{"set_member_data",
[this](ObjectId proposal_id, kv::Tx& tx, const nlohmann::json& args) {
const auto parsed = args.get<SetMemberData>();
auto members_view = tx.get_view(this->network.members);
auto member_info = members_view->get(parsed.member_id);
if (!member_info.has_value())
{
LOG_FAIL_FMT(
"Proposal {}: {} is not a valid member ID",
proposal_id,
parsed.member_id);
return false;
}

member_info->member_data = parsed.member_data;
members_view->put(parsed.member_id, member_info.value());
return true;
}},
{"new_user",
Expand Down Expand Up @@ -1308,9 +1335,9 @@ namespace ccf
g.init_values();
g.create_service(in.network_cert);

for (auto& [cert, k_encryption_key] : in.members_info)
for (const auto& info : in.members_info)
{
g.add_member(cert, k_encryption_key);
g.add_member(info);
}

g.set_recovery_threshold(in.recovery_threshold);
Expand Down
83 changes: 74 additions & 9 deletions src/node/rpc/test/member_voting_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,15 @@ DOCTEST_TEST_CASE("Add and remove user via proposed calls")
}
}

DOCTEST_TEST_CASE("Passing members ballot with operator")
nlohmann::json operator_member_data()
{
auto md = nlohmann::json::object();
md["is_operator"] = true;
return md;
}

DOCTEST_TEST_CASE(
"Passing members ballot with operator" * doctest::test_suite("operator"))
{
// Members pass a ballot with a constitution that includes an operator
// Operator votes, but is _not_ taken into consideration
Expand All @@ -1294,9 +1302,10 @@ DOCTEST_TEST_CASE("Passing members ballot with operator")
gen.init_values();
gen.create_service({});

// Operating member, as set in operator_gov.lua
// Operating member, as indicated by member data
const auto operator_cert = get_cert(0, kp);
const auto operator_id = gen.add_member(operator_cert, {});
const auto operator_id =
gen.add_member(operator_cert, {}, operator_member_data());
gen.activate_member(operator_id);

// Non-operating members
Expand Down Expand Up @@ -1393,7 +1402,7 @@ DOCTEST_TEST_CASE("Passing members ballot with operator")
}
}

DOCTEST_TEST_CASE("Passing operator vote")
DOCTEST_TEST_CASE("Passing operator vote" * doctest::test_suite("operator"))
{
// Operator issues a proposal that only requires its own vote
// and gets it through without member votes
Expand All @@ -1409,9 +1418,10 @@ DOCTEST_TEST_CASE("Passing operator vote")
ni.cert = new_ca;
gen.add_node(ni);

// Operating member, as set in operator_gov.lua
// Operating member, as indicated by member data
const auto operator_cert = get_cert(0, kp);
const auto operator_id = gen.add_member(operator_cert, {});
const auto operator_id =
gen.add_member(operator_cert, {}, operator_member_data());
gen.activate_member(operator_id);

// Non-operating members
Expand All @@ -1424,6 +1434,9 @@ DOCTEST_TEST_CASE("Passing operator vote")
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));
Expand Down Expand Up @@ -1478,9 +1491,60 @@ DOCTEST_TEST_CASE("Passing operator vote")
DOCTEST_CHECK(proposer_vote != votes.end());
DOCTEST_CHECK(proposer_vote->second == vote_for);
}

auto new_operator_kp = tls::make_key_pair();
eddyashton marked this conversation as resolved.
Show resolved Hide resolved
const auto new_operator_cert = get_cert(42, new_operator_kp);

{
DOCTEST_INFO("Operator adds another operator");
Propose::In proposal;
proposal.script = std::string(R"xxx(
local tables, member_info = ...
return Calls:call("new_member", member_info)
)xxx");

proposal.parameter["cert"] = new_operator_cert;
proposal.parameter["keyshare"] = dummy_key_share;
proposal.parameter["member_data"] = operator_member_data();

const auto propose = create_signed_request(proposal, "proposals", kp);
const auto r = parse_response_body<Propose::Out>(
frontend_process(frontend, propose, operator_cert));

DOCTEST_CHECK(r.state == ProposalState::ACCEPTED);

{
DOCTEST_INFO("New operator acks to become active");
const auto state_digest_req =
create_request(nullptr, "ack/update_state_digest");
const auto ack = parse_response_body<StateDigest>(
frontend_process(frontend, state_digest_req, new_operator_cert));

StateDigest params;
params.state_digest = ack.state_digest;
const auto ack_req =
create_signed_request(params, "ack", new_operator_kp);
const auto resp = frontend_process(frontend, ack_req, new_operator_cert);
}
}

{
DOCTEST_INFO("New operator retires original operator");
Propose::In proposal;
proposal.script = fmt::format(
R"xxx(return Calls:call("retire_member", {}))xxx", operator_id);

const auto propose =
create_signed_request(proposal, "proposals", new_operator_kp);
const auto r = parse_response_body<Propose::Out>(
frontend_process(frontend, propose, new_operator_cert));

DOCTEST_CHECK(r.state == ProposalState::ACCEPTED);
}
}

DOCTEST_TEST_CASE("Members passing an operator vote")
DOCTEST_TEST_CASE(
"Members passing an operator vote" * doctest::test_suite("operator"))
{
// Operator proposes a vote, but does not vote for it
// A majority of members pass the vote
Expand All @@ -1496,9 +1560,10 @@ DOCTEST_TEST_CASE("Members passing an operator vote")
ni.cert = new_ca;
gen.add_node(ni);

// Operating member, as set in operator_gov.lua
// Operating member, as indicated by member data
const auto operator_cert = get_cert(0, kp);
const auto operator_id = gen.add_member(operator_cert, {});
const auto operator_id =
gen.add_member(operator_cert, {}, operator_member_data());
gen.activate_member(operator_id);

// Non-operating members
Expand Down
Loading