Don't remove completed proposals (#353)

* Add ProposalState enum

* Rename Proposal to Propose

* Rename OpenProposal to Proposal

* Add withdraw RPC

* Document withdrawal

* Consistency: Rename "removal" to verb "remove"

* Expose withdraw RPC in memberclient

* Improve error when voting for accepted proposal

* Test withdrawal behaviour in member_client_test

* Update schema

* Add formatted info to error messages

* Typo fix

* Distinguish INVALID_CALLER from INVALID_PARAMS

* Add tests of new failure modes

* Upper-case ProposalState for consistency

* Accept proposed removal of proposal removal

* Update docs
This commit is contained in:
Eddy Ashton 2019-09-05 17:57:08 +01:00 коммит произвёл GitHub
Родитель f744d557bb
Коммит 108211d5e0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 252 добавлений и 105 удалений

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

@ -82,12 +82,15 @@ 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 ``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
------------------
Withdrawing proposals
---------------------
At any stage during the voting process and before the proposal is completed, the proposing 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 withdraw a pending proposal:
.. code-block:: bash
$ memberclient --server-address 127.83.203.69:55526 --cert member1_cert.pem --privk member1_privk.pem --ca networkcert.pem removal --proposal-id 0
$ memberclient --server-address 127.83.203.69:55526 --cert member1_cert.pem --privk member1_privk.pem --ca networkcert.pem withdraw --proposal-id 0
{"commit":110,"global_commit":109,"id":0,"jsonrpc":"2.0","result":true,"term":4}
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.

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

@ -132,12 +132,6 @@ read
.. jsonschema:: schemas/read_params.json
.. jsonschema:: schemas/read_result.json
removal
-------
.. jsonschema:: schemas/removal_params.json
.. jsonschema:: schemas/removal_result.json
updateAckNonce
--------------
@ -149,6 +143,12 @@ vote
.. jsonschema:: schemas/vote_params.json
.. jsonschema:: schemas/vote_result.json
withdraw
-------
.. jsonschema:: schemas/withdraw_params.json
.. jsonschema:: schemas/withdraw_result.json
Management methods
``````````````````

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

@ -10,6 +10,6 @@
"required": [
"id"
],
"title": "removal/params",
"title": "withdraw/params",
"type": "object"
}

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

@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "removal/result",
"title": "withdraw/result",
"type": "boolean"
}

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

@ -75,7 +75,7 @@ static const string accept_code_proposal(R"xxx(
template <typename T>
json proposal_params(const string& script, const T& parameter)
{
return Proposal::In{script, parameter};
return Propose::In{script, parameter};
}
auto query_params(const string& script)
@ -204,10 +204,10 @@ void submit_raw_puts(
cout << response << endl;
}
void submit_removal(RpcTlsClient& tls_connection, ObjectId proposal_id)
void submit_withdraw(RpcTlsClient& tls_connection, ObjectId proposal_id)
{
const auto response = json::from_msgpack(
tls_connection.call("removal", ProposalAction{proposal_id}));
tls_connection.call("withdraw", ProposalAction{proposal_id}));
cout << response << endl;
}
@ -360,8 +360,8 @@ int main(int argc, char** argv)
->required(true)
->check(CLI::ExistingFile);
auto removal = app.add_subcommand("removal", "Remove a proposal");
removal->add_option("--proposal-id", proposal_id, "The proposal id")
auto withdraw = app.add_subcommand("withdraw", "Withdraw a proposal");
withdraw->add_option("--proposal-id", proposal_id, "The proposal id")
->required(true);
auto accept_recovery =
@ -452,9 +452,9 @@ int main(int argc, char** argv)
submit_raw_puts(*tls_connection, script, param_file);
}
if (*removal)
if (*withdraw)
{
submit_removal(*tls_connection, proposal_id);
submit_withdraw(*tls_connection, proposal_id);
}
if (*proposal_display)

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

