34 const std::string& created_at,
35 const std::vector<uint8_t>& request_digest,
38 auto* cose_recent_proposals =
40 auto key = fmt::format(
"{}:{}", created_at, ds::to_hex(request_digest));
42 if (cose_recent_proposals->has(key))
44 auto colliding_proposal_id = cose_recent_proposals->get(key);
45 if (colliding_proposal_id.has_value())
49 *colliding_proposal_id};
51 throw std::logic_error(fmt::format(
52 "Failed to get value for existing key in {}",
53 ccf::Tables::COSE_RECENT_PROPOSALS));
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);
63 std::sort(replay_keys.begin(), replay_keys.end());
66 if (!replay_keys.empty())
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)
75 std::string(min_created_at)};
79 size_t window_size = ccf::default_recent_cose_proposals_window_size;
81 tx.
ro<ccf::Configuration>(ccf::Tables::CONFIGURATION);
82 auto config = config_handle->get();
85 config->recent_cose_proposals_window_size.has_value())
87 window_size = config->recent_cose_proposals_window_size.value();
89 cose_recent_proposals->put(key, proposal_id);
92 if (replay_keys.size() >= (window_size - 1) )
94 for (
size_t i = 0; i < (replay_keys.size() - (window_size - 1)); i++)
96 cose_recent_proposals->remove(replay_keys[i]);
143 const std::span<const uint8_t>& proposal_bytes,
145 const std::string& constitution)
152 const std::string_view proposal{
153 reinterpret_cast<const char*
>(proposal_bytes.data()),
154 proposal_bytes.size()};
156 auto* proposal_info_handle = tx.template rw<ccf::jsgov::ProposalInfoMap>(
157 jsgov::Tables::PROPOSALS_INFO);
160 for (
const auto& [mid, mb] : proposal_info.
ballots)
164 auto ballot_func = js_context.get_exported_function(
168 "{}[{}].ballots[{}]",
169 ccf::jsgov::Tables::PROPOSALS_INFO,
173 std::vector<js::core::JSWrappedValue> argv = {
174 js_context.new_string(proposal),
177 auto val = js_context.call_with_rt_options(
183 if (!val.is_exception())
185 votes[mid] = val.is_true();
189 auto [reason, trace] = js_context.error_message();
191 if (js_context.interrupt_data.request_timed_out)
193 reason =
"Operation took too long to complete.";
206 auto resolve_func = js_context.get_exported_function(
209 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
211 std::vector<js::core::JSWrappedValue> argv;
212 argv.push_back(js_context.new_string(proposal));
217 auto vs = js_context.new_array();
219 for (
auto& [member_id, vote_result] : votes)
221 auto v = js_context.new_obj();
225 js_context.new_string_len(member_id.data(), member_id.size())));
235 argv.push_back(js_context.new_string(proposal_id));
237 auto val = js_context.call_with_rt_options(
243 if (val.is_exception())
246 auto [reason, trace] = js_context.error_message();
247 if (js_context.interrupt_data.request_timed_out)
249 reason =
"Operation took too long to complete.";
252 fmt::format(
"Failed to resolve(): {}", reason), trace};
256 auto status = js_context.to_str(val).value_or(
"");
260 const std::unordered_map<std::string, ProposalState>
265 const auto it = js_str_to_status.find(status);
266 if (it != js_str_to_status.end())
268 proposal_info.
state = it->second;
275 "resolve() returned invalid status value: \"{}\"", status),
282 proposal_info_handle->put(proposal_id, proposal_info);
293 proposal_info_handle->put(proposal_id, proposal_info);
300 if (gov_effects ==
nullptr)
302 throw std::logic_error(
303 "Unexpected: Could not access GovEffects subsytem");
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>(
314 js_context.add_extension(
315 std::make_shared<ccf::js::extensions::GovEffectsExtension>(&tx));
317 auto apply_func = js_context.get_exported_function(
320 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
322 std::vector<js::core::JSWrappedValue> argv = {
323 js_context.new_string(proposal),
324 js_context.new_string(proposal_id)};
326 auto val = js_context.call_with_rt_options(
332 if (val.is_exception())
335 auto [reason, trace] = js_context.error_message();
336 if (js_context.interrupt_data.request_timed_out)
338 reason =
"Operation took too long to complete.";
341 fmt::format(
"Failed to apply(): {}", reason), trace};
344 proposal_info_handle->put(proposal_id, proposal_info);
399 auto create_proposal = [&](
auto& ctx,
ApiVersion api_version) {
406 const auto& cose_ident =
407 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
409 std::span<const uint8_t> proposal_body = cose_ident.content;
411 std::optional<std::string> constitution;
415 std::vector<uint8_t> request_digest;
417 auto root_at_read = ctx.tx.get_root_at_read_version();
418 if (!root_at_read.has_value())
420 detail::set_gov_error(
422 HTTP_STATUS_INTERNAL_SERVER_ERROR,
423 ccf::errors::InternalError,
424 "Proposal failed to bind to state.");
429 hasher->update_hash(root_at_read.value().h);
432 cose_ident.signature.data(), cose_ident.signature.size());
434 hasher->update_hash(request_digest);
437 proposal_id = proposal_hash.
hex_str();
443 ctx.tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
445 if (!constitution.has_value())
447 detail::set_gov_error(
449 HTTP_STATUS_INTERNAL_SERVER_ERROR,
450 ccf::errors::InternalError,
451 "No constitution is set - proposals cannot be evaluated");
457 auto validate_func = context.get_exported_function(
458 constitution.value(),
460 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
462 auto proposal_arg = context.new_string_len(cose_ident.content);
463 auto validate_result = context.call_with_rt_options(
466 ctx.tx.template ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
471 if (validate_result.is_exception())
473 auto [reason, trace] = context.error_message();
474 if (context.interrupt_data.request_timed_out)
476 reason =
"Operation took too long to complete.";
478 detail::set_gov_error(
480 HTTP_STATUS_INTERNAL_SERVER_ERROR,
481 ccf::errors::InternalError,
483 "Failed to execute validation: {} {}",
485 trace.value_or(
"")));
489 if (!validate_result.is_obj())
491 detail::set_gov_error(
493 HTTP_STATUS_INTERNAL_SERVER_ERROR,
494 ccf::errors::InternalError,
495 "Validation failed to return an object");
499 std::string description;
500 auto desc = validate_result[
"description"];
503 description = context.to_str(desc).value_or(
"");
506 auto valid = validate_result[
"valid"];
507 if (!valid.is_true())
509 detail::set_gov_error(
511 HTTP_STATUS_BAD_REQUEST,
512 ccf::errors::ProposalFailedToValidate,
513 fmt::format(
"Proposal failed to validate: {}", description));
520 auto proposals_handle =
521 ctx.tx.template rw<ccf::jsgov::ProposalMap>(
522 jsgov::Tables::PROPOSALS);
528 if (proposals_handle->has(proposal_id))
530 detail::set_gov_error(
532 HTTP_STATUS_INTERNAL_SERVER_ERROR,
533 ccf::errors::InternalError,
534 "Proposal ID collision.");
537 proposals_handle->put(
538 proposal_id, {proposal_body.begin(), proposal_body.end()});
540 auto proposal_info_handle =
541 ctx.tx.template wo<ccf::jsgov::ProposalInfoMap>(
542 jsgov::Tables::PROPOSALS_INFO);
547 proposal_info_handle->put(proposal_id, proposal_info);
550 ctx.tx, cose_ident.member_id, cose_ident.envelope);
566 if (cose_ident.protected_header.gov_msg_created_at > 9'999'999'999)
568 detail::set_gov_error(
570 HTTP_STATUS_BAD_REQUEST,
571 ccf::errors::InvalidCreatedAt,
572 "Header parameter created_at value is too large");
576 const auto created_at_str = fmt::format(
577 "{:0>10}", cose_ident.protected_header.gov_msg_created_at);
579 const auto subtime_result =
581 ctx.tx, created_at_str, request_digest, proposal_id);
582 switch (subtime_result.status)
586 detail::set_gov_error(
588 HTTP_STATUS_BAD_REQUEST,
589 ccf::errors::ProposalCreatedTooLongAgo,
591 "Proposal created too long ago, created_at must be greater "
593 subtime_result.info));
599 detail::set_gov_error(
601 HTTP_STATUS_BAD_REQUEST,
602 ccf::errors::ProposalReplay,
604 "Proposal submission replay, already exists as proposal {}",
605 subtime_result.info));
616 throw std::runtime_error(
617 "Invalid ProposalSubmissionResult::Status value");
631 constitution.value());
641 detail::set_gov_error(
643 HTTP_STATUS_INTERNAL_SERVER_ERROR,
644 ccf::errors::InternalError,
645 fmt::format(
"{}", proposal_info.
failure));
650 proposal_id, proposal_info);
652 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
660 "/members/proposals:create",
667 auto withdraw_proposal = [&](
auto& ctx,
ApiVersion api_version) {
674 const auto& cose_ident =
675 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
679 cose_ident, ctx.rpc_ctx, proposal_id))
684 auto proposal_info_handle =
685 ctx.tx.template rw<ccf::jsgov::ProposalInfoMap>(
686 jsgov::Tables::PROPOSALS_INFO);
689 auto proposal_info = proposal_info_handle->get(proposal_id);
690 if (!proposal_info.has_value())
695 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
700 const auto member_id = cose_ident.member_id;
701 if (member_id != proposal_info->proposer_id)
703 detail::set_gov_error(
705 HTTP_STATUS_FORBIDDEN,
706 ccf::errors::AuthorizationFailed,
708 "Proposal {} can only be withdrawn by proposer {}, not caller "
711 proposal_info->proposer_id,
722 detail::set_gov_error(
724 HTTP_STATUS_BAD_REQUEST,
725 ccf::errors::ProposalNotOpen,
727 "Proposal {} is currently in state {} and cannot be withdrawn.",
729 proposal_info->state));
738 proposal_info_handle->put(proposal_id, proposal_info.value());
742 ctx.tx, cose_ident.member_id, cose_ident.envelope);
746 proposal_id, proposal_info.value());
748 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
755 "/members/proposals/{proposalId}:withdraw",
762 auto get_proposal = [&](
auto& ctx,
ApiVersion api_version) {
775 auto proposal_info_handle =
776 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
777 jsgov::Tables::PROPOSALS_INFO);
778 auto proposal_info = proposal_info_handle->get(proposal_id);
779 if (!proposal_info.has_value())
781 detail::set_gov_error(
783 HTTP_STATUS_NOT_FOUND,
784 ccf::errors::ProposalNotFound,
785 fmt::format(
"Could not find proposal {}.", proposal_id));
790 proposal_id, proposal_info.value());
792 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
799 "/members/proposals/{proposalId}",
806 auto list_proposals = [&](
auto& ctx,
ApiVersion api_version) {
813 auto proposal_info_handle =
814 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
815 jsgov::Tables::PROPOSALS_INFO);
817 auto proposal_list = nlohmann::json::array();
818 proposal_info_handle->foreach(
820 const auto& proposal_id,
const auto& proposal_info) {
822 proposal_id, proposal_info);
823 proposal_list.push_back(api_proposal);
827 auto response_body = nlohmann::json::object();
828 response_body[
"value"] = proposal_list;
830 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
837 "/members/proposals",
844 auto get_actions = [&](
auto& ctx,
ApiVersion api_version) {
857 auto proposal_handle = ctx.tx.template ro<ccf::jsgov::ProposalMap>(
858 jsgov::Tables::PROPOSALS);
860 const auto proposal = proposal_handle->get(proposal_id);
861 if (!proposal.has_value())
863 detail::set_gov_error(
865 HTTP_STATUS_NOT_FOUND,
866 ccf::errors::ProposalNotFound,
867 fmt::format(
"Could not find proposal {}.", proposal_id));
871 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
872 ctx.rpc_ctx->set_response_body(proposal.value());
880 "/members/proposals/{proposalId}/actions",
888 auto submit_ballot = [&](
897 const auto& cose_ident =
898 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
902 cose_ident, ctx.
rpc_ctx, proposal_id))
909 cose_ident, ctx.
rpc_ctx, member_id))
915 auto* proposal_info_handle =
916 ctx.
tx.template rw<ccf::jsgov::ProposalInfoMap>(
917 jsgov::Tables::PROPOSALS_INFO);
918 auto proposal_info = proposal_info_handle->get(proposal_id);
919 if (!proposal_info.has_value())
921 detail::set_gov_error(
923 HTTP_STATUS_NOT_FOUND,
924 ccf::errors::ProposalNotFound,
925 fmt::format(
"Could not find proposal {}.", proposal_id));
931 detail::set_gov_error(
933 HTTP_STATUS_BAD_REQUEST,
934 ccf::errors::ProposalNotOpen,
936 "Proposal {} is currently in state {} - only {} proposals "
939 proposal_info->state,
945 auto* proposals_handle = ctx.
tx.template ro<ccf::jsgov::ProposalMap>(
946 ccf::jsgov::Tables::PROPOSALS);
947 const auto proposal = proposals_handle->get(proposal_id);
948 if (!proposal.has_value())
950 detail::set_gov_error(
952 HTTP_STATUS_NOT_FOUND,
953 ccf::errors::ProposalNotFound,
954 fmt::format(
"Could not find proposal {}.", proposal_id));
959 const auto params = nlohmann::json::parse(cose_ident.content);
960 const auto ballot_it = params.find(
"ballot");
961 if (ballot_it == params.end() || !ballot_it.value().is_string())
963 detail::set_gov_error(
965 HTTP_STATUS_BAD_REQUEST,
966 ccf::errors::InvalidInput,
967 "Signed request body is not a JSON object containing required "
968 "string field \"ballot\"");
973 const auto constitution =
974 ctx.
tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
976 if (!constitution.has_value())
978 detail::set_gov_error(
980 HTTP_STATUS_INTERNAL_SERVER_ERROR,
981 ccf::errors::InternalError,
982 "No constitution is set - ballots cannot be evaluated");
986 const auto ballot = ballot_it.value().get<std::string>();
988 const auto info_ballot_it = proposal_info->ballots.find(member_id);
989 if (info_ballot_it != proposal_info->ballots.end())
993 if (info_ballot_it->second == ballot)
996 proposal_id, proposal_info.value());
998 ctx.
rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1002 detail::set_gov_error(
1004 HTTP_STATUS_BAD_REQUEST,
1005 ccf::errors::VoteAlreadyExists,
1007 "Different ballot already submitted by {} for {}.",
1014 proposal_info->ballots.insert_or_assign(
1015 info_ballot_it, member_id, ballot_it.
value().get<std::string>());
1018 ctx.
tx, cose_ident.member_id, cose_ident.envelope);
1026 proposal_info.value(),
1027 constitution.value());
1035 detail::set_gov_error(
1037 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1038 ccf::errors::InternalError,
1039 fmt::format(
"{}", proposal_info->failure));
1044 proposal_id, proposal_info.value());
1046 ctx.
rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1053 "/members/proposals/{proposalId}/ballots/{memberId}:submit",
1060 auto get_ballot = [&](
auto& ctx,
ApiVersion api_version) {
1061 switch (api_version)
1080 auto proposal_info_handle =
1081 ctx.
tx.template ro<ccf::jsgov::ProposalInfoMap>(
1082 ccf::jsgov::Tables::PROPOSALS_INFO);
1086 auto proposal_info = proposal_info_handle->get(proposal_id);
1087 if (!proposal_info.has_value())
1089 detail::set_gov_error(
1091 HTTP_STATUS_NOT_FOUND,
1092 ccf::errors::ProposalNotFound,
1093 fmt::format(
"Proposal {} does not exist.", proposal_id));
1098 auto ballot_it = proposal_info->ballots.find(member_id);
1099 if (ballot_it == proposal_info->ballots.end())
1101 detail::set_gov_error(
1103 HTTP_STATUS_NOT_FOUND,
1104 ccf::errors::VoteNotFound,
1106 "Member {} has not voted for proposal {}.",
1113 ctx.
rpc_ctx->set_response_status(HTTP_STATUS_OK);
1114 ctx.
rpc_ctx->set_response_body(std::move(ballot_it->second));
1115 ctx.
rpc_ctx->set_response_header(
1116 http::headers::CONTENT_TYPE,
1117 http::headervalues::contenttype::JAVASCRIPT);
1124 "/members/proposals/{proposalId}/ballots/{memberId}",