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;
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);
412 auto create_proposal = [&](
auto& ctx,
ApiVersion api_version) {
419 const auto& cose_ident =
420 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
422 std::span<const uint8_t> proposal_body = cose_ident.content;
424 std::optional<std::string> constitution;
428 std::vector<uint8_t> request_digest;
430 auto root_at_read = ctx.tx.get_root_at_read_version();
431 if (!root_at_read.has_value())
433 detail::set_gov_error(
435 HTTP_STATUS_INTERNAL_SERVER_ERROR,
436 ccf::errors::InternalError,
437 "Proposal failed to bind to state.");
442 hasher->update_hash(root_at_read.value().h);
445 cose_ident.signature.data(), cose_ident.signature.size());
447 hasher->update_hash(request_digest);
450 proposal_id = proposal_hash.
hex_str();
456 ctx.tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
458 if (!constitution.has_value())
460 detail::set_gov_error(
462 HTTP_STATUS_INTERNAL_SERVER_ERROR,
463 ccf::errors::InternalError,
464 "No constitution is set - proposals cannot be evaluated");
470 auto validate_func = context.get_exported_function(
471 constitution.value(),
473 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
475 auto proposal_arg = context.new_string_len(cose_ident.content);
476 auto validate_result = context.call_with_rt_options(
479 ctx.tx.template ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
484 if (validate_result.is_exception())
486 auto [reason, trace] = context.error_message();
487 if (context.interrupt_data.request_timed_out)
489 reason =
"Operation took too long to complete.";
491 detail::set_gov_error(
493 HTTP_STATUS_INTERNAL_SERVER_ERROR,
494 ccf::errors::InternalError,
496 "Failed to execute validation: {} {}",
498 trace.value_or(
"")));
502 if (!validate_result.is_obj())
504 detail::set_gov_error(
506 HTTP_STATUS_INTERNAL_SERVER_ERROR,
507 ccf::errors::InternalError,
508 "Validation failed to return an object");
512 std::string description;
513 auto desc = validate_result[
"description"];
516 description = context.to_str(desc).value_or(
"");
519 auto valid = validate_result[
"valid"];
520 if (!valid.is_true())
522 detail::set_gov_error(
524 HTTP_STATUS_BAD_REQUEST,
525 ccf::errors::ProposalFailedToValidate,
526 fmt::format(
"Proposal failed to validate: {}", description));
533 auto proposals_handle =
534 ctx.tx.template rw<ccf::jsgov::ProposalMap>(
535 jsgov::Tables::PROPOSALS);
541 if (proposals_handle->has(proposal_id))
543 detail::set_gov_error(
545 HTTP_STATUS_INTERNAL_SERVER_ERROR,
546 ccf::errors::InternalError,
547 "Proposal ID collision.");
550 proposals_handle->put(
551 proposal_id, {proposal_body.begin(), proposal_body.end()});
553 auto proposal_info_handle =
554 ctx.tx.template wo<ccf::jsgov::ProposalInfoMap>(
555 jsgov::Tables::PROPOSALS_INFO);
560 proposal_info_handle->put(proposal_id, proposal_info);
563 ctx.tx, cose_ident.member_id, cose_ident.envelope);
579 if (cose_ident.protected_header.gov_msg_created_at > 9'999'999'999)
581 detail::set_gov_error(
583 HTTP_STATUS_BAD_REQUEST,
584 ccf::errors::InvalidCreatedAt,
585 "Header parameter created_at value is too large");
589 const auto created_at_str = fmt::format(
590 "{:0>10}", cose_ident.protected_header.gov_msg_created_at);
592 const auto subtime_result =
594 ctx.tx, created_at_str, request_digest, proposal_id);
595 switch (subtime_result.status)
599 detail::set_gov_error(
601 HTTP_STATUS_BAD_REQUEST,
602 ccf::errors::ProposalCreatedTooLongAgo,
604 "Proposal created too long ago, created_at must be greater "
606 subtime_result.info));
612 detail::set_gov_error(
614 HTTP_STATUS_BAD_REQUEST,
615 ccf::errors::ProposalReplay,
617 "Proposal submission replay, already exists as proposal {}",
618 subtime_result.info));
629 throw std::runtime_error(
630 "Invalid ProposalSubmissionResult::Status value");
644 constitution.value());
654 detail::set_gov_error(
656 HTTP_STATUS_INTERNAL_SERVER_ERROR,
657 ccf::errors::InternalError,
658 fmt::format(
"{}", proposal_info.
failure));
663 proposal_id, proposal_info);
665 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
673 "/members/proposals:create",
680 auto withdraw_proposal = [&](
auto& ctx,
ApiVersion api_version) {
687 const auto& cose_ident =
688 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
692 cose_ident, ctx.rpc_ctx, proposal_id))
697 auto proposal_info_handle =
698 ctx.tx.template rw<ccf::jsgov::ProposalInfoMap>(
699 jsgov::Tables::PROPOSALS_INFO);
702 auto proposal_info = proposal_info_handle->get(proposal_id);
703 if (!proposal_info.has_value())
708 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
713 const auto member_id = cose_ident.member_id;
714 if (member_id != proposal_info->proposer_id)
716 detail::set_gov_error(
718 HTTP_STATUS_FORBIDDEN,
719 ccf::errors::AuthorizationFailed,
721 "Proposal {} can only be withdrawn by proposer {}, not caller "
724 proposal_info->proposer_id,
735 detail::set_gov_error(
737 HTTP_STATUS_BAD_REQUEST,
738 ccf::errors::ProposalNotOpen,
740 "Proposal {} is currently in state {} and cannot be withdrawn.",
742 proposal_info->state));
751 proposal_info_handle->put(proposal_id, proposal_info.value());
755 ctx.tx, cose_ident.member_id, cose_ident.envelope);
759 proposal_id, proposal_info.value());
761 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
768 "/members/proposals/{proposalId}:withdraw",
775 auto get_proposal = [&](
auto& ctx,
ApiVersion api_version) {
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())
794 detail::set_gov_error(
796 HTTP_STATUS_NOT_FOUND,
797 ccf::errors::ProposalNotFound,
798 fmt::format(
"Could not find proposal {}.", proposal_id));
803 proposal_id, proposal_info.value());
805 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
812 "/members/proposals/{proposalId}",
819 auto list_proposals = [&](
auto& ctx,
ApiVersion api_version) {
826 auto proposal_info_handle =
827 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
828 jsgov::Tables::PROPOSALS_INFO);
830 auto proposal_list = nlohmann::json::array();
831 proposal_info_handle->foreach(
833 const auto& proposal_id,
const auto& proposal_info) {
835 proposal_id, proposal_info);
836 proposal_list.push_back(api_proposal);
840 auto response_body = nlohmann::json::object();
841 response_body[
"value"] = proposal_list;
843 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
850 "/members/proposals",
857 auto get_actions = [&](
auto& ctx,
ApiVersion api_version) {
870 auto proposal_handle = ctx.tx.template ro<ccf::jsgov::ProposalMap>(
871 jsgov::Tables::PROPOSALS);
873 const auto proposal = proposal_handle->get(proposal_id);
874 if (!proposal.has_value())
876 detail::set_gov_error(
878 HTTP_STATUS_NOT_FOUND,
879 ccf::errors::ProposalNotFound,
880 fmt::format(
"Could not find proposal {}.", proposal_id));
884 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
885 ctx.rpc_ctx->set_response_body(proposal.value());
893 "/members/proposals/{proposalId}/actions",
901 auto submit_ballot = [&](
910 const auto& cose_ident =
911 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
915 cose_ident, ctx.
rpc_ctx, proposal_id))
922 cose_ident, ctx.
rpc_ctx, member_id))
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())
934 detail::set_gov_error(
936 HTTP_STATUS_NOT_FOUND,
937 ccf::errors::ProposalNotFound,
938 fmt::format(
"Could not find proposal {}.", proposal_id));
944 detail::set_gov_error(
946 HTTP_STATUS_BAD_REQUEST,
947 ccf::errors::ProposalNotOpen,
949 "Proposal {} is currently in state {} - only {} proposals "
952 proposal_info->state,
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())
963 detail::set_gov_error(
965 HTTP_STATUS_NOT_FOUND,
966 ccf::errors::ProposalNotFound,
967 fmt::format(
"Could not find proposal {}.", proposal_id));
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())
976 detail::set_gov_error(
978 HTTP_STATUS_BAD_REQUEST,
979 ccf::errors::InvalidInput,
980 "Signed request body is not a JSON object containing required "
981 "string field \"ballot\"");
986 const auto constitution =
987 ctx.
tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
989 if (!constitution.has_value())
991 detail::set_gov_error(
993 HTTP_STATUS_INTERNAL_SERVER_ERROR,
994 ccf::errors::InternalError,
995 "No constitution is set - ballots cannot be evaluated");
999 const auto ballot = ballot_it.value().get<std::string>();
1001 const auto info_ballot_it = proposal_info->ballots.find(member_id);
1002 if (info_ballot_it != proposal_info->ballots.end())
1006 if (info_ballot_it->second == ballot)
1009 proposal_id, proposal_info.value());
1011 ctx.
rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1015 detail::set_gov_error(
1017 HTTP_STATUS_BAD_REQUEST,
1018 ccf::errors::VoteAlreadyExists,
1020 "Different ballot already submitted by {} for {}.",
1027 proposal_info->ballots.insert_or_assign(
1028 info_ballot_it, member_id, ballot_it.
value().get<std::string>());
1031 ctx.
tx, cose_ident.member_id, cose_ident.envelope);
1039 proposal_info.value(),
1040 constitution.value());
1048 detail::set_gov_error(
1050 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1051 ccf::errors::InternalError,
1052 fmt::format(
"{}", proposal_info->failure));
1057 proposal_id, proposal_info.value());
1059 ctx.
rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1066 "/members/proposals/{proposalId}/ballots/{memberId}:submit",
1073 auto get_ballot = [&](
auto& ctx,
ApiVersion api_version) {
1074 switch (api_version)
1093 auto proposal_info_handle =
1094 ctx.
tx.template ro<ccf::jsgov::ProposalInfoMap>(
1095 ccf::jsgov::Tables::PROPOSALS_INFO);
1099 auto proposal_info = proposal_info_handle->get(proposal_id);
1100 if (!proposal_info.has_value())
1102 detail::set_gov_error(
1104 HTTP_STATUS_NOT_FOUND,
1105 ccf::errors::ProposalNotFound,
1106 fmt::format(
"Proposal {} does not exist.", proposal_id));
1111 auto ballot_it = proposal_info->ballots.find(member_id);
1112 if (ballot_it == proposal_info->ballots.end())
1114 detail::set_gov_error(
1116 HTTP_STATUS_NOT_FOUND,
1117 ccf::errors::VoteNotFound,
1119 "Member {} has not voted for proposal {}.",
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);
1137 "/members/proposals/{proposalId}/ballots/{memberId}",