@ -48,7 +48,7 @@ namespace ccf
* local tables, param = ...
* return Calls:call(Puts:put("table", "key", tables["values"]:get(param))
*/
struct Proposal
struct Propose
{
//! arguments for propose RPC
struct In
@ -70,38 +70,53 @@ namespace ccf
bool completed;
};
};
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)
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Propose::In)
DECLARE_JSON_REQUIRED_FIELDS(Propose::In, script, parameter)
DECLARE_JSON_OPTIONAL_FIELDS(Propose::In, ballot)
DECLARE_JSON_TYPE(Propose::Out)
DECLARE_JSON_REQUIRED_FIELDS(Propose::Out, id, completed)
struct OpenProposal
enum class ProposalState
{
OPEN,
ACCEPTED,
WITHDRAWN,
};
DECLARE_JSON_ENUM(
ProposalState,
{{ProposalState::OPEN, "OPEN"},
{ProposalState::ACCEPTED, "ACCEPTED"},
{ProposalState::WITHDRAWN, "WITHDRAWN"}});
struct Proposal
{
Script script = {};
nlohmann::json parameter = {};
MemberId proposer = {};
ProposalState state = ProposalState::OPEN;
std::unordered_map<MemberId, Script> votes = {};
OpenProposal() = default;
OpenProposal(const Script& s, const nlohmann::json& param, MemberId prop) :
Proposal() = default;
Proposal(const Script& s, const nlohmann::json& param, MemberId prop) :
script(s),
parameter(param),
proposer(prop)
proposer(prop),
state(ProposalState::OPEN)
{}
bool operator==(const OpenProposal& o) const
bool operator==(const Proposal& o) const
{
return script == o.script && parameter == o.parameter &&
proposer == o.proposer && votes == o.votes;
proposer == o.proposer && state == o.state && votes == o.votes;
}
MSGPACK_DEFINE(script, parameter, proposer, votes);
MSGPACK_DEFINE(script, parameter, proposer, state, votes);
};
DECLARE_JSON_TYPE(OpenProposal)
DECLARE_JSON_REQUIRED_FIELDS(OpenProposal, script, parameter, proposer, votes)
DECLARE_JSON_TYPE(Proposal)
DECLARE_JSON_REQUIRED_FIELDS(
Proposal, script, parameter, proposer, state, votes)
using Proposals = Store::Map<ObjectId, OpenProposal>;
using Proposals = Store::Map<ObjectId, Proposal>;
struct ProposalAction
{
@ -147,3 +162,38 @@ namespace ccf
DECLARE_JSON_TYPE(KVRead::In)
DECLARE_JSON_REQUIRED_FIELDS(KVRead::In, table, key);
}
MSGPACK_ADD_ENUM(ccf::ProposalState);
FMT_BEGIN_NAMESPACE
template <>
struct formatter<ccf::ProposalState>
{
template <typename ParseContext>
auto parse(ParseContext& ctx)
{
return ctx.begin();
}
template <typename FormatContext>
auto format(const ccf::ProposalState& state, FormatContext& ctx)
-> decltype(ctx.out())
{
switch (state)
{
case (ccf::ProposalState::OPEN):
{
return format_to(ctx.out(), "open");
}
case (ccf::ProposalState::ACCEPTED):
{
return format_to(ctx.out(), "accepted");
}
case (ccf::ProposalState::WITHDRAWN):
{
return format_to(ctx.out(), "withdrawn");
}
}
}
};
FMT_END_NAMESPACE

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

@ -39,7 +39,7 @@ namespace ccf
static constexpr auto COMPLETE = "complete";
static constexpr auto VOTE = "vote";
static constexpr auto PROPOSE = "propose";
static constexpr auto REMOVAL = "removal";
static constexpr auto WITHDRAW = "withdraw";
static constexpr auto ADD_NODE = "add_node";

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

@ -24,7 +24,7 @@ namespace ccf
const auto s = tx.get_view(network.gov_scripts)->get(name);
if (!s)
throw std::logic_error(
std::string("Could not find gov script: ") + name);
fmt::format("Could not find gov script: {}", name));
return *s;
}
@ -57,11 +57,11 @@ namespace ccf
// accept a node
{"accept_node",
[this](Store::Tx& tx, const nlohmann::json& args) {
const auto id = args;
const auto id = args.get<NodeId>();
auto nodes = tx.get_view(this->network.nodes);
auto info = nodes->get(id);
if (!info)
throw std::logic_error("Node does not exist.");
throw std::logic_error(fmt::format("Node {} does not exist", id));
info->status = NodeStatus::TRUSTED;
nodes->put(id, *info);
return true;
@ -69,11 +69,11 @@ namespace ccf
// retire a node
{"retire_node",
[this](Store::Tx& tx, const nlohmann::json& args) {
const auto id = args;
const auto id = args.get<NodeId>();
auto nodes = tx.get_view(this->network.nodes);
auto info = nodes->get(id);
if (!info)
throw std::logic_error("Node does not exist.");
throw std::logic_error(fmt::format("Node {} does not exist", id));
info->status = NodeStatus::RETIRED;
nodes->put(id, *info);
return true;
@ -81,11 +81,13 @@ namespace ccf
// accept new code
{"new_code",
[this](Store::Tx& tx, const nlohmann::json& args) {
const auto id = args;
const auto id = args.get<CodeDigest>();
auto code_ids = tx.get_view(this->network.code_id);
auto existing_code_id = code_ids->get(id);
if (existing_code_id)
throw std::logic_error("Code signature already exists");
throw std::logic_error(fmt::format(
"Code signature already exists with digest: {:02x}",
fmt::join(id, "")));
code_ids->put(id, CodeStatus::ACCEPTED);
return true;
}},
@ -103,9 +105,14 @@ namespace ccf
bool complete_proposal(Store::Tx& tx, const ObjectId id)
{
auto proposals = tx.get_view(this->network.proposals);
const auto proposal = proposals->get(id);
auto proposal = proposals->get(id);
if (!proposal)
throw std::logic_error("No proposal");
throw std::logic_error(fmt::format("No proposal {}", id));
if (proposal->state != ProposalState::OPEN)
throw std::logic_error(fmt::format(
"Cannot complete non-open proposal - current state is {}",
proposal->state));
// run proposal script
const auto proposed_calls = tsr.run<nlohmann::json>(
@ -183,8 +190,10 @@ namespace ccf
call.args);
}
// if the vote was successful, remove the proposal
proposals->remove(id);
// if the vote was successful, update the proposal's state
proposal->state = ProposalState::ACCEPTED;
proposals->put(id, *proposal);
return true;
}
@ -250,7 +259,9 @@ namespace ccf
in.key);
if (value.empty())
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_PARAMS, "key does not exist");
jsonrpc::StandardErrorCodes::INVALID_PARAMS,
fmt::format(
"Key {} does not exist in table {}", in.key.dump(), in.table));
return jsonrpc::success(value);
};
install_with_auto_schema<KVRead>(MemberProcs::READ, read, Read);
@ -270,19 +281,19 @@ namespace ccf
if (!check_member_active(args.tx, args.caller_id))
return jsonrpc::error(jsonrpc::CCFErrorCodes::INSUFFICIENT_RIGHTS);
const auto in = args.params.get<Proposal::In>();
const auto in = args.params.get<Propose::In>();
const auto proposal_id = get_next_id(
args.tx.get_view(this->network.values), ValueIds::NEXT_PROPOSAL_ID);
OpenProposal proposal(in.script, in.parameter, args.caller_id);
Proposal 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});
return jsonrpc::success<Propose::Out>({proposal_id, completed});
};
install_with_auto_schema<Proposal>(MemberProcs::PROPOSE, propose, Write);
install_with_auto_schema<Propose>(MemberProcs::PROPOSE, propose, Write);
auto removal = [this](RequestArgs& args) {
auto withdraw = [this](RequestArgs& args) {
if (!check_member_status(
args.tx, args.caller_id, {MemberStatus::ACTIVE}))
return jsonrpc::error(jsonrpc::CCFErrorCodes::INSUFFICIENT_RIGHTS);
@ -290,23 +301,39 @@ namespace ccf
const auto proposal_action = args.params.get<ProposalAction>();
const auto proposal_id = proposal_action.id;
auto proposals = args.tx.get_view(this->network.proposals);
const auto proposal = proposals->get(proposal_id);
auto proposal = proposals->get(proposal_id);
if (!proposal)
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_PARAMS,
"Proposal does not exist");
fmt::format("Proposal {} does not exist", proposal_id));
if (proposal->proposer != args.caller_id)
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_REQUEST,
"Proposals can only be removed by proposer.");
jsonrpc::CCFErrorCodes::INVALID_CALLER_ID,
fmt::format(
"Proposal {} can only be withdrawn by proposer {}, not caller {}",
proposal_id,
proposal->proposer,
args.caller_id));
if (proposal->state != ProposalState::OPEN)
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_PARAMS,
fmt::format(
"Proposal {} is currently in state {} - only {} proposals can be "
"withdrawn",
proposal_id,
proposal->state,
ProposalState::OPEN));
proposal->state = ProposalState::WITHDRAWN;
proposals->put(proposal_id, *proposal);
proposals->remove(proposal_id);
return jsonrpc::success(true);
};
install_with_auto_schema<ProposalAction, bool>(
MemberProcs::REMOVAL, removal, Write);
MemberProcs::WITHDRAW, withdraw, Write);
auto vote = [this](RequestArgs& args) {
if (!check_member_active(args.tx, args.caller_id))
@ -322,7 +349,17 @@ namespace ccf
if (!proposal)
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_PARAMS,
"Proposal does not exist");
fmt::format("Proposal {} does not exist", vote.id));
if (proposal->state != ProposalState::OPEN)
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_PARAMS,
fmt::format(
"Proposal {} is currently in state {} - only {} proposals can "
"receive votes",
vote.id,
proposal->state,
ProposalState::OPEN));
// record vote
proposal->votes[args.caller_id] = vote.ballot;
@ -357,8 +394,8 @@ namespace ccf
const auto last_ma = mas->get(args.caller_id);
if (!last_ma)
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_PARAMS,
"No ACK record exists (1)");
jsonrpc::CCFErrorCodes::INVALID_CALLER_ID,
fmt::format("No ACK record exists for caller {}", args.caller_id));
auto verifier =
tls::make_verifier(std::vector<uint8_t>(args.rpc_ctx.caller_cert));
@ -390,8 +427,8 @@ namespace ccf
auto ma = mas->get(args.caller_id);
if (!ma)
return jsonrpc::error(
jsonrpc::StandardErrorCodes::INVALID_PARAMS,
"No ACK record exists (2)");
jsonrpc::CCFErrorCodes::INVALID_CALLER_ID,
fmt::format("No ACK record exists for caller {}", args.caller_id));
ma->next_nonce = rng->random(SIZE_NONCE);
mas->put(args.caller_id, *ma);
return jsonrpc::success(true);

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

@ -317,12 +317,12 @@ TEST_CASE("Proposer ballot")
return Calls:call("new_member", member_cert)
)xxx");
const auto proposej = create_json_req(
Proposal::In{proposal, proposed_member, vote_against}, "propose");
Propose::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 =
Response<Propose::Out> r =
frontend.process_json(rpc_ctx, tx, proposer_id, proposej, sr).value();
// the proposal should be accepted, but not succeed immediately
@ -355,7 +355,7 @@ TEST_CASE("Proposer ballot")
Store::Tx tx;
enclave::RPCContext rpc_ctx(proposer_id, proposer_cert);
const Response<OpenProposal> proposal =
const Response<Proposal> proposal =
get_proposal(rpc_ctx, frontend, proposal_id, proposer_id);
const auto& votes = proposal.result.votes;
@ -446,12 +446,12 @@ TEST_CASE("Add new members until there are 7, then reject")
)xxx");
const auto proposej =
create_json_req(Proposal::In{proposal, new_member.cert}, "propose");
create_json_req(Propose::In{proposal, new_member.cert}, "propose");
{
Store::Tx tx;
ccf::SignedReq sr(proposej);
Response<Proposal::Out> r =
Response<Propose::Out> r =
frontend.process_json(rpc_ctx, tx, proposer_id, proposej, sr).value();
// the proposal should be accepted, but not succeed immediately
@ -460,7 +460,7 @@ TEST_CASE("Add new members until there are 7, then reject")
}
// read initial proposal, as second member
const Response<OpenProposal> initial_read =
const Response<Proposal> initial_read =
get_proposal(rpc_ctx, frontend, proposal_id, voter_a);
CHECK(initial_read.result.proposer == proposer_id);
CHECK(initial_read.result.script == proposal);
@ -513,7 +513,7 @@ TEST_CASE("Add new members until there are 7, then reject")
CCFErrorCodes::INVALID_CALLER_ID);
// re-read proposal, as second member
const Response<OpenProposal> final_read =
const Response<Proposal> final_read =
get_proposal(rpc_ctx, frontend, proposal_id, voter_a);
CHECK(final_read.result.proposer == proposer_id);
CHECK(final_read.result.script == proposal);
@ -615,11 +615,11 @@ TEST_CASE("Accept node")
return Calls:call("accept_node", node_id)
)xxx");
json proposej = create_json_req(Proposal::In{proposal, node_id}, "propose");
json proposej = create_json_req(Propose::In{proposal, node_id}, "propose");
ccf::SignedReq sr(proposej);
Store::Tx tx;
Response<Proposal::Out> r =
Response<Propose::Out> r =
frontend.process_json(rpc_ctx, tx, mid0, proposej, sr).value();
CHECK(!r.result.completed);
CHECK(r.result.id == 0);
@ -655,7 +655,7 @@ bool test_raw_writes(
NetworkTables& network,
GenesisGenerator& gen,
StubNodeState& node,
Proposal::In proposal,
Propose::In proposal,
const int n_members = 1,
const int pro_votes = 1,
bool explicit_proposer_vote = false)
@ -678,7 +678,7 @@ bool test_raw_writes(
ccf::SignedReq sr(proposej);
Store::Tx tx;
Response<Proposal::Out> r =
Response<Propose::Out> r =
frontend.process_json(rpc_ctx, tx, proposer_id, proposej, sr).value();
CHECK(r.result.completed == (n_members == 1));
CHECK(r.result.id == proposal_id);
@ -719,7 +719,7 @@ bool test_raw_writes(
}
else
{
// proposal does not exist anymore, because it completed -> invalid params
// proposal has been accepted - additional votes return an error
check_error(
frontend.process_json(mem_rpc_ctx, tx, i, votej["req"], sr).value(),
StandardErrorCodes::INVALID_PARAMS);
@ -851,12 +851,11 @@ TEST_CASE("Remove proposal")
}
{
json proposej =
create_json_req(Proposal::In{proposal_script, 0}, "propose");
json proposej = create_json_req(Propose::In{proposal_script, 0}, "propose");
ccf::SignedReq sr(proposej);
Store::Tx tx;
Response<Proposal::Out> r =
Response<Propose::Out> r =
frontend.process_json(rpc_ctx, tx, 0, proposej, sr).value();
CHECK(r.result.id == proposal_id);
CHECK(!r.result.completed);
@ -866,46 +865,48 @@ TEST_CASE("Remove proposal")
Store::Tx tx;
auto proposal = tx.get_view(network.proposals)->get(proposal_id);
REQUIRE(proposal);
CHECK(proposal->state == ProposalState::OPEN);
CHECK(proposal->script.text.value() == proposal_script.text.value());
}
SUBCASE("Attempt remove proposal with non existing id")
SUBCASE("Attempt withdraw proposal with non existing id")
{
Store::Tx tx;
json param;
param["id"] = wrong_proposal_id;
json removalj = create_json_req(param, "removal");
ccf::SignedReq sr(removalj);
json withdrawj = create_json_req(param, "withdraw");
ccf::SignedReq sr(withdrawj);
check_error(
frontend.process_json(rpc_ctx, tx, 0, removalj, sr).value(),
frontend.process_json(rpc_ctx, tx, 0, withdrawj, sr).value(),
StandardErrorCodes::INVALID_PARAMS);
}
SUBCASE("Attempt remove proposal that you didn't propose")
SUBCASE("Attempt withdraw proposal that you didn't propose")
{
Store::Tx tx;
json param;
param["id"] = proposal_id;
json removalj = create_json_req(param, "removal");
ccf::SignedReq sr(removalj);
json withdrawj = create_json_req(param, "withdraw");
ccf::SignedReq sr(withdrawj);
check_error(
frontend.process_json(rpc_ctx, tx, 1, removalj, sr).value(),
StandardErrorCodes::INVALID_REQUEST);
frontend.process_json(rpc_ctx, tx, 1, withdrawj, sr).value(),
CCFErrorCodes::INVALID_CALLER_ID);
}
SUBCASE("Successfully remove proposal")
SUBCASE("Successfully withdraw proposal")
{
Store::Tx tx;
json param;
param["id"] = proposal_id;
json removalj = create_json_req(param, "removal");
ccf::SignedReq sr(removalj);
json withdrawj = create_json_req(param, "withdraw");
ccf::SignedReq sr(withdrawj);
check_success(frontend.process_json(rpc_ctx, tx, 0, removalj, sr).value());
// check that the proposal doesn't exist anymore
check_success(frontend.process_json(rpc_ctx, tx, 0, withdrawj, sr).value());
// check that the proposal is now withdrawn
{
Store::Tx tx;
auto proposal = tx.get_view(network.proposals)->get(proposal_id);
CHECK(!proposal);
CHECK(proposal.has_value());
CHECK(proposal->state == ProposalState::WITHDRAWN);
}
}
}
@ -923,11 +924,11 @@ TEST_CASE("Complete proposal after initial rejection")
{
const auto proposal =
"return Calls:call('raw_puts', Puts:put('values', 999, 999))"s;
const auto proposej = create_json_req(Proposal::In{proposal}, "propose");
const auto proposej = create_json_req(Propose::In{proposal}, "propose");
ccf::SignedReq sr(proposej);
Store::Tx tx;
Response<Proposal::Out> r =
Response<Propose::Out> r =
frontend.process_json(rpc_ctx, tx, 0, proposej, sr).value();
CHECK(r.result.completed == false);
}
@ -989,11 +990,11 @@ TEST_CASE("Add user via proposed call")
)xxx");
const vector<uint8_t> user_cert = {1, 2, 3};
json proposej = create_json_req(Proposal::In{proposal, user_cert}, "propose");
json proposej = create_json_req(Propose::In{proposal, user_cert}, "propose");
ccf::SignedReq sr(proposej);
Store::Tx tx;
Response<Proposal::Out> r =
Response<Propose::Out> r =
frontend.process_json(rpc_ctx, tx, 0, proposej, sr).value();
CHECK(r.result.completed);
CHECK(r.result.id == 0);

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

@ -68,13 +68,13 @@ TEST_CASE("nlohmann::json")
}
}
TEST_CASE("OpenProposal")
TEST_CASE("Proposal")
{
using namespace ccf;
{
INFO("Empty proposal");
OpenProposal proposal;
Proposal proposal;
const auto converted = msgpack_roundtrip(proposal);
CHECK(proposal == converted);
}
@ -84,7 +84,7 @@ TEST_CASE("OpenProposal")
Script s("return true");
nlohmann::json p("hello world");
MemberId m(0);
OpenProposal proposal(s, p, m);
Proposal proposal(s, p, m);
const auto converted = msgpack_roundtrip(proposal);
CHECK(proposal == converted);
}
@ -94,7 +94,7 @@ TEST_CASE("OpenProposal")
Script s("return true");
nlohmann::json p("hello world");
MemberId m(0);
OpenProposal proposal(s, p, m);
Proposal proposal(s, p, m);
proposal.votes[1] = Script("return true");
proposal.votes[2] = Script("return false");
proposal.votes[3] = Script("return RoN");

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

@ -55,7 +55,12 @@ def run(args):
assert proposal_id == 0
# display all proposals
network.member_client_rpc_as_json(1, primary, "proposal_display")
proposals = network.member_client_rpc_as_json(1, primary, "proposal_display")
# check proposal is present and open
proposal_entry = proposals.get(str(proposal_id))
assert proposal_entry
assert proposal_entry["state"] == "OPEN"
# 2 out of 3 members vote to accept the new member so that that member can send its own proposals
result = network.vote(1, primary, proposal_id, True)
@ -64,6 +69,27 @@ def run(args):
result = network.vote(2, primary, proposal_id, True)
assert result[0] and result[1]
# further vote requests fail - the proposal has already been accepted
params_error = infra.jsonrpc.ErrorCode.INVALID_PARAMS.value
assert network.vote(1, primary, proposal_id, True)[1]["code"] == params_error
assert network.vote(1, primary, proposal_id, False)[1]["code"] == params_error
assert network.vote(2, primary, proposal_id, True)[1]["code"] == params_error
assert network.vote(2, primary, proposal_id, False)[1]["code"] == params_error
assert network.vote(3, primary, proposal_id, True)[1]["code"] == params_error
assert network.vote(3, primary, proposal_id, False)[1]["code"] == params_error
# accepted proposal cannot be withdrawn
j_result = network.member_client_rpc_as_json(
1, primary, "withdraw", "--proposal-id=0"
)
assert j_result["error"]["code"] == params_error
j_result = network.member_client_rpc_as_json(
2, primary, "withdraw", "--proposal-id=0"
)
assert (
j_result["error"]["code"] == infra.jsonrpc.ErrorCode.INVALID_CALLER_ID.value
)
# member 4 try to make a proposal without having been accepted should get insufficient rights response
result = network.propose(4, primary, "accept_node", "--node-id=0")
assert result[1]["code"] == infra.jsonrpc.ErrorCode.INSUFFICIENT_RIGHTS.value
@ -87,18 +113,48 @@ def run(args):
result = network.vote(2, primary, proposal_id, True)
assert result[0] and result[1]
# member 4 is makes a proposal and then removes it
# member 4 makes a proposal
# proposal number 2
result = network.propose(4, primary, "accept_node", "--node-id=1")
proposal_id = result[1]["id"]
assert not result[1]["completed"]
assert proposal_id == 2
# other members are unable to withdraw proposal 2
j_result = network.member_client_rpc_as_json(
4, primary, "removal", "--proposal-id=2"
2, primary, "withdraw", "--proposal-id=2"
)
assert (
j_result["error"]["code"] == infra.jsonrpc.ErrorCode.INVALID_CALLER_ID.value
)
# member 4 withdraws proposal 2
j_result = network.member_client_rpc_as_json(
4, primary, "withdraw", "--proposal-id=2"
)
assert j_result["result"]
# check proposal is still present, but withdrawn
proposals = network.member_client_rpc_as_json(4, primary, "proposal_display")
proposal_entry = proposals.get("2")
assert proposal_entry
assert proposal_entry["state"] == "WITHDRAWN"
# further withdrawal requests fail
j_result = network.member_client_rpc_as_json(
4, primary, "withdraw", "--proposal-id=2"
)
assert j_result["error"]["code"] == params_error
# further vote requests fail
result = network.vote(4, primary, proposal_id, True)
assert not result[0]
assert result[1]["code"] == params_error
result = network.vote(4, primary, proposal_id, False)
assert not result[0]
assert result[1]["code"] == params_error
# member 4 proposes to inactivate member 1 and other members vote yes
# proposal number 3
j_result = network.member_client_rpc_as_json(