CCF
Loading...
Searching...
No Matches
proposals.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
4
8#include "js/checks.h"
13
14namespace ccf::gov::endpoints
15{
16 namespace detail
17 {
19 {
20 enum class Status : uint8_t
21 {
24 TooOld
26
27 // May be empty, a colliding proposal ID, or a min_created_at value,
28 // depending on status
29 std::string info;
30 };
31
33 ccf::kv::Tx& tx,
34 const std::string& created_at,
35 const std::vector<uint8_t>& request_digest,
36 const ccf::ProposalId& proposal_id)
37 {
38 auto* cose_recent_proposals =
39 tx.rw<ccf::COSERecentProposals>(ccf::Tables::COSE_RECENT_PROPOSALS);
40 auto key = fmt::format("{}:{}", created_at, ds::to_hex(request_digest));
41
42 if (cose_recent_proposals->has(key))
43 {
44 auto colliding_proposal_id = cose_recent_proposals->get(key);
45 if (colliding_proposal_id.has_value())
46 {
47 return {
49 *colliding_proposal_id};
50 }
51 throw std::logic_error(fmt::format(
52 "Failed to get value for existing key in {}",
53 ccf::Tables::COSE_RECENT_PROPOSALS));
54 }
55
56 std::vector<std::string> replay_keys;
57 cose_recent_proposals->foreach_key(
58 [&replay_keys](const std::string& replay_key) {
59 replay_keys.push_back(replay_key);
60 return true;
61 });
62
63 std::sort(replay_keys.begin(), replay_keys.end());
64
65 // New proposal must be more recent than median proposal kept
66 if (!replay_keys.empty())
67 {
68 const auto [min_created_at, _] =
69 ccf::nonstd::split_1(replay_keys[replay_keys.size() / 2], ":");
70 auto [key_ts, __] = ccf::nonstd::split_1(key, ":");
71 if (key_ts < min_created_at)
72 {
73 return {
75 std::string(min_created_at)};
76 }
77 }
78
79 size_t window_size = ccf::default_recent_cose_proposals_window_size;
80 auto* config_handle =
81 tx.ro<ccf::Configuration>(ccf::Tables::CONFIGURATION);
82 auto config = config_handle->get();
83 if (
84 config.has_value() &&
85 config->recent_cose_proposals_window_size.has_value())
86 {
87 window_size = config->recent_cose_proposals_window_size.value();
88 }
89 cose_recent_proposals->put(key, proposal_id);
90 // Only keep the most recent window_size proposals, to avoid
91 // unbounded memory usage
92 if (replay_keys.size() >= (window_size - 1) /* We just added one */)
93 {
94 for (size_t i = 0; i < (replay_keys.size() - (window_size - 1)); i++)
95 {
96 cose_recent_proposals->remove(replay_keys[i]);
97 }
98 }
99 return {ProposalSubmissionResult::Status::Acceptable, {} /* reason */};
100 }
101
103 ccf::kv::Tx& tx,
104 const MemberId& caller_id,
105 const std::span<const uint8_t>& cose_sign1)
106 {
107 auto* cose_governance_history =
108 tx.wo<ccf::COSEGovernanceHistory>(ccf::Tables::COSE_GOV_HISTORY);
109 cose_governance_history->put(
110 caller_id, {cose_sign1.begin(), cose_sign1.end()});
111 }
112
114 ccf::kv::Tx& tx, const ProposalId& proposal_id)
115 {
116 auto* p = tx.rw<ccf::jsgov::ProposalMap>(jsgov::Tables::PROPOSALS);
117 auto* pi =
118 tx.rw<ccf::jsgov::ProposalInfoMap>(jsgov::Tables::PROPOSALS_INFO);
119 std::vector<ProposalId> to_be_removed;
120 pi->foreach(
121 [&to_be_removed, &proposal_id](
122 const ProposalId& pid, const ccf::jsgov::ProposalInfo& pinfo) {
123 if (pid != proposal_id && pinfo.state != ProposalState::OPEN)
124 {
125 to_be_removed.push_back(pid);
126 }
127 return true;
128 });
129 for (const auto& pr : to_be_removed)
130 {
131 p->remove(pr);
132 pi->remove(pr);
133 }
134 }
135
136 // Evaluate JS functions on this proposal. Result is presented in modified
137 // proposal_info argument, which is written back to the KV by this function
138 inline void resolve_proposal(
140 ccf::NetworkState& network,
141 ccf::kv::Tx& tx,
142 const ProposalId& proposal_id,
143 const std::span<const uint8_t>& proposal_bytes,
144 ccf::jsgov::ProposalInfo& proposal_info,
145 const std::string& constitution)
146 {
147 // Create some temporaries to store resolution progress. These are written
148 // to proposal_info, and the KV, when proposals leave the Open state.
149 ccf::jsgov::Votes votes = {};
150 ccf::jsgov::VoteFailures vote_failures = {};
151
152 const std::string_view proposal{
153 reinterpret_cast<const char*>(proposal_bytes.data()),
154 proposal_bytes.size()};
155
156 auto* proposal_info_handle = tx.template rw<ccf::jsgov::ProposalInfoMap>(
157 jsgov::Tables::PROPOSALS_INFO);
158
159 // Evaluate ballots
160 for (const auto& [mid, mb] : proposal_info.ballots)
161 {
163
164 auto ballot_func = js_context.get_exported_function(
165 mb,
166 "vote",
167 fmt::format(
168 "{}[{}].ballots[{}]",
169 ccf::jsgov::Tables::PROPOSALS_INFO,
170 proposal_id,
171 mid));
172
173 std::vector<js::core::JSWrappedValue> argv = {
174 js_context.new_string(proposal),
175 js_context.new_string(proposal_info.proposer_id.value())};
176
177 auto val = js_context.call_with_rt_options(
178 ballot_func,
179 argv,
180 tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
182
183 if (!val.is_exception())
184 {
185 votes[mid] = val.is_true();
186 }
187 else
188 {
189 auto [reason, trace] = js_context.error_message();
190
191 if (js_context.interrupt_data.request_timed_out)
192 {
193 reason = "Operation took too long to complete.";
194 }
195 vote_failures[mid] = ccf::jsgov::Failure{reason, trace};
196 }
197 }
198
199 // Evaluate resolve function
200 // NB: Since the only change from the calls to `apply` is some tentative
201 // votes, there is no change to the proposal stored in the KV.
202 {
203 {
205
206 auto resolve_func = js_context.get_exported_function(
207 constitution,
208 "resolve",
209 fmt::format("{}[0]", ccf::Tables::CONSTITUTION));
210
211 std::vector<js::core::JSWrappedValue> argv;
212 argv.push_back(js_context.new_string(proposal));
213
214 argv.push_back(
215 js_context.new_string(proposal_info.proposer_id.value()));
216
217 auto vs = js_context.new_array();
218 size_t index = 0;
219 for (auto& [member_id, vote_result] : votes)
220 {
221 auto v = js_context.new_obj();
222
223 JS_CHECK_OR_THROW(v.set(
224 "member_id",
225 js_context.new_string_len(member_id.data(), member_id.size())));
226 JS_CHECK_OR_THROW(v.set_bool("vote", vote_result));
227
228 JS_CHECK_OR_THROW(vs.set_at_index(index++, std::move(v)));
229 }
230 argv.push_back(vs);
231
232 // Also pass the proposal_id as a string. This is useful for proposals
233 // that want to refer to themselves in the resolve function, for
234 // example to examine/distinguish themselves other pending proposals.
235 argv.push_back(js_context.new_string(proposal_id));
236
237 auto val = js_context.call_with_rt_options(
238 resolve_func,
239 argv,
240 tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
242
243 if (val.is_exception())
244 {
245 proposal_info.state = ProposalState::FAILED;
246 auto [reason, trace] = js_context.error_message();
247 if (js_context.interrupt_data.request_timed_out)
248 {
249 reason = "Operation took too long to complete.";
250 }
251 proposal_info.failure = ccf::jsgov::Failure{
252 fmt::format("Failed to resolve(): {}", reason), trace};
253 }
254 else
255 {
256 auto status = js_context.to_str(val).value_or("");
257 // NB: It is not possible to produce every possible ProposalState
258 // here! WITHDRAWN and DROPPED are states that we transition to
259 // elsewhere, but not valid return values from resolve()
260 const std::unordered_map<std::string, ProposalState>
261 js_str_to_status = {
262 {"Open", ProposalState::OPEN},
263 {"Accepted", ProposalState::ACCEPTED},
264 {"Rejected", ProposalState::REJECTED}};
265 const auto it = js_str_to_status.find(status);
266 if (it != js_str_to_status.end())
267 {
268 proposal_info.state = it->second;
269 }
270 else
271 {
272 proposal_info.state = ProposalState::FAILED;
273 proposal_info.failure = ccf::jsgov::Failure{
274 fmt::format(
275 "resolve() returned invalid status value: \"{}\"", status),
276 std::nullopt // No trace
277 };
278 }
279 }
280
281 // Ensure resolved proposal_info is visible in the KV
282 proposal_info_handle->put(proposal_id, proposal_info);
283 }
284
285 if (proposal_info.state != ProposalState::OPEN)
286 {
288
289 // Write now-permanent values back to proposal_state, and into the
290 // KV
291 proposal_info.final_votes = votes;
292 proposal_info.vote_failures = vote_failures;
293 proposal_info_handle->put(proposal_id, proposal_info);
294
295 if (proposal_info.state == ProposalState::ACCEPTED)
296 {
297 // Evaluate apply function
298 auto gov_effects =
300 if (gov_effects == nullptr)
301 {
302 throw std::logic_error(
303 "Unexpected: Could not access GovEffects subsytem");
304 }
305
307
308 js_context.add_extension(
309 std::make_shared<ccf::js::extensions::NodeExtension>(
310 gov_effects.get(), &tx));
311 js_context.add_extension(
312 std::make_shared<ccf::js::extensions::NetworkExtension>(
313 &network, &tx));
314 js_context.add_extension(
315 std::make_shared<ccf::js::extensions::GovEffectsExtension>(&tx));
316
317 auto apply_func = js_context.get_exported_function(
318 constitution,
319 "apply",
320 fmt::format("{}[0]", ccf::Tables::CONSTITUTION));
321
322 std::vector<js::core::JSWrappedValue> argv = {
323 js_context.new_string(proposal),
324 js_context.new_string(proposal_id)};
325
326 auto val = js_context.call_with_rt_options(
327 apply_func,
328 argv,
329 tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
331
332 if (val.is_exception())
333 {
334 proposal_info.state = ProposalState::FAILED;
335 auto [reason, trace] = js_context.error_message();
336 if (js_context.interrupt_data.request_timed_out)
337 {
338 reason = "Operation took too long to complete.";
339 }
340 proposal_info.failure = ccf::jsgov::Failure{
341 fmt::format("Failed to apply(): {}", reason), trace};
342
343 // Update final proposal_info (in KV) again, with failure info
344 proposal_info_handle->put(proposal_id, proposal_info);
345 }
346 }
347 }
348 }
349 }
350
351 inline nlohmann::json convert_proposal_to_api_format(
352 const ProposalId& proposal_id, const ccf::jsgov::ProposalInfo& summary)
353 {
354 auto response_body = nlohmann::json::object();
355
356 response_body["proposalId"] = proposal_id;
357 response_body["proposerId"] = summary.proposer_id;
358 response_body["proposalState"] = summary.state;
359 response_body["ballotCount"] = summary.ballots.size();
360
361 auto ballot_submitters = nlohmann::json::array();
362 std::vector<ccf::MemberId> submitter_ids;
363 for (const auto& [member_id, _] : summary.ballots)
364 {
365 submitter_ids.push_back(member_id);
366 }
367 std::sort(submitter_ids.begin(), submitter_ids.end());
368 for (const auto& member_id : submitter_ids)
369 {
370 ballot_submitters.push_back(member_id);
371 }
372 response_body["ballotSubmitters"] = ballot_submitters;
373
374 std::optional<ccf::jsgov::Votes> votes = summary.final_votes;
375
376 if (votes.has_value())
377 {
378 auto final_votes = nlohmann::json::object();
379 for (const auto& [voter_id, vote_result] : *votes)
380 {
381 final_votes[voter_id.value()] = vote_result;
382 }
383 response_body["finalVotes"] = final_votes;
384 }
385
386 if (summary.vote_failures.has_value())
387 {
388 auto vote_failures = nlohmann::json::object();
389 for (const auto& [failer_id, failure] : *summary.vote_failures)
390 {
391 vote_failures[failer_id.value()] = failure;
392 }
393 response_body["voteFailures"] = vote_failures;
394 }
395
396 if (summary.failure.has_value())
397 {
398 auto failure = nlohmann::json::object();
399 response_body["failure"] = *summary.failure;
400 }
401
402 return response_body;
403 }
404 }
405
408 NetworkState& network,
409 ccf::AbstractNodeContext& node_context)
410 {
412 auto create_proposal = [&](auto& ctx, ApiVersion api_version) {
413 switch (api_version)
414 {
416 case ApiVersion::v1:
417 default:
418 {
419 const auto& cose_ident =
420 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
421
422 std::span<const uint8_t> proposal_body = cose_ident.content;
423 ccf::jsgov::ProposalInfo proposal_info;
424 std::optional<std::string> constitution;
425
426 // Construct proposal_id, as digest of request and root
427 ProposalId proposal_id;
428 std::vector<uint8_t> request_digest;
429 {
430 auto root_at_read = ctx.tx.get_root_at_read_version();
431 if (!root_at_read.has_value())
432 {
433 detail::set_gov_error(
434 ctx.rpc_ctx,
435 HTTP_STATUS_INTERNAL_SERVER_ERROR,
436 ccf::errors::InternalError,
437 "Proposal failed to bind to state.");
438 return;
439 }
440
442 hasher->update_hash(root_at_read.value().h);
443
444 request_digest = ccf::crypto::sha256(
445 cose_ident.signature.data(), cose_ident.signature.size());
446
447 hasher->update_hash(request_digest);
448
449 const ccf::crypto::Sha256Hash proposal_hash = hasher->finalise();
450 proposal_id = proposal_hash.hex_str();
451 }
452
453 // Validate proposal, by calling into JS constitution
454 {
455 constitution =
456 ctx.tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
457 ->get();
458 if (!constitution.has_value())
459 {
460 detail::set_gov_error(
461 ctx.rpc_ctx,
462 HTTP_STATUS_INTERNAL_SERVER_ERROR,
463 ccf::errors::InternalError,
464 "No constitution is set - proposals cannot be evaluated");
465 return;
466 }
467
469
470 auto validate_func = context.get_exported_function(
471 constitution.value(),
472 "validate",
473 fmt::format("{}[0]", ccf::Tables::CONSTITUTION));
474
475 auto proposal_arg = context.new_string_len(cose_ident.content);
476 auto validate_result = context.call_with_rt_options(
477 validate_func,
478 {proposal_arg},
479 ctx.tx.template ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
481
482 // Handle error cases of validation
483 {
484 if (validate_result.is_exception())
485 {
486 auto [reason, trace] = context.error_message();
487 if (context.interrupt_data.request_timed_out)
488 {
489 reason = "Operation took too long to complete.";
490 }
491 detail::set_gov_error(
492 ctx.rpc_ctx,
493 HTTP_STATUS_INTERNAL_SERVER_ERROR,
494 ccf::errors::InternalError,
495 fmt::format(
496 "Failed to execute validation: {} {}",
497 reason,
498 trace.value_or("")));
499 return;
500 }
501
502 if (!validate_result.is_obj())
503 {
504 detail::set_gov_error(
505 ctx.rpc_ctx,
506 HTTP_STATUS_INTERNAL_SERVER_ERROR,
507 ccf::errors::InternalError,
508 "Validation failed to return an object");
509 return;
510 }
511
512 std::string description;
513 auto desc = validate_result["description"];
514 if (desc.is_str())
515 {
516 description = context.to_str(desc).value_or("");
517 }
518
519 auto valid = validate_result["valid"];
520 if (!valid.is_true())
521 {
522 detail::set_gov_error(
523 ctx.rpc_ctx,
524 HTTP_STATUS_BAD_REQUEST,
525 ccf::errors::ProposalFailedToValidate,
526 fmt::format("Proposal failed to validate: {}", description));
527 return;
528 }
529 }
530
531 // Write proposal to KV
532 {
533 auto proposals_handle =
534 ctx.tx.template rw<ccf::jsgov::ProposalMap>(
535 jsgov::Tables::PROPOSALS);
536 // Introduce a read dependency, so that if identical proposal
537 // creations are in-flight and reading at the same version, all
538 // except the first conflict and are re-executed. If we ever
539 // produce a proposal ID which already exists, we must have a
540 // hash collision.
541 if (proposals_handle->has(proposal_id))
542 {
543 detail::set_gov_error(
544 ctx.rpc_ctx,
545 HTTP_STATUS_INTERNAL_SERVER_ERROR,
546 ccf::errors::InternalError,
547 "Proposal ID collision.");
548 return;
549 }
550 proposals_handle->put(
551 proposal_id, {proposal_body.begin(), proposal_body.end()});
552
553 auto proposal_info_handle =
554 ctx.tx.template wo<ccf::jsgov::ProposalInfoMap>(
555 jsgov::Tables::PROPOSALS_INFO);
556
557 proposal_info.proposer_id = cose_ident.member_id;
558 proposal_info.state = ccf::ProposalState::OPEN;
559
560 proposal_info_handle->put(proposal_id, proposal_info);
561
563 ctx.tx, cose_ident.member_id, cose_ident.envelope);
564 }
565 }
566
567 // Validate proposal's created_at time
568 {
569 // created_at, submitted as a binary integer number of seconds
570 // since epoch in the COSE Sign1 envelope, is converted to a
571 // decimal representation in ASCII, stored as a string, and
572 // compared alphanumerically. This is partly to keep governance as
573 // text-based as possible, to faciliate audit, but also to be able
574 // to benefit from future planned ordering support in the KV. To
575 // compare correctly, the string representation needs to be padded
576 // with leading zeroes, and must therefore not exceed a fixed
577 // digit width. 10 digits is enough to last until November 2286,
578 // ie. long enough.
579 if (cose_ident.protected_header.gov_msg_created_at > 9'999'999'999)
580 {
581 detail::set_gov_error(
582 ctx.rpc_ctx,
583 HTTP_STATUS_BAD_REQUEST,
584 ccf::errors::InvalidCreatedAt,
585 "Header parameter created_at value is too large");
586 return;
587 }
588
589 const auto created_at_str = fmt::format(
590 "{:0>10}", cose_ident.protected_header.gov_msg_created_at);
591
592 const auto subtime_result =
594 ctx.tx, created_at_str, request_digest, proposal_id);
595 switch (subtime_result.status)
596 {
598 {
599 detail::set_gov_error(
600 ctx.rpc_ctx,
601 HTTP_STATUS_BAD_REQUEST,
602 ccf::errors::ProposalCreatedTooLongAgo,
603 fmt::format(
604 "Proposal created too long ago, created_at must be greater "
605 "than {}",
606 subtime_result.info));
607 return;
608 }
609
611 {
612 detail::set_gov_error(
613 ctx.rpc_ctx,
614 HTTP_STATUS_BAD_REQUEST,
615 ccf::errors::ProposalReplay,
616 fmt::format(
617 "Proposal submission replay, already exists as proposal {}",
618 subtime_result.info));
619 return;
620 }
621
623 {
624 break;
625 }
626
627 default:
628 {
629 throw std::runtime_error(
630 "Invalid ProposalSubmissionResult::Status value");
631 }
632 }
633 }
634
635 // Resolve proposal (may pass immediately)
636 {
638 node_context,
639 network,
640 ctx.tx,
641 proposal_id,
642 proposal_body,
643 proposal_info,
644 constitution.value());
645
646 if (proposal_info.state == ProposalState::FAILED)
647 {
648 // If the proposal failed to apply, we want to discard the tx and
649 // not apply its side-effects to the KV state, because it may have
650 // failed mid-execution (eg - thrown an exception), in which case
651 // we do not want to apply partial writes. Note this differs from
652 // a failure that happens after a vote, in that this proposal is
653 // not recorded in the KV at all.
654 detail::set_gov_error(
655 ctx.rpc_ctx,
656 HTTP_STATUS_INTERNAL_SERVER_ERROR,
657 ccf::errors::InternalError,
658 fmt::format("{}", proposal_info.failure));
659 return;
660 }
661
662 const auto response_body = detail::convert_proposal_to_api_format(
663 proposal_id, proposal_info);
664
665 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
666 return;
667 }
668 }
669 }
670 };
671 registry
673 "/members/proposals:create",
674 HTTP_POST,
675 api_version_adapter(create_proposal),
677 .set_openapi_hidden(true)
678 .install();
679
680 auto withdraw_proposal = [&](auto& ctx, ApiVersion api_version) {
681 switch (api_version)
682 {
684 case ApiVersion::v1:
685 default:
686 {
687 const auto& cose_ident =
688 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
689 ccf::ProposalId proposal_id;
690
692 cose_ident, ctx.rpc_ctx, proposal_id))
693 {
694 return;
695 }
696
697 auto proposal_info_handle =
698 ctx.tx.template rw<ccf::jsgov::ProposalInfoMap>(
699 jsgov::Tables::PROPOSALS_INFO);
700
701 // Check proposal exists
702 auto proposal_info = proposal_info_handle->get(proposal_id);
703 if (!proposal_info.has_value())
704 {
705 // If it doesn't, then withdrawal is idempotent - we don't know if
706 // this previously existed or not, was withdrawn or accepted, but
707 // return a 204
708 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
709 return;
710 }
711
712 // Check caller is proposer
713 const auto member_id = cose_ident.member_id;
714 if (member_id != proposal_info->proposer_id)
715 {
716 detail::set_gov_error(
717 ctx.rpc_ctx,
718 HTTP_STATUS_FORBIDDEN,
719 ccf::errors::AuthorizationFailed,
720 fmt::format(
721 "Proposal {} can only be withdrawn by proposer {}, not caller "
722 "{}.",
723 proposal_id,
724 proposal_info->proposer_id,
725 member_id));
726 return;
727 }
728
729 // If proposal is still known, and state is neither OPEN nor
730 // WITHDRAWN, return an error - caller has done something wrong
731 if (
732 proposal_info->state != ProposalState::OPEN &&
733 proposal_info->state != ProposalState::WITHDRAWN)
734 {
735 detail::set_gov_error(
736 ctx.rpc_ctx,
737 HTTP_STATUS_BAD_REQUEST,
738 ccf::errors::ProposalNotOpen,
739 fmt::format(
740 "Proposal {} is currently in state {} and cannot be withdrawn.",
741 proposal_id,
742 proposal_info->state));
743 return;
744 }
745
746 // Check proposal is open - only write withdrawal if currently
747 // open
748 if (proposal_info->state == ProposalState::OPEN)
749 {
750 proposal_info->state = ProposalState::WITHDRAWN;
751 proposal_info_handle->put(proposal_id, proposal_info.value());
752
755 ctx.tx, cose_ident.member_id, cose_ident.envelope);
756 }
757
758 auto response_body = detail::convert_proposal_to_api_format(
759 proposal_id, proposal_info.value());
760
761 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
762 return;
763 }
764 }
765 };
766 registry
768 "/members/proposals/{proposalId}:withdraw",
769 HTTP_POST,
770 api_version_adapter(withdraw_proposal),
772 .set_openapi_hidden(true)
773 .install();
774
775 auto get_proposal = [&](auto& ctx, ApiVersion api_version) {
776 switch (api_version)
777 {
779 case ApiVersion::v1:
780 default:
781 {
782 ccf::ProposalId proposal_id;
783 if (!detail::try_parse_proposal_id(ctx.rpc_ctx, proposal_id))
784 {
785 return;
786 }
787
788 auto proposal_info_handle =
789 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
790 jsgov::Tables::PROPOSALS_INFO);
791 auto proposal_info = proposal_info_handle->get(proposal_id);
792 if (!proposal_info.has_value())
793 {
794 detail::set_gov_error(
795 ctx.rpc_ctx,
796 HTTP_STATUS_NOT_FOUND,
797 ccf::errors::ProposalNotFound,
798 fmt::format("Could not find proposal {}.", proposal_id));
799 return;
800 }
801
802 auto response_body = detail::convert_proposal_to_api_format(
803 proposal_id, proposal_info.value());
804
805 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
806 return;
807 }
808 }
809 };
810 registry
812 "/members/proposals/{proposalId}",
813 HTTP_GET,
814 api_version_adapter(get_proposal),
815 no_auth_required)
816 .set_openapi_hidden(true)
817 .install();
818
819 auto list_proposals = [&](auto& ctx, ApiVersion api_version) {
820 switch (api_version)
821 {
823 case ApiVersion::v1:
824 default:
825 {
826 auto proposal_info_handle =
827 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
828 jsgov::Tables::PROPOSALS_INFO);
829
830 auto proposal_list = nlohmann::json::array();
831 proposal_info_handle->foreach(
832 [&proposal_list](
833 const auto& proposal_id, const auto& proposal_info) {
834 auto api_proposal = detail::convert_proposal_to_api_format(
835 proposal_id, proposal_info);
836 proposal_list.push_back(api_proposal);
837 return true;
838 });
839
840 auto response_body = nlohmann::json::object();
841 response_body["value"] = proposal_list;
842
843 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
844 return;
845 }
846 }
847 };
848 registry
850 "/members/proposals",
851 HTTP_GET,
852 api_version_adapter(list_proposals),
853 no_auth_required)
854 .set_openapi_hidden(true)
855 .install();
856
857 auto get_actions = [&](auto& ctx, ApiVersion api_version) {
858 switch (api_version)
859 {
861 case ApiVersion::v1:
862 default:
863 {
864 ccf::ProposalId proposal_id;
865 if (!detail::try_parse_proposal_id(ctx.rpc_ctx, proposal_id))
866 {
867 return;
868 }
869
870 auto proposal_handle = ctx.tx.template ro<ccf::jsgov::ProposalMap>(
871 jsgov::Tables::PROPOSALS);
872
873 const auto proposal = proposal_handle->get(proposal_id);
874 if (!proposal.has_value())
875 {
876 detail::set_gov_error(
877 ctx.rpc_ctx,
878 HTTP_STATUS_NOT_FOUND,
879 ccf::errors::ProposalNotFound,
880 fmt::format("Could not find proposal {}.", proposal_id));
881 return;
882 }
883
884 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
885 ctx.rpc_ctx->set_response_body(proposal.value());
886 return;
887 break;
888 }
889 }
890 };
891 registry
893 "/members/proposals/{proposalId}/actions",
894 HTTP_GET,
895 api_version_adapter(get_actions),
896 no_auth_required)
897 .set_openapi_hidden(true)
898 .install();
899
901 auto submit_ballot = [&](
903 ApiVersion api_version) {
904 switch (api_version)
905 {
907 case ApiVersion::v1:
908 default:
909 {
910 const auto& cose_ident =
911 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
912
913 ccf::ProposalId proposal_id;
915 cose_ident, ctx.rpc_ctx, proposal_id))
916 {
917 return;
918 }
919
920 ccf::MemberId member_id;
922 cose_ident, ctx.rpc_ctx, member_id))
923 {
924 return;
925 }
926
927 // Look up proposal info and check expected state
928 auto* proposal_info_handle =
929 ctx.tx.template rw<ccf::jsgov::ProposalInfoMap>(
930 jsgov::Tables::PROPOSALS_INFO);
931 auto proposal_info = proposal_info_handle->get(proposal_id);
932 if (!proposal_info.has_value())
933 {
934 detail::set_gov_error(
935 ctx.rpc_ctx,
936 HTTP_STATUS_NOT_FOUND,
937 ccf::errors::ProposalNotFound,
938 fmt::format("Could not find proposal {}.", proposal_id));
939 return;
940 }
941
942 if (proposal_info->state != ccf::ProposalState::OPEN)
943 {
944 detail::set_gov_error(
945 ctx.rpc_ctx,
946 HTTP_STATUS_BAD_REQUEST,
947 ccf::errors::ProposalNotOpen,
948 fmt::format(
949 "Proposal {} is currently in state {} - only {} proposals "
950 "can receive votes",
951 proposal_id,
952 proposal_info->state,
954 return;
955 }
956
957 // Look up proposal contents
958 auto* proposals_handle = ctx.tx.template ro<ccf::jsgov::ProposalMap>(
959 ccf::jsgov::Tables::PROPOSALS);
960 const auto proposal = proposals_handle->get(proposal_id);
961 if (!proposal.has_value())
962 {
963 detail::set_gov_error(
964 ctx.rpc_ctx,
965 HTTP_STATUS_NOT_FOUND,
966 ccf::errors::ProposalNotFound,
967 fmt::format("Could not find proposal {}.", proposal_id));
968 return;
969 }
970
971 // Parse and validate incoming ballot
972 const auto params = nlohmann::json::parse(cose_ident.content);
973 const auto ballot_it = params.find("ballot");
974 if (ballot_it == params.end() || !ballot_it.value().is_string())
975 {
976 detail::set_gov_error(
977 ctx.rpc_ctx,
978 HTTP_STATUS_BAD_REQUEST,
979 ccf::errors::InvalidInput,
980 "Signed request body is not a JSON object containing required "
981 "string field \"ballot\"");
982 return;
983 }
984
985 // Access constitution to evaluate ballots
986 const auto constitution =
987 ctx.tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
988 ->get();
989 if (!constitution.has_value())
990 {
991 detail::set_gov_error(
992 ctx.rpc_ctx,
993 HTTP_STATUS_INTERNAL_SERVER_ERROR,
994 ccf::errors::InternalError,
995 "No constitution is set - ballots cannot be evaluated");
996 return;
997 }
998
999 const auto ballot = ballot_it.value().get<std::string>();
1000
1001 const auto info_ballot_it = proposal_info->ballots.find(member_id);
1002 if (info_ballot_it != proposal_info->ballots.end())
1003 {
1004 // If ballot matches previously submitted, aim for idempotent
1005 // matching response
1006 if (info_ballot_it->second == ballot)
1007 {
1008 const auto response_body = detail::convert_proposal_to_api_format(
1009 proposal_id, proposal_info.value());
1010
1011 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1012 return;
1013 }
1014
1015 detail::set_gov_error(
1016 ctx.rpc_ctx,
1017 HTTP_STATUS_BAD_REQUEST,
1018 ccf::errors::VoteAlreadyExists,
1019 fmt::format(
1020 "Different ballot already submitted by {} for {}.",
1021 member_id,
1022 proposal_id));
1023 return;
1024 }
1025
1026 // Store newly provided ballot
1027 proposal_info->ballots.insert_or_assign(
1028 info_ballot_it, member_id, ballot_it.value().get<std::string>());
1029
1031 ctx.tx, cose_ident.member_id, cose_ident.envelope);
1032
1034 node_context,
1035 network,
1036 ctx.tx,
1037 proposal_id,
1038 proposal.value(),
1039 proposal_info.value(),
1040 constitution.value());
1041
1042 if (proposal_info->state == ProposalState::FAILED)
1043 {
1044 // If the proposal failed to apply, we want to discard the tx and
1045 // not apply its side-effects to the KV state, because it may have
1046 // failed mid-execution (eg - thrown an exception), in which case
1047 // we do not want to apply partial writes
1048 detail::set_gov_error(
1049 ctx.rpc_ctx,
1050 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1051 ccf::errors::InternalError,
1052 fmt::format("{}", proposal_info->failure));
1053 return;
1054 }
1055
1056 const auto response_body = detail::convert_proposal_to_api_format(
1057 proposal_id, proposal_info.value());
1058
1059 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1060 return;
1061 }
1062 }
1063 };
1064 registry
1066 "/members/proposals/{proposalId}/ballots/{memberId}:submit",
1067 HTTP_POST,
1068 api_version_adapter(submit_ballot),
1070 .set_openapi_hidden(true)
1071 .install();
1072
1073 auto get_ballot = [&](auto& ctx, ApiVersion api_version) {
1074 switch (api_version)
1075 {
1077 case ApiVersion::v1:
1078 default:
1079 {
1080 ccf::ProposalId proposal_id;
1081 if (!detail::try_parse_proposal_id(ctx.rpc_ctx, proposal_id))
1082 {
1083 return;
1084 }
1085
1086 ccf::MemberId member_id;
1087 if (!detail::try_parse_member_id(ctx.rpc_ctx, member_id))
1088 {
1089 return;
1090 }
1091
1092 // Look up proposal
1093 auto proposal_info_handle =
1094 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
1095 ccf::jsgov::Tables::PROPOSALS_INFO);
1096
1097 // NB: Logically constant (read-only), but non-const so we can
1098 // eventually move a field out
1099 auto proposal_info = proposal_info_handle->get(proposal_id);
1100 if (!proposal_info.has_value())
1101 {
1102 detail::set_gov_error(
1103 ctx.rpc_ctx,
1104 HTTP_STATUS_NOT_FOUND,
1105 ccf::errors::ProposalNotFound,
1106 fmt::format("Proposal {} does not exist.", proposal_id));
1107 return;
1108 }
1109
1110 // Look up ballot
1111 auto ballot_it = proposal_info->ballots.find(member_id);
1112 if (ballot_it == proposal_info->ballots.end())
1113 {
1114 detail::set_gov_error(
1115 ctx.rpc_ctx,
1116 HTTP_STATUS_NOT_FOUND,
1117 ccf::errors::VoteNotFound,
1118 fmt::format(
1119 "Member {} has not voted for proposal {}.",
1120 member_id,
1121 proposal_id));
1122 return;
1123 }
1124
1125 // Return the raw ballot, with appropriate content-type
1126 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1127 ctx.rpc_ctx->set_response_body(std::move(ballot_it->second));
1128 ctx.rpc_ctx->set_response_header(
1129 http::headers::CONTENT_TYPE,
1130 http::headervalues::contenttype::JAVASCRIPT);
1131 return;
1132 }
1133 }
1134 };
1135 registry
1137 "/members/proposals/{proposalId}/ballots/{memberId}",
1138 HTTP_GET,
1139 api_version_adapter(get_ballot),
1140 no_auth_required)
1141 .set_openapi_hidden(true)
1142 .install();
1143 }
1144}
#define JS_CHECK_OR_THROW(val)
Definition checks.h:23
Definition gov_effects_interface.h:12
Definition base_endpoint_registry.h:121
Definition sha256_hash.h:16
std::string hex_str() const
Definition sha256_hash.cpp:57
virtual Endpoint make_endpoint(const std::string &method, RESTVerb verb, const EndpointFunction &f, const AuthnPolicies &ap)
Definition endpoint_registry.cpp:282
virtual Endpoint make_read_only_endpoint(const std::string &method, RESTVerb verb, const ReadOnlyEndpointFunction &f, const AuthnPolicies &ap)
Definition endpoint_registry.cpp:313
Definition common_context.h:54
M::ReadOnlyHandle * ro(M &m)
Definition tx.h:168
Definition tx.h:200
M::Handle * rw(M &m)
Definition tx.h:211
M::WriteOnlyHandle * wo(M &m)
Definition tx.h:232
Definition map.h:30
Definition value.h:32
HashBytes sha256(const std::span< uint8_t const > &data)
Definition hash.cpp:24
std::shared_ptr< ISha256Hash > make_incremental_sha256()
Definition hash.cpp:46
bool try_parse_member_id(const std::shared_ptr< ccf::RpcContext > &rpc_ctx, ccf::MemberId &member_id)
Definition helpers.h:64
void record_cose_governance_history(ccf::kv::Tx &tx, const MemberId &caller_id, const std::span< const uint8_t > &cose_sign1)
Definition proposals.h:102
bool try_parse_proposal_id(const std::shared_ptr< ccf::RpcContext > &rpc_ctx, ccf::ProposalId &proposal_id)
Definition helpers.h:158
void remove_all_other_non_open_proposals(ccf::kv::Tx &tx, const ProposalId &proposal_id)
Definition proposals.h:113
nlohmann::json convert_proposal_to_api_format(const ProposalId &proposal_id, const ccf::jsgov::ProposalInfo &summary)
Definition proposals.h:351
void resolve_proposal(ccf::AbstractNodeContext &context, ccf::NetworkState &network, ccf::kv::Tx &tx, const ProposalId &proposal_id, const std::span< const uint8_t > &proposal_bytes, ccf::jsgov::ProposalInfo &proposal_info, const std::string &constitution)
Definition proposals.h:138
bool try_parse_signed_member_id(const ccf::MemberCOSESign1AuthnIdentity &cose_ident, const std::shared_ptr< ccf::RpcContext > &rpc_ctx, ccf::MemberId &member_id)
Definition helpers.h:100
bool try_parse_signed_proposal_id(const ccf::MemberCOSESign1AuthnIdentity &cose_ident, const std::shared_ptr< ccf::RpcContext > &rpc_ctx, ccf::ProposalId &proposal_id)
Definition helpers.h:198
ProposalSubmissionResult validate_proposal_submission_time(ccf::kv::Tx &tx, const std::string &created_at, const std::vector< uint8_t > &request_digest, const ccf::ProposalId &proposal_id)
Definition proposals.h:32
AuthnPolicies active_member_sig_only_policies(const std::string &gov_msg_type)
Definition helpers.h:16
Definition api_version.h:11
void init_proposals_handlers(ccf::BaseEndpointRegistry &registry, NetworkState &network, ccf::AbstractNodeContext &node_context)
Definition proposals.h:406
ApiVersion
Definition api_version.h:13
auto api_version_adapter(Fn &&f, ApiVersion min_accepted=ApiVersion::MIN)
Definition api_version.h:101
std::unordered_map< ccf::MemberId, bool > Votes
Definition gov.h:12
std::unordered_map< ccf::MemberId, Failure > VoteFailures
Definition gov.h:30
std::string ProposalId
Definition proposals.h:40
Definition node_context.h:12
std::shared_ptr< T > get_subsystem() const
Definition node_context.h:60
Value & value()
Definition entity_id.h:67
Definition network_state.h:12
std::shared_ptr< ccf::RpcContext > rpc_ctx
Definition endpoint_context.h:34
Definition endpoint_context.h:58
ccf::kv::Tx & tx
Definition endpoint_context.h:64
Endpoint & set_openapi_hidden(bool hidden)
Definition endpoint.cpp:10
void install()
Definition endpoint.cpp:135
std::string info
Definition proposals.h:29
enum ccf::gov::endpoints::detail::ProposalSubmissionResult::Status status
Definition gov.h:15
Proposal metadata stored in the KV.
Definition gov.h:34
std::optional< Votes > final_votes
Definition gov.h:48
ccf::MemberId proposer_id
ID of the member who originally created/submitted this proposal.
Definition gov.h:36
std::optional< Failure > failure
Definition gov.h:56
std::optional< VoteFailures > vote_failures
Definition gov.h:52
Ballots ballots
Definition gov.h:43
ccf::ProposalState state
Current state of this proposal (eg - open, accepted, withdrawn)
Definition gov.h:38