136 const std::span<const uint8_t>& proposal_bytes,
138 const std::string& constitution)
145 const std::string_view proposal{
146 (
const char*)proposal_bytes.data(), proposal_bytes.size()};
148 auto proposal_info_handle = tx.template rw<ccf::jsgov::ProposalInfoMap>(
149 jsgov::Tables::PROPOSALS_INFO);
152 for (
const auto& [mid, mb] : proposal_info.
ballots)
156 auto ballot_func = js_context.get_exported_function(
160 "{}[{}].ballots[{}]",
161 ccf::jsgov::Tables::PROPOSALS_INFO,
165 std::vector<js::core::JSWrappedValue> argv = {
166 js_context.new_string(proposal),
169 auto val = js_context.call_with_rt_options(
175 if (!val.is_exception())
177 votes[mid] = val.is_true();
181 auto [reason, trace] = js_context.error_message();
183 if (js_context.interrupt_data.request_timed_out)
185 reason =
"Operation took too long to complete.";
198 auto resolve_func = js_context.get_exported_function(
201 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
203 std::vector<js::core::JSWrappedValue> argv;
204 argv.push_back(js_context.new_string(proposal));
209 auto vs = js_context.new_array();
211 for (
auto& [mid, vote] : votes)
213 auto v = JS_NewObject(js_context);
215 JS_NewStringLen(js_context, mid.data(), mid.size());
216 JS_DefinePropertyValueStr(
217 js_context, v,
"member_id", member_id, JS_PROP_C_W_E);
218 auto vote_status = JS_NewBool(js_context, vote);
219 JS_DefinePropertyValueStr(
220 js_context, v,
"vote", vote_status, JS_PROP_C_W_E);
221 JS_DefinePropertyValueUint32(
222 js_context, vs.val, index++, v, JS_PROP_C_W_E);
229 argv.push_back(js_context.new_string(proposal_id));
231 auto val = js_context.call_with_rt_options(
237 if (val.is_exception())
240 auto [reason, trace] = js_context.error_message();
241 if (js_context.interrupt_data.request_timed_out)
243 reason =
"Operation took too long to complete.";
246 fmt::format(
"Failed to resolve(): {}", reason), trace};
250 auto status = js_context.to_str(val).value_or(
"");
254 const std::unordered_map<std::string, ProposalState>
259 const auto it = js_str_to_status.find(status);
260 if (it != js_str_to_status.end())
262 proposal_info.
state = it->second;
269 "resolve() returned invalid status value: \"{}\"", status),
276 proposal_info_handle->put(proposal_id, proposal_info);
287 proposal_info_handle->put(proposal_id, proposal_info);
294 if (gov_effects ==
nullptr)
296 throw std::logic_error(
297 "Unexpected: Could not access GovEffects subsytem");
302 js_context.add_extension(
303 std::make_shared<ccf::js::extensions::NodeExtension>(
304 gov_effects.get(), &tx));
305 js_context.add_extension(
306 std::make_shared<ccf::js::extensions::NetworkExtension>(
308 js_context.add_extension(
309 std::make_shared<ccf::js::extensions::GovEffectsExtension>(&tx));
311 auto apply_func = js_context.get_exported_function(
314 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
316 std::vector<js::core::JSWrappedValue> argv = {
317 js_context.new_string(proposal),
318 js_context.new_string(proposal_id)};
320 auto val = js_context.call_with_rt_options(
326 if (val.is_exception())
329 auto [reason, trace] = js_context.error_message();
330 if (js_context.interrupt_data.request_timed_out)
332 reason =
"Operation took too long to complete.";
335 fmt::format(
"Failed to apply(): {}", reason), trace};
338 proposal_info_handle->put(proposal_id, proposal_info);
393 auto create_proposal = [&](
auto& ctx,
ApiVersion api_version) {
400 const auto& cose_ident =
401 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
403 std::span<const uint8_t> proposal_body = cose_ident.content;
405 std::optional<std::string> constitution;
409 std::vector<uint8_t> request_digest;
411 auto root_at_read = ctx.tx.get_root_at_read_version();
412 if (!root_at_read.has_value())
414 detail::set_gov_error(
416 HTTP_STATUS_INTERNAL_SERVER_ERROR,
417 ccf::errors::InternalError,
418 "Proposal failed to bind to state.");
423 hasher->update_hash(root_at_read.value().h);
426 cose_ident.signature.data(), cose_ident.signature.size());
428 hasher->update_hash(request_digest);
431 proposal_id = proposal_hash.
hex_str();
437 ctx.tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
439 if (!constitution.has_value())
441 detail::set_gov_error(
443 HTTP_STATUS_INTERNAL_SERVER_ERROR,
444 ccf::errors::InternalError,
445 "No constitution is set - proposals cannot be evaluated");
451 auto validate_func = context.get_exported_function(
452 constitution.value(),
454 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
456 auto proposal_arg = context.new_string_len(cose_ident.content);
457 auto validate_result = context.call_with_rt_options(
460 ctx.tx.template ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
465 if (validate_result.is_exception())
467 auto [reason, trace] = context.error_message();
468 if (context.interrupt_data.request_timed_out)
470 reason =
"Operation took too long to complete.";
472 detail::set_gov_error(
474 HTTP_STATUS_INTERNAL_SERVER_ERROR,
475 ccf::errors::InternalError,
477 "Failed to execute validation: {} {}",
479 trace.value_or(
"")));
483 if (!validate_result.is_obj())
485 detail::set_gov_error(
487 HTTP_STATUS_INTERNAL_SERVER_ERROR,
488 ccf::errors::InternalError,
489 "Validation failed to return an object");
493 std::string description;
494 auto desc = validate_result[
"description"];
497 description = context.to_str(desc).value_or(
"");
500 auto valid = validate_result[
"valid"];
501 if (!valid.is_true())
503 detail::set_gov_error(
505 HTTP_STATUS_BAD_REQUEST,
506 ccf::errors::ProposalFailedToValidate,
507 fmt::format(
"Proposal failed to validate: {}", description));
514 auto proposals_handle =
515 ctx.tx.template rw<ccf::jsgov::ProposalMap>(
516 jsgov::Tables::PROPOSALS);
522 if (proposals_handle->has(proposal_id))
524 detail::set_gov_error(
526 HTTP_STATUS_INTERNAL_SERVER_ERROR,
527 ccf::errors::InternalError,
528 "Proposal ID collision.");
531 proposals_handle->put(
532 proposal_id, {proposal_body.begin(), proposal_body.end()});
534 auto proposal_info_handle =
535 ctx.tx.template wo<ccf::jsgov::ProposalInfoMap>(
536 jsgov::Tables::PROPOSALS_INFO);
541 proposal_info_handle->put(proposal_id, proposal_info);
544 ctx.tx, cose_ident.member_id, cose_ident.envelope);
560 if (cose_ident.protected_header.gov_msg_created_at > 9'999'999'999)
562 detail::set_gov_error(
564 HTTP_STATUS_BAD_REQUEST,
565 ccf::errors::InvalidCreatedAt,
566 "Header parameter created_at value is too large");
570 const auto created_at_str = fmt::format(
571 "{:0>10}", cose_ident.protected_header.gov_msg_created_at);
574 std::string min_created_at;
576 const auto subtime_result =
578 ctx.tx, created_at_str, request_digest, proposal_id);
579 switch (subtime_result.status)
583 detail::set_gov_error(
585 HTTP_STATUS_BAD_REQUEST,
586 ccf::errors::ProposalCreatedTooLongAgo,
588 "Proposal created too long ago, created_at must be greater "
590 subtime_result.info));
596 detail::set_gov_error(
598 HTTP_STATUS_BAD_REQUEST,
599 ccf::errors::ProposalReplay,
601 "Proposal submission replay, already exists as proposal {}",
602 subtime_result.info));
613 throw std::runtime_error(
614 "Invalid ProposalSubmissionResult::Status value");
628 constitution.value());
638 detail::set_gov_error(
640 HTTP_STATUS_INTERNAL_SERVER_ERROR,
641 ccf::errors::InternalError,
642 fmt::format(
"{}", proposal_info.
failure));
647 proposal_id, proposal_info);
649 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
657 "/members/proposals:create",
664 auto withdraw_proposal = [&](
auto& ctx,
ApiVersion api_version) {
671 const auto& cose_ident =
672 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
676 cose_ident, ctx.rpc_ctx, proposal_id))
681 auto proposal_info_handle =
682 ctx.tx.template rw<ccf::jsgov::ProposalInfoMap>(
683 jsgov::Tables::PROPOSALS_INFO);
686 auto proposal_info = proposal_info_handle->get(proposal_id);
687 if (!proposal_info.has_value())
692 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
697 const auto member_id = cose_ident.member_id;
698 if (member_id != proposal_info->proposer_id)
700 detail::set_gov_error(
702 HTTP_STATUS_FORBIDDEN,
703 ccf::errors::AuthorizationFailed,
705 "Proposal {} can only be withdrawn by proposer {}, not caller "
708 proposal_info->proposer_id,
719 detail::set_gov_error(
721 HTTP_STATUS_BAD_REQUEST,
722 ccf::errors::ProposalNotOpen,
724 "Proposal {} is currently in state {} and cannot be withdrawn.",
726 proposal_info->state));
735 proposal_info_handle->put(proposal_id, proposal_info.value());
739 ctx.tx, cose_ident.member_id, cose_ident.envelope);
743 proposal_id, proposal_info.value());
745 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
752 "/members/proposals/{proposalId}:withdraw",
759 auto get_proposal = [&](
auto& ctx,
ApiVersion api_version) {
772 auto proposal_info_handle =
773 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
774 jsgov::Tables::PROPOSALS_INFO);
775 auto proposal_info = proposal_info_handle->get(proposal_id);
776 if (!proposal_info.has_value())
778 detail::set_gov_error(
780 HTTP_STATUS_NOT_FOUND,
781 ccf::errors::ProposalNotFound,
782 fmt::format(
"Could not find proposal {}.", proposal_id));
787 proposal_id, proposal_info.value());
789 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
796 "/members/proposals/{proposalId}",
803 auto list_proposals = [&](
auto& ctx,
ApiVersion api_version) {
810 auto proposal_info_handle =
811 ctx.tx.template ro<ccf::jsgov::ProposalInfoMap>(
812 jsgov::Tables::PROPOSALS_INFO);
814 auto proposal_list = nlohmann::json::array();
815 proposal_info_handle->foreach(
817 const auto& proposal_id,
const auto& proposal_info) {
819 proposal_id, proposal_info);
820 proposal_list.push_back(api_proposal);
824 auto response_body = nlohmann::json::object();
825 response_body[
"value"] = proposal_list;
827 ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
834 "/members/proposals",
841 auto get_actions = [&](
auto& ctx,
ApiVersion api_version) {
854 auto proposal_handle = ctx.tx.template ro<ccf::jsgov::ProposalMap>(
855 jsgov::Tables::PROPOSALS);
857 const auto proposal = proposal_handle->get(proposal_id);
858 if (!proposal.has_value())
860 detail::set_gov_error(
862 HTTP_STATUS_NOT_FOUND,
863 ccf::errors::ProposalNotFound,
864 fmt::format(
"Could not find proposal {}.", proposal_id));
868 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
869 ctx.rpc_ctx->set_response_body(proposal.value());
877 "/members/proposals/{proposalId}/actions",
893 const auto& cose_ident =
894 ctx.template get_caller<ccf::MemberCOSESign1AuthnIdentity>();
898 cose_ident, ctx.
rpc_ctx, proposal_id))
905 cose_ident, ctx.
rpc_ctx, member_id))
911 auto proposal_info_handle =
912 ctx.
tx.template rw<ccf::jsgov::ProposalInfoMap>(
913 jsgov::Tables::PROPOSALS_INFO);
914 auto proposal_info = proposal_info_handle->get(proposal_id);
915 if (!proposal_info.has_value())
917 detail::set_gov_error(
919 HTTP_STATUS_NOT_FOUND,
920 ccf::errors::ProposalNotFound,
921 fmt::format(
"Could not find proposal {}.", proposal_id));
927 detail::set_gov_error(
929 HTTP_STATUS_BAD_REQUEST,
930 ccf::errors::ProposalNotOpen,
932 "Proposal {} is currently in state {} - only {} proposals "
935 proposal_info->state,
941 auto proposals_handle = ctx.
tx.template ro<ccf::jsgov::ProposalMap>(
942 ccf::jsgov::Tables::PROPOSALS);
943 const auto proposal = proposals_handle->get(proposal_id);
944 if (!proposal.has_value())
946 detail::set_gov_error(
948 HTTP_STATUS_NOT_FOUND,
949 ccf::errors::ProposalNotFound,
950 fmt::format(
"Could not find proposal {}.", proposal_id));
955 const auto params = nlohmann::json::parse(cose_ident.content);
956 const auto ballot_it = params.find(
"ballot");
957 if (ballot_it == params.end() || !ballot_it.value().is_string())
959 detail::set_gov_error(
961 HTTP_STATUS_BAD_REQUEST,
962 ccf::errors::InvalidInput,
963 "Signed request body is not a JSON object containing required "
964 "string field \"ballot\"");
969 const auto constitution =
970 ctx.
tx.template ro<ccf::Constitution>(ccf::Tables::CONSTITUTION)
972 if (!constitution.has_value())
974 detail::set_gov_error(
976 HTTP_STATUS_INTERNAL_SERVER_ERROR,
977 ccf::errors::InternalError,
978 "No constitution is set - ballots cannot be evaluated");
982 const auto ballot = ballot_it.value().get<std::string>();
984 const auto info_ballot_it = proposal_info->ballots.find(member_id);
985 if (info_ballot_it != proposal_info->ballots.end())
989 if (info_ballot_it->second == ballot)
991 const auto response_body =
993 proposal_id, proposal_info.value());
995 ctx.
rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1000 detail::set_gov_error(
1002 HTTP_STATUS_BAD_REQUEST,
1003 ccf::errors::VoteAlreadyExists,
1005 "Different ballot already submitted by {} for {}.",
1013 proposal_info->ballots.insert_or_assign(
1014 info_ballot_it, member_id, ballot_it.
value().get<std::string>());
1017 ctx.
tx, cose_ident.member_id, cose_ident.envelope);
1025 proposal_info.value(),
1026 constitution.value());
1034 detail::set_gov_error(
1036 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1037 ccf::errors::InternalError,
1038 fmt::format(
"{}", proposal_info->failure));
1043 proposal_id, proposal_info.value());
1045 ctx.
rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
1052 "/members/proposals/{proposalId}/ballots/{memberId}:submit",
1059 auto get_ballot = [&](
auto& ctx,
ApiVersion api_version) {
1060 switch (api_version)
1079 auto proposal_info_handle =
1080 ctx.
tx.template ro<ccf::jsgov::ProposalInfoMap>(
1081 ccf::jsgov::Tables::PROPOSALS_INFO);
1085 auto proposal_info = proposal_info_handle->get(proposal_id);
1086 if (!proposal_info.has_value())
1088 detail::set_gov_error(
1090 HTTP_STATUS_NOT_FOUND,
1091 ccf::errors::ProposalNotFound,
1092 fmt::format(
"Proposal {} does not exist.", proposal_id));
1097 auto ballot_it = proposal_info->ballots.find(member_id);
1098 if (ballot_it == proposal_info->ballots.end())
1100 detail::set_gov_error(
1102 HTTP_STATUS_NOT_FOUND,
1103 ccf::errors::VoteNotFound,
1105 "Member {} has not voted for proposal {}.",
1112 ctx.
rpc_ctx->set_response_status(HTTP_STATUS_OK);
1113 ctx.
rpc_ctx->set_response_body(std::move(ballot_it->second));
1114 ctx.
rpc_ctx->set_response_header(
1115 http::headers::CONTENT_TYPE,
1116 http::headervalues::contenttype::JAVASCRIPT);
1123 "/members/proposals/{proposalId}/ballots/{memberId}",