зеркало из https://github.com/microsoft/CCF.git
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:
Родитель
f744d557bb
Коммит
108211d5e0
|
@ -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(
|
||||
|
|
Загрузка…
Ссылка в новой задаче