From f5a86270df3fdc8326a1f1849d9c1c65df70146e Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 1 Oct 2026 17:28:38 +0100 Subject: [PATCH 1/9] Move gov ack handlers out of init_ack_handlers Move the three endpoint handler bodies (get_state_digest, update_state_digest, ack_state_digest) out of the lambdas in init_ack_handlers() into named function templates in a nested detail namespace, registered via thin forwarding lambdas. This removes all the cognitive complexity from init_ack_handlers() (now 0), which previously inherited it from its inline lambda bodies (up to 69 combined, mostly from ack_state_digest at ~34 standalone), letting the readability-function-cognitive-complexity NOLINTNEXTLINE be dropped. Pure reshuffle: handler bodies are token-identical to the former lambda bodies (captures become explicit parameters where needed, e.g. ShareManager& for ack_state_digest); registration call sites, paths, verbs, adapters, auth policies, and install() chains are unchanged. Part of #7358. Layer 1 of a stack of refactors applying this same pattern to the other init_*_handlers functions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/node/gov/handlers/acks.h | 83 ++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/src/node/gov/handlers/acks.h b/src/node/gov/handlers/acks.h index b5df903116f..75749de9723 100644 --- a/src/node/gov/handlers/acks.h +++ b/src/node/gov/handlers/acks.h @@ -24,13 +24,17 @@ namespace ccf::gov::endpoints StateDigest, member_id, "memberId", state_digest, "stateDigest"); } - // NOLINTNEXTLINE(readability-function-cognitive-complexity) - inline void init_ack_handlers( - ccf::BaseEndpointRegistry& registry, - NetworkState& /*network*/, - ShareManager& share_manager) + namespace detail { - auto get_state_digest = [&](auto& ctx, ApiVersion api_version) { + // Handler bodies are moved out of init_ack_handlers() into named + // functions here, so that their cognitive complexity is no longer + // attributed to the registration function. Each takes the endpoint + // context by template parameter (the same genericity as the former + // lambda's `auto& ctx`), plus any previously-captured dependencies as + // explicit parameters, and is registered via a thin forwarding lambda. + template + inline void get_state_digest(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -74,18 +78,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/members/state-digests/{memberId}", - HTTP_GET, - api_version_adapter(get_state_digest), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get a member's state digest") - .install(); + } - auto update_state_digest = [&](auto& ctx, ApiVersion api_version) { + template + inline void update_state_digest(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -162,18 +159,12 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_endpoint( - "/members/state-digests/{memberId}:update", - HTTP_POST, - api_version_adapter(update_state_digest), - detail::member_sig_only_policies("state_digest")) - .set_auto_schema() - .set_openapi_summary("Update a member's state digest") - .install(); + } - auto ack_state_digest = [&](auto& ctx, ApiVersion api_version) { + template + inline void ack_state_digest( + Ctx& ctx, ApiVersion api_version, ShareManager& share_manager) + { switch (api_version) { case ApiVersion::preview_v1: @@ -358,6 +349,42 @@ namespace ccf::gov::endpoints break; } } + } + } + + inline void init_ack_handlers( + ccf::BaseEndpointRegistry& registry, + NetworkState& /*network*/, + ShareManager& share_manager) + { + auto get_state_digest = [](auto& ctx, ApiVersion api_version) { + detail::get_state_digest(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/members/state-digests/{memberId}", + HTTP_GET, + api_version_adapter(get_state_digest), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get a member's state digest") + .install(); + + auto update_state_digest = [](auto& ctx, ApiVersion api_version) { + detail::update_state_digest(ctx, api_version); + }; + registry + .make_endpoint( + "/members/state-digests/{memberId}:update", + HTTP_POST, + api_version_adapter(update_state_digest), + detail::member_sig_only_policies("state_digest")) + .set_auto_schema() + .set_openapi_summary("Update a member's state digest") + .install(); + + auto ack_state_digest = [&](auto& ctx, ApiVersion api_version) { + detail::ack_state_digest(ctx, api_version, share_manager); }; registry .make_endpoint( From a1973ea87d598ae12cd61d8a2a8ff14aebb0e38b Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 1 Oct 2026 17:47:08 +0100 Subject: [PATCH 2/9] Rewrite detail namespace comment to describe design, not edit history Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/node/gov/handlers/acks.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/node/gov/handlers/acks.h b/src/node/gov/handlers/acks.h index 75749de9723..e2a842bfc59 100644 --- a/src/node/gov/handlers/acks.h +++ b/src/node/gov/handlers/acks.h @@ -26,12 +26,9 @@ namespace ccf::gov::endpoints namespace detail { - // Handler bodies are moved out of init_ack_handlers() into named - // functions here, so that their cognitive complexity is no longer - // attributed to the registration function. Each takes the endpoint - // context by template parameter (the same genericity as the former - // lambda's `auto& ctx`), plus any previously-captured dependencies as - // explicit parameters, and is registered via a thin forwarding lambda. + // Endpoint handlers registered by init_ack_handlers(), via forwarding + // lambdas. Kept out of the registration function so each handler's + // complexity is measured on its own. template inline void get_state_digest(Ctx& ctx, ApiVersion api_version) { From 3d4187275b090ccc146b477deedf1b526139cec6 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 1 Oct 2026 18:27:29 +0100 Subject: [PATCH 3/9] Move gov service state handlers out of init_service_state_handlers Extract each of the 13 endpoint handler lambdas in init_service_state_handlers() into named functions in a detail namespace, following the convention established for init_ack_handlers() in acks.h. The registration function keeps thin forwarding lambdas, so each handler's cognitive complexity is measured on its own instead of being rolled up into one large function. Pure reshuffle: handler bodies and endpoint registration (paths, verbs, adapters, auth policies, chained set_* calls, install() calls, registration order) are unchanged. Removes the NOLINTNEXTLINE(readability-function-cognitive-complexity) suppression, since the registration function's own complexity is now effectively 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/node/gov/handlers/service_state.h | 435 +++++++++++++++----------- 1 file changed, 259 insertions(+), 176 deletions(-) diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index a595fb52460..316a73fd06b 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -418,10 +418,14 @@ namespace ccf::gov::endpoints return node; } - // NOLINTNEXTLINE(readability-function-cognitive-complexity) - inline void init_service_state_handlers(ccf::BaseEndpointRegistry& registry) + namespace detail { - auto get_constitution = [&](auto& ctx, ApiVersion api_version) { + // Endpoint handlers registered by init_service_state_handlers(), via + // forwarding lambdas. Kept out of the registration function so each + // handler's complexity is measured on its own. + template + inline void get_constitution(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -451,18 +455,12 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/constitution", - HTTP_GET, - api_version_adapter(get_constitution), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get the service constitution") - .install(); + } - auto get_service_info = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_service_info( + Ctx& ctx, ApiVersion api_version, ccf::BaseEndpointRegistry& registry) + { switch (api_version) { case ApiVersion::preview_v1: @@ -535,18 +533,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/info", - HTTP_GET, - api_version_adapter(get_service_info), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get service information") - .install(); + } - auto get_javascript_app = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_javascript_app(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -647,20 +638,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/javascript-app", - HTTP_GET, - api_version_adapter(get_javascript_app, ApiVersion::v1), - no_auth_required) - .set_auto_schema() - .add_query_parameter( - "case", ccf::endpoints::QueryParamPresence::OptionalParameter) - .set_openapi_summary("Get the installed JavaScript application") - .install(); + } - auto get_javascript_modules = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_javascript_modules(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -680,80 +662,65 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/javascript-modules", - HTTP_GET, - api_version_adapter(get_javascript_modules, ApiVersion::v1), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("List JavaScript modules") - .install(); + } - auto get_javascript_module_by_name = - [&](auto& ctx, ApiVersion api_version) { - switch (api_version) + template + inline void get_javascript_module_by_name(Ctx& ctx, ApiVersion api_version) + { + switch (api_version) + { + case ApiVersion::preview_v1: + case ApiVersion::v1: + case ApiVersion::Latest: { - case ApiVersion::preview_v1: - case ApiVersion::v1: - case ApiVersion::Latest: + std::string module_name; { - std::string module_name; - { - std::string error; - if (!ccf::endpoints::get_path_param( - ctx.rpc_ctx->get_request_path_params(), - "moduleName", - module_name, - error)) - { - detail::set_gov_error( - ctx.rpc_ctx, - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidResourceName, - std::move(error)); - return; - } - } - - module_name = ::http::url_decode(module_name); - - auto modules_handle = - ctx.tx.template ro(ccf::Tables::MODULES); - auto module = modules_handle->get(module_name); - - if (!module.has_value()) + std::string error; + if (!ccf::endpoints::get_path_param( + ctx.rpc_ctx->get_request_path_params(), + "moduleName", + module_name, + error)) { detail::set_gov_error( ctx.rpc_ctx, - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - fmt::format("Module {} does not exist.", module_name)); + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + std::move(error)); return; } + } + + module_name = ::http::url_decode(module_name); + + auto modules_handle = + ctx.tx.template ro(ccf::Tables::MODULES); + auto module = modules_handle->get(module_name); - // Return raw JS module content in body - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_body(std::move(module.value())); - ctx.rpc_ctx->set_response_header( - ccf::http::headers::CONTENT_TYPE, - http::headervalues::contenttype::JAVASCRIPT); + if (!module.has_value()) + { + detail::set_gov_error( + ctx.rpc_ctx, + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + fmt::format("Module {} does not exist.", module_name)); return; } + + // Return raw JS module content in body + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_body(std::move(module.value())); + ctx.rpc_ctx->set_response_header( + ccf::http::headers::CONTENT_TYPE, + http::headervalues::contenttype::JAVASCRIPT); + return; } - }; - registry - .make_read_only_endpoint( - "/service/javascript-modules/{moduleName}", - HTTP_GET, - api_version_adapter(get_javascript_module_by_name, ApiVersion::v1), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get a JavaScript module") - .install(); + } + } - auto get_join_policy = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_join_policy(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -858,18 +825,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/join-policy", - HTTP_GET, - api_version_adapter(get_join_policy), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get the service join policy") - .install(); + } - auto get_jwk = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_jwk(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -937,18 +897,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/jwk", - HTTP_GET, - api_version_adapter(get_jwk), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get accepted JWT issuers and keys") - .install(); + } - auto get_members = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_members(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -980,18 +933,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/members", - HTTP_GET, - api_version_adapter(get_members), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("List consortium members") - .install(); + } - auto get_member_by_id = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_member_by_id(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -1033,18 +979,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/members/{memberId}", - HTTP_GET, - api_version_adapter(get_member_by_id), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get a consortium member") - .install(); + } - auto get_users = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_users(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -1069,18 +1008,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/users", - HTTP_GET, - api_version_adapter(get_users), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("List application users") - .install(); + } - auto get_user_by_id = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_user_by_id(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -1117,18 +1049,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/users/{userId}", - HTTP_GET, - api_version_adapter(get_user_by_id), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get an application user") - .install(); + } - auto get_nodes = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_nodes(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -1154,18 +1079,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/service/nodes", - HTTP_GET, - api_version_adapter(get_nodes), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("List service nodes") - .install(); + } - auto get_node_by_id = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_node_by_id(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -1201,6 +1119,171 @@ namespace ccf::gov::endpoints return; } } + } + } + + inline void init_service_state_handlers(ccf::BaseEndpointRegistry& registry) + { + auto get_constitution = [](auto& ctx, ApiVersion api_version) { + detail::get_constitution(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/constitution", + HTTP_GET, + api_version_adapter(get_constitution), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get the service constitution") + .install(); + + auto get_service_info = [&](auto& ctx, ApiVersion api_version) { + detail::get_service_info(ctx, api_version, registry); + }; + registry + .make_read_only_endpoint( + "/service/info", + HTTP_GET, + api_version_adapter(get_service_info), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get service information") + .install(); + + auto get_javascript_app = [](auto& ctx, ApiVersion api_version) { + detail::get_javascript_app(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/javascript-app", + HTTP_GET, + api_version_adapter(get_javascript_app, ApiVersion::v1), + no_auth_required) + .set_auto_schema() + .add_query_parameter( + "case", ccf::endpoints::QueryParamPresence::OptionalParameter) + .set_openapi_summary("Get the installed JavaScript application") + .install(); + + auto get_javascript_modules = [](auto& ctx, ApiVersion api_version) { + detail::get_javascript_modules(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/javascript-modules", + HTTP_GET, + api_version_adapter(get_javascript_modules, ApiVersion::v1), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("List JavaScript modules") + .install(); + + auto get_javascript_module_by_name = [](auto& ctx, ApiVersion api_version) { + detail::get_javascript_module_by_name(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/javascript-modules/{moduleName}", + HTTP_GET, + api_version_adapter(get_javascript_module_by_name, ApiVersion::v1), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get a JavaScript module") + .install(); + + auto get_join_policy = [](auto& ctx, ApiVersion api_version) { + detail::get_join_policy(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/join-policy", + HTTP_GET, + api_version_adapter(get_join_policy), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get the service join policy") + .install(); + + auto get_jwk = [](auto& ctx, ApiVersion api_version) { + detail::get_jwk(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/jwk", + HTTP_GET, + api_version_adapter(get_jwk), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get accepted JWT issuers and keys") + .install(); + + auto get_members = [](auto& ctx, ApiVersion api_version) { + detail::get_members(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/members", + HTTP_GET, + api_version_adapter(get_members), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("List consortium members") + .install(); + + auto get_member_by_id = [](auto& ctx, ApiVersion api_version) { + detail::get_member_by_id(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/members/{memberId}", + HTTP_GET, + api_version_adapter(get_member_by_id), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get a consortium member") + .install(); + + auto get_users = [](auto& ctx, ApiVersion api_version) { + detail::get_users(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/users", + HTTP_GET, + api_version_adapter(get_users), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("List application users") + .install(); + + auto get_user_by_id = [](auto& ctx, ApiVersion api_version) { + detail::get_user_by_id(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/users/{userId}", + HTTP_GET, + api_version_adapter(get_user_by_id), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get an application user") + .install(); + + auto get_nodes = [](auto& ctx, ApiVersion api_version) { + detail::get_nodes(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/service/nodes", + HTTP_GET, + api_version_adapter(get_nodes), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("List service nodes") + .install(); + + auto get_node_by_id = [](auto& ctx, ApiVersion api_version) { + detail::get_node_by_id(ctx, api_version); }; registry .make_read_only_endpoint( From cd173f2dc3d62f2dbd26c0d1e4e0f99e583d3631 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 1 Oct 2026 19:06:33 +0100 Subject: [PATCH 4/9] Move gov proposal handlers out of init_proposals_handlers Move the seven endpoint handler bodies (create_proposal, withdraw_proposal, get_proposal, list_proposals, get_actions, submit_ballot, get_ballot) out of the lambdas in init_proposals_handlers() into named functions in a nested detail namespace, registered via thin forwarding lambdas. This removes all the cognitive complexity from init_proposals_handlers() (now 0), which previously inherited it from its inline lambda bodies (127 combined, mostly from create_proposal at 29 and submit_ballot at 23 standalone), letting the readability-function-cognitive-complexity NOLINTNEXTLINE be dropped. submit_ballot is not templated on Ctx, unlike the other six: its original lambda took a concrete ccf::endpoints::EndpointContext&, so its body relies on non-dependent name lookup that a template parameter would turn into dependent names requiring '.template' disambiguators. Pure reshuffle: handler bodies are token-identical to the former lambda bodies (captures become explicit parameters where needed, e.g. NetworkState& and AbstractNodeContext& for create_proposal and submit_ballot); registration call sites, paths, verbs, adapters, auth policies, and install() chains are unchanged. Part of #7358. Layer 3 of a stack of refactors applying this same pattern to the other init_*_handlers functions, on top of #8486. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/node/gov/handlers/proposals.h | 217 +++++++++++++++++++----------- 1 file changed, 138 insertions(+), 79 deletions(-) diff --git a/src/node/gov/handlers/proposals.h b/src/node/gov/handlers/proposals.h index 6b5f2437d67..55ecbf634e2 100644 --- a/src/node/gov/handlers/proposals.h +++ b/src/node/gov/handlers/proposals.h @@ -469,14 +469,18 @@ namespace ccf::gov::endpoints } } - // NOLINTNEXTLINE(readability-function-cognitive-complexity) - inline void init_proposals_handlers( - ccf::BaseEndpointRegistry& registry, - NetworkState& network, - ccf::AbstractNodeContext& node_context) + namespace detail { - //// implementation of TSP interface Proposals - auto create_proposal = [&](auto& ctx, ApiVersion api_version) { + // Endpoint handlers registered by init_proposals_handlers(), via + // forwarding lambdas. Kept out of the registration function so each + // handler's complexity is measured on its own. + template + inline void create_proposal( + Ctx& ctx, + ApiVersion api_version, + ccf::AbstractNodeContext& node_context, + ccf::NetworkState& network) + { switch (api_version) { case ApiVersion::preview_v1: @@ -762,18 +766,11 @@ namespace ccf::gov::endpoints } } } - }; - registry - .make_endpoint( - "/members/proposals:create", - HTTP_POST, - api_version_adapter(create_proposal), - detail::active_member_sig_only_policies("proposal")) - .set_auto_schema() - .set_openapi_summary("Create a governance proposal") - .install(); + } - auto withdraw_proposal = [&](auto& ctx, ApiVersion api_version) { + template + inline void withdraw_proposal(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -858,20 +855,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_endpoint( - "/members/proposals/{proposalId}:withdraw", - HTTP_POST, - api_version_adapter(withdraw_proposal), - detail::active_member_sig_only_policies("withdrawal")) - .set_auto_schema() - .add_openapi_response( - HTTP_STATUS_NO_CONTENT, "The proposal no longer exists.") - .set_openapi_summary("Withdraw a governance proposal") - .install(); + } - auto get_proposal = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_proposal(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -905,18 +893,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/members/proposals/{proposalId}", - HTTP_GET, - api_version_adapter(get_proposal), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get a governance proposal") - .install(); + } - auto list_proposals = [&](auto& ctx, ApiVersion api_version) { + template + inline void list_proposals(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -941,18 +922,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_read_only_endpoint( - "/members/proposals", - HTTP_GET, - api_version_adapter(list_proposals), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("List governance proposals") - .install(); + } - auto get_actions = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_actions(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -987,21 +961,18 @@ namespace ccf::gov::endpoints break; } } - }; - registry - .make_read_only_endpoint( - "/members/proposals/{proposalId}/actions", - HTTP_GET, - api_version_adapter(get_actions), - no_auth_required) - .set_auto_schema() - .set_openapi_summary("Get a proposal's actions") - .install(); + } - //// implementation of TSP interface Ballots - auto submit_ballot = [&]( - ccf::endpoints::EndpointContext& ctx, - ApiVersion api_version) { + // Not templated on Ctx, unlike the other handlers here: the original + // lambda took a concrete ccf::endpoints::EndpointContext&, so its body + // relies on non-dependent lookup (e.g. ballot_it.value().get<...>()) + // that a template parameter would turn into dependent names. + inline void submit_ballot( + ccf::endpoints::EndpointContext& ctx, + ApiVersion api_version, + ccf::AbstractNodeContext& node_context, + ccf::NetworkState& network) + { switch (api_version) { case ApiVersion::preview_v1: @@ -1161,18 +1132,11 @@ namespace ccf::gov::endpoints return; } } - }; - registry - .make_endpoint( - "/members/proposals/{proposalId}/ballots/{memberId}:submit", - HTTP_POST, - api_version_adapter(submit_ballot), - detail::active_member_sig_only_policies("ballot")) - .set_auto_schema() - .set_openapi_summary("Submit a ballot") - .install(); + } - auto get_ballot = [&](auto& ctx, ApiVersion api_version) { + template + inline void get_ballot(Ctx& ctx, ApiVersion api_version) + { switch (api_version) { case ApiVersion::preview_v1: @@ -1233,6 +1197,101 @@ namespace ccf::gov::endpoints return; } } + } + } + + inline void init_proposals_handlers( + ccf::BaseEndpointRegistry& registry, + NetworkState& network, + ccf::AbstractNodeContext& node_context) + { + //// implementation of TSP interface Proposals + auto create_proposal = [&network, + &node_context](auto& ctx, ApiVersion api_version) { + detail::create_proposal(ctx, api_version, node_context, network); + }; + registry + .make_endpoint( + "/members/proposals:create", + HTTP_POST, + api_version_adapter(create_proposal), + detail::active_member_sig_only_policies("proposal")) + .set_auto_schema() + .set_openapi_summary("Create a governance proposal") + .install(); + + auto withdraw_proposal = [](auto& ctx, ApiVersion api_version) { + detail::withdraw_proposal(ctx, api_version); + }; + registry + .make_endpoint( + "/members/proposals/{proposalId}:withdraw", + HTTP_POST, + api_version_adapter(withdraw_proposal), + detail::active_member_sig_only_policies("withdrawal")) + .set_auto_schema() + .add_openapi_response( + HTTP_STATUS_NO_CONTENT, "The proposal no longer exists.") + .set_openapi_summary("Withdraw a governance proposal") + .install(); + + auto get_proposal = [](auto& ctx, ApiVersion api_version) { + detail::get_proposal(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/members/proposals/{proposalId}", + HTTP_GET, + api_version_adapter(get_proposal), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get a governance proposal") + .install(); + + auto list_proposals = [](auto& ctx, ApiVersion api_version) { + detail::list_proposals(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/members/proposals", + HTTP_GET, + api_version_adapter(list_proposals), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("List governance proposals") + .install(); + + auto get_actions = [](auto& ctx, ApiVersion api_version) { + detail::get_actions(ctx, api_version); + }; + registry + .make_read_only_endpoint( + "/members/proposals/{proposalId}/actions", + HTTP_GET, + api_version_adapter(get_actions), + no_auth_required) + .set_auto_schema() + .set_openapi_summary("Get a proposal's actions") + .install(); + + //// implementation of TSP interface Ballots + auto submit_ballot = [&network, &node_context]( + ccf::endpoints::EndpointContext& ctx, + ApiVersion api_version) { + detail::submit_ballot(ctx, api_version, node_context, network); + }; + registry + .make_endpoint( + "/members/proposals/{proposalId}/ballots/{memberId}:submit", + HTTP_POST, + api_version_adapter(submit_ballot), + detail::active_member_sig_only_policies("ballot")) + .set_auto_schema() + .set_openapi_summary("Submit a ballot") + .install(); + + auto get_ballot = [](auto& ctx, ApiVersion api_version) { + detail::get_ballot(ctx, api_version); }; registry .make_read_only_endpoint( @@ -1244,4 +1303,4 @@ namespace ccf::gov::endpoints .set_openapi_summary("Get a member's ballot") .install(); } -} \ No newline at end of file +} From 85bb0f8a813578bc7c29bf0f604cc47a04a15f7c Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 1 Oct 2026 19:52:43 +0100 Subject: [PATCH 5/9] Move file serving handlers out of init_file_serving_handlers init_file_serving_handlers() in file_serving_handlers.h had a cognitive complexity of 66, entirely from its 4 inline handler lambdas (find_snapshot, find_chunk, get_snapshot, get_ledger_chunk). Move each lambda's body into a named function in a ccf::node::detail namespace, keeping thin forwarding lambdas in init_file_serving_handlers() for registration. Remove the now-unneeded NOLINTNEXTLINE. fill_range_response_from_file() is untouched; its own complexity is handled separately. Moving find_chunk, get_snapshot and get_ledger_chunk out of their lambdas exposed 3 pre-existing trailing `return;` statements as flagged by readability-function-cognitive-complexity's sibling check readability-redundant-control-flow (which, like bugprone-unchecked-optional-access, does not look inside lambda bodies). These are removed as a minimal, behaviour-preserving fix. Part of #7358 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/node/rpc/file_serving_handlers.h | 245 +++++++++++++++------------ 1 file changed, 140 insertions(+), 105 deletions(-) diff --git a/src/node/rpc/file_serving_handlers.h b/src/node/rpc/file_serving_handlers.h index 7b1fdfab71f..f1a5ddbbad9 100644 --- a/src/node/rpc/file_serving_handlers.h +++ b/src/node/rpc/file_serving_handlers.h @@ -548,13 +548,19 @@ namespace ccf::node } } - // NOLINTNEXTLINE(readability-function-cognitive-complexity) - static void init_file_serving_handlers( - ccf::BaseEndpointRegistry& registry, ccf::AbstractNodeContext& node_context) + namespace detail { - static constexpr auto file_since_param_key = "since"; - - auto find_snapshot = [&](ccf::endpoints::ReadOnlyEndpointContext& ctx) { + // Endpoint handlers registered by init_file_serving_handlers(), via + // forwarding lambdas. Kept out of the registration function so each + // handler's complexity is measured on its own. Not templated on Ctx: + // each took a concrete context type in the original lambda, and their + // bodies rely on non-dependent lookups that would become dependent + // names under a template. + static void find_snapshot( + ccf::endpoints::ReadOnlyEndpointContext& ctx, + ccf::AbstractNodeContext& node_context, + const char* file_since_param_key) + { size_t latest_idx = 0; { // Get latest_idx from query param, if present @@ -663,34 +669,14 @@ namespace ccf::node ctx.rpc_ctx->set_response_header( ccf::http::headers::LOCATION, redirect_url); ctx.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT); - }; - registry - .make_read_only_endpoint( - "/snapshot", HTTP_HEAD, find_snapshot, no_auth_required) - .set_forwarding_required(endpoints::ForwardingRequired::Never) - .add_query_parameter( - file_since_param_key, ccf::endpoints::OptionalParameter) - .add_openapi_response( - HTTP_STATUS_PERMANENT_REDIRECT, "Redirect to the selected snapshot.") - .add_openapi_response( - HTTP_STATUS_NOT_FOUND, "No matching snapshot is available.") - .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) - .install(); - registry - .make_read_only_endpoint( - "/snapshot", HTTP_GET, find_snapshot, no_auth_required) - .set_forwarding_required(endpoints::ForwardingRequired::Never) - .add_query_parameter( - file_since_param_key, ccf::endpoints::OptionalParameter) - .add_openapi_response( - HTTP_STATUS_PERMANENT_REDIRECT, "Redirect to the selected snapshot.") - .add_openapi_response( - HTTP_STATUS_NOT_FOUND, "No matching snapshot is available.") - .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) - .install(); + } // Find a ledger chunk that includes the since value - auto find_chunk = [&](ccf::endpoints::ReadOnlyEndpointContext& ctx) { + static void find_chunk( + ccf::endpoints::ReadOnlyEndpointContext& ctx, + ccf::AbstractNodeContext& node_context, + const char* file_since_param_key) + { size_t since_idx = 0; { // Get since_idx from query param, if present @@ -838,46 +824,12 @@ namespace ccf::node ccf::errors::ResourceNotFound, fmt::format( "This node has no ledger chunk including index {}", since_idx)); - return; - }; - registry - .make_read_only_endpoint( - "/ledger_chunk", HTTP_HEAD, find_chunk, no_auth_required) - .set_forwarding_required(endpoints::ForwardingRequired::Never) - .add_query_parameter( - file_since_param_key, ccf::endpoints::RequiredParameter) - .add_openapi_response( - HTTP_STATUS_PERMANENT_REDIRECT, - "Redirect to the selected ledger chunk.") - .add_openapi_response( - HTTP_STATUS_NOT_FOUND, "No matching ledger chunk is available.") - .require_operator_feature(endpoints::OperatorFeature::LedgerChunkRead) - .set_openapi_summary("Ledger chunk metadata") - .set_openapi_description( - "Redirect to the corresponding /node/ledger_chunk/{chunk_name} " - "endpoint for the ledger chunk including the sequence number specified " - "in the 'since' query parameter.") - .install(); - registry - .make_read_only_endpoint( - "/ledger_chunk", HTTP_GET, find_chunk, no_auth_required) - .set_forwarding_required(endpoints::ForwardingRequired::Never) - .add_query_parameter( - file_since_param_key, ccf::endpoints::RequiredParameter) - .add_openapi_response( - HTTP_STATUS_PERMANENT_REDIRECT, - "Redirect to the selected ledger chunk.") - .add_openapi_response( - HTTP_STATUS_NOT_FOUND, "No matching ledger chunk is available.") - .require_operator_feature(endpoints::OperatorFeature::LedgerChunkRead) - .set_openapi_summary("Download ledger chunk") - .set_openapi_description( - "Redirect to the corresponding /node/ledger_chunk/{chunk_name} " - "endpoint for the ledger chunk including the sequence number specified " - "in the 'since' query parameter.") - .install(); + } - auto get_snapshot = [&](ccf::endpoints::CommandEndpointContext& ctx) { + static void get_snapshot( + ccf::endpoints::CommandEndpointContext& ctx, + ccf::AbstractNodeContext& node_context) + { auto node_configuration_subsystem = get_node_configuration_subsystem(node_context, ctx); if (node_configuration_subsystem == nullptr) @@ -923,40 +875,12 @@ namespace ccf::node ccf::http::headers::CCF_SNAPSHOT_NAME, snapshot_name); fill_range_response_from_file(ctx, f); - return; - }; - registry - .make_command_endpoint( - "/snapshot/{snapshot_name}", HTTP_HEAD, get_snapshot, no_auth_required) - .set_forwarding_required(endpoints::ForwardingRequired::Never) - .add_openapi_response( - HTTP_STATUS_OK, "Metadata for the requested snapshot.") - .add_openapi_response( - HTTP_STATUS_PARTIAL_CONTENT, - "Metadata for the requested snapshot range.") - .add_openapi_response( - HTTP_STATUS_NOT_MODIFIED, "The requested snapshot has not changed.") - .add_openapi_response( - HTTP_STATUS_NOT_FOUND, "The requested snapshot is not available.") - .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) - .install(); - registry - .make_command_endpoint( - "/snapshot/{snapshot_name}", HTTP_GET, get_snapshot, no_auth_required) - .set_forwarding_required(endpoints::ForwardingRequired::Never) - .add_openapi_response( - HTTP_STATUS_OK, "The requested snapshot.") - .add_openapi_response( - HTTP_STATUS_PARTIAL_CONTENT, - "The requested byte range of the snapshot.") - .add_openapi_response( - HTTP_STATUS_NOT_MODIFIED, "The requested snapshot has not changed.") - .add_openapi_response( - HTTP_STATUS_NOT_FOUND, "The requested snapshot is not available.") - .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) - .install(); + } - auto get_ledger_chunk = [&](ccf::endpoints::CommandEndpointContext& ctx) { + static void get_ledger_chunk( + ccf::endpoints::CommandEndpointContext& ctx, + ccf::AbstractNodeContext& node_context) + { auto node_configuration_subsystem = get_node_configuration_subsystem(node_context, ctx); if (node_configuration_subsystem == nullptr) @@ -1004,8 +928,119 @@ namespace ccf::node ccf::http::headers::CCF_LEDGER_CHUNK_NAME, chunk_name); fill_range_response_from_file(ctx, f); + } + } // namespace detail - return; + static void init_file_serving_handlers( + ccf::BaseEndpointRegistry& registry, ccf::AbstractNodeContext& node_context) + { + static constexpr auto file_since_param_key = "since"; + + auto find_snapshot = [&](ccf::endpoints::ReadOnlyEndpointContext& ctx) { + detail::find_snapshot(ctx, node_context, file_since_param_key); + }; + registry + .make_read_only_endpoint( + "/snapshot", HTTP_HEAD, find_snapshot, no_auth_required) + .set_forwarding_required(endpoints::ForwardingRequired::Never) + .add_query_parameter( + file_since_param_key, ccf::endpoints::OptionalParameter) + .add_openapi_response( + HTTP_STATUS_PERMANENT_REDIRECT, "Redirect to the selected snapshot.") + .add_openapi_response( + HTTP_STATUS_NOT_FOUND, "No matching snapshot is available.") + .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) + .install(); + registry + .make_read_only_endpoint( + "/snapshot", HTTP_GET, find_snapshot, no_auth_required) + .set_forwarding_required(endpoints::ForwardingRequired::Never) + .add_query_parameter( + file_since_param_key, ccf::endpoints::OptionalParameter) + .add_openapi_response( + HTTP_STATUS_PERMANENT_REDIRECT, "Redirect to the selected snapshot.") + .add_openapi_response( + HTTP_STATUS_NOT_FOUND, "No matching snapshot is available.") + .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) + .install(); + + // Find a ledger chunk that includes the since value + auto find_chunk = [&](ccf::endpoints::ReadOnlyEndpointContext& ctx) { + detail::find_chunk(ctx, node_context, file_since_param_key); + }; + registry + .make_read_only_endpoint( + "/ledger_chunk", HTTP_HEAD, find_chunk, no_auth_required) + .set_forwarding_required(endpoints::ForwardingRequired::Never) + .add_query_parameter( + file_since_param_key, ccf::endpoints::RequiredParameter) + .add_openapi_response( + HTTP_STATUS_PERMANENT_REDIRECT, + "Redirect to the selected ledger chunk.") + .add_openapi_response( + HTTP_STATUS_NOT_FOUND, "No matching ledger chunk is available.") + .require_operator_feature(endpoints::OperatorFeature::LedgerChunkRead) + .set_openapi_summary("Ledger chunk metadata") + .set_openapi_description( + "Redirect to the corresponding /node/ledger_chunk/{chunk_name} " + "endpoint for the ledger chunk including the sequence number specified " + "in the 'since' query parameter.") + .install(); + registry + .make_read_only_endpoint( + "/ledger_chunk", HTTP_GET, find_chunk, no_auth_required) + .set_forwarding_required(endpoints::ForwardingRequired::Never) + .add_query_parameter( + file_since_param_key, ccf::endpoints::RequiredParameter) + .add_openapi_response( + HTTP_STATUS_PERMANENT_REDIRECT, + "Redirect to the selected ledger chunk.") + .add_openapi_response( + HTTP_STATUS_NOT_FOUND, "No matching ledger chunk is available.") + .require_operator_feature(endpoints::OperatorFeature::LedgerChunkRead) + .set_openapi_summary("Download ledger chunk") + .set_openapi_description( + "Redirect to the corresponding /node/ledger_chunk/{chunk_name} " + "endpoint for the ledger chunk including the sequence number specified " + "in the 'since' query parameter.") + .install(); + + auto get_snapshot = [&](ccf::endpoints::CommandEndpointContext& ctx) { + detail::get_snapshot(ctx, node_context); + }; + registry + .make_command_endpoint( + "/snapshot/{snapshot_name}", HTTP_HEAD, get_snapshot, no_auth_required) + .set_forwarding_required(endpoints::ForwardingRequired::Never) + .add_openapi_response( + HTTP_STATUS_OK, "Metadata for the requested snapshot.") + .add_openapi_response( + HTTP_STATUS_PARTIAL_CONTENT, + "Metadata for the requested snapshot range.") + .add_openapi_response( + HTTP_STATUS_NOT_MODIFIED, "The requested snapshot has not changed.") + .add_openapi_response( + HTTP_STATUS_NOT_FOUND, "The requested snapshot is not available.") + .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) + .install(); + registry + .make_command_endpoint( + "/snapshot/{snapshot_name}", HTTP_GET, get_snapshot, no_auth_required) + .set_forwarding_required(endpoints::ForwardingRequired::Never) + .add_openapi_response( + HTTP_STATUS_OK, "The requested snapshot.") + .add_openapi_response( + HTTP_STATUS_PARTIAL_CONTENT, + "The requested byte range of the snapshot.") + .add_openapi_response( + HTTP_STATUS_NOT_MODIFIED, "The requested snapshot has not changed.") + .add_openapi_response( + HTTP_STATUS_NOT_FOUND, "The requested snapshot is not available.") + .require_operator_feature(endpoints::OperatorFeature::SnapshotRead) + .install(); + + auto get_ledger_chunk = [&](ccf::endpoints::CommandEndpointContext& ctx) { + detail::get_ledger_chunk(ctx, node_context); }; registry .make_command_endpoint( From 3052e282f8f6d1971f55079217d833fc54120e8d Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 1 Oct 2026 20:10:53 +0100 Subject: [PATCH 6/9] Move programmability sample handlers out of its constructor The ProgrammabilityHandlers constructor in programmability.cpp had a cognitive complexity of 55, entirely from its 8 inline [this] handler lambdas (put, get, post, put_custom_endpoints, get_custom_endpoints, get_custom_endpoints_module, patch_runtime_options, get_runtime_options). Move each lambda's body into a private member function of ProgrammabilityHandlers with the same name, parameter list and (for the 7 that capture [this]) implicit access to the registry's inherited members, keeping thin forwarding lambdas in the constructor for registration. post is the only handler with no captures (it doesn't use `this`), so it becomes a private static member function instead of an instance one, and its forwarding lambda calls it via the qualified ProgrammabilityHandlers::post(ctx) rather than this->post(ctx). All other forwarders call this->name(ctx); none of the chosen names clash with members of DynamicJSEndpointRegistry or its bases, so no renaming was needed. This mirrors the free-function convention used in earlier layers of this stack (named functions registered via forwarding lambdas), adapted to a member-function registry. Pure reshuffle: handler bodies are token-identical to the former lambda bodies; registration call sites, paths, verbs, auth policies, schemas, and install() chains are unchanged. The small capture-less lambda passed inline to set_js_kv_namespace_restriction(), and the pre-existing private helpers (try_get_user_id, get_action_content, set_error_details), are untouched. This drops the constructor's complexity from 55 to 5; the moved functions range from 1 to 10, well under the 50 threshold, so the NOLINTNEXTLINE(readability-function-cognitive-complexity) is removed. Part of #7358. Layer 5 of a stack of refactors applying this same pattern to the other init_*_handlers / constructor functions, on top of #8488. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../apps/programmability/programmability.cpp | 677 +++++++++--------- 1 file changed, 354 insertions(+), 323 deletions(-) diff --git a/samples/apps/programmability/programmability.cpp b/samples/apps/programmability/programmability.cpp index 91bba33d224..06a8feca762 100644 --- a/samples/apps/programmability/programmability.cpp +++ b/samples/apps/programmability/programmability.cpp @@ -276,8 +276,353 @@ namespace programmabilityapp } } + // Endpoint handlers registered by the constructor, via forwarding + // lambdas. Kept out of the constructor so each handler's complexity is + // measured on its own. + void put(ccf::endpoints::EndpointContext& ctx) + { + std::string key; + std::string error; + if (!get_path_param( + ctx.rpc_ctx->get_request_path_params(), "key", key, error)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NO_CONTENT, + ccf::errors::InvalidResourceName, + "Missing key"); + return; + } + + auto* records_handle = ctx.tx.template rw(PRIVATE_RECORDS); + records_handle->put(key, ctx.rpc_ctx->get_request_body()); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + } + + void get(ccf::endpoints::ReadOnlyEndpointContext& ctx) + { + std::string key; + std::string error; + if (!get_path_param( + ctx.rpc_ctx->get_request_path_params(), "key", key, error)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NO_CONTENT, + ccf::errors::InvalidResourceName, + "Missing key"); + return; + } + + auto* records_handle = ctx.tx.template ro(PRIVATE_RECORDS); + auto record = records_handle->get(key); + + if (record.has_value()) + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + ccf::http::headers::CONTENT_TYPE, + ccf::http::headervalues::contenttype::TEXT); + ctx.rpc_ctx->set_response_body(record.value()); + return; + } + + ctx.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, ccf::errors::InvalidResourceName, "No such key"); + } + + // Does not capture or use `this` (unlike the other handlers here), so is + // a static member function rather than an instance one, while still + // following the same naming convention as the rest. + static void post(ccf::endpoints::EndpointContext& ctx) + { + const nlohmann::json body = + ccf::parse_json_safe(ctx.rpc_ctx->get_request_body()); + + const auto records = body.get>(); + + auto* records_handle = ctx.tx.template rw(PRIVATE_RECORDS); + for (const auto& [key, value] : records) + { + const std::vector value_vec(value.begin(), value.end()); + records_handle->put(key, value_vec); + } + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + } + + void put_custom_endpoints(ccf::endpoints::EndpointContext& ctx) + { + const auto user_id = try_get_user_id(ctx); + if (!user_id.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_UNAUTHORIZED, + ccf::errors::InternalError, + "Failed to get user id"); + return; + } + // Authorization Check + nlohmann::json user_data = nullptr; + auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); + if (result == ccf::ApiResult::InternalError) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get user data for user {}: {}", + user_id.value(), + ccf::api_result_to_str(result))); + return; + } + const auto is_admin_it = user_data.find("isAdmin"); + + // Not every user gets to define custom endpoints, only users with + // isAdmin + if ( + !user_data.is_object() || is_admin_it == user_data.end() || + !is_admin_it.value().get()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Only admins may access this endpoint."); + return; + } + // End of Authorization Check + + const auto [format, content, created_at] = get_action_content(ctx); + const auto parsed_content = + ccf::parse_json_safe(content.begin(), content.end()); + const auto parsed_bundle = parsed_content.get(); + + // Make operation auditable + record_action_for_audit_v1( + ctx.tx, + format, + user_id.value(), + fmt::format( + "{} {}", ctx.rpc_ctx->get_method(), ctx.rpc_ctx->get_request_path()), + ctx.rpc_ctx->get_request_body()); + + // Ensure signed actions are not replayed + if (format == ccf::ActionFormat::COSE) + { + if (!created_at.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::MissingRequiredHeader, + fmt::format("Missing {} protected header", CREATED_AT_NAME)); + return; + } + ccf::InvalidArgsReason reason = {}; + result = check_action_not_replayed_v1( + ctx.tx, created_at.value(), ctx.rpc_ctx->get_request_body(), reason); + + if (set_error_details(ctx, result, reason)) + { + return; + } + } + + result = install_custom_endpoints_v1(ctx.tx, parsed_bundle); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to install endpoints: {}", ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + } + + void get_custom_endpoints(ccf::endpoints::EndpointContext& ctx) + { + ccf::js::Bundle bundle; + + auto result = get_custom_endpoints_v1(bundle, ctx.tx); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get endpoints: {}", ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_json(bundle, HTTP_STATUS_OK); + } + + void get_custom_endpoints_module(ccf::endpoints::EndpointContext& ctx) + { + std::string module_name; + + { + const auto parsed_query = + ccf::http::parse_query(ctx.rpc_ctx->get_request_query()); + + std::string error; + if (!ccf::http::get_query_value( + parsed_query, "module_name", module_name, error)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidQueryParameterValue, + std::move(error)); + return; + } + } + + std::string code; + + auto result = get_custom_endpoint_module_v1(code, ctx.tx, module_name); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get module: {}", ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + ccf::http::headers::CONTENT_TYPE, + ccf::http::headervalues::contenttype::JAVASCRIPT); + ctx.rpc_ctx->set_response_body(std::move(code)); + } + + void patch_runtime_options(ccf::endpoints::EndpointContext& ctx) + { + const auto user_id = try_get_user_id(ctx); + if (!user_id.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_UNAUTHORIZED, + ccf::errors::InternalError, + "Failed to get user id"); + return; + } + + // Authorization Check + nlohmann::json user_data = nullptr; + auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); + if (result == ccf::ApiResult::InternalError) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get user data for user {}: {}", + user_id.value(), + ccf::api_result_to_str(result))); + return; + } + const auto is_admin_it = user_data.find("isAdmin"); + + // Not every user gets to define custom endpoints, only users with + // isAdmin + if ( + !user_data.is_object() || is_admin_it == user_data.end() || + !is_admin_it.value().get()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Only admins may access this endpoint."); + return; + } + // End of Authorization Check + + // Implement patch semantics. + // - Fetch current options + ccf::JSRuntimeOptions options; + get_js_runtime_options_v1(options, ctx.tx); + + // - Convert current options to JSON + auto j_options = nlohmann::json(options); + + const auto [format, content, created_at] = get_action_content(ctx); + // - Parse content as JSON options + const auto arg_content = + ccf::parse_json_safe(content.begin(), content.end()); + + // - Merge, to overwrite current options with anything from body. Note + // that nulls mean deletions, which results in resetting to a default + // value + j_options.merge_patch(arg_content); + + // - Parse patched options from JSON + options = j_options.get(); + + // Make operation auditable + record_action_for_audit_v1( + ctx.tx, + format, + user_id.value(), + fmt::format( + "{} {}", ctx.rpc_ctx->get_method(), ctx.rpc_ctx->get_request_path()), + ctx.rpc_ctx->get_request_body()); + + // Ensure signed actions are not replayed + if (format == ccf::ActionFormat::COSE) + { + if (!created_at.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::MissingRequiredHeader, + fmt::format("Missing {} protected header", CREATED_AT_NAME)); + return; + } + ccf::InvalidArgsReason reason = {}; + result = check_action_not_replayed_v1( + ctx.tx, created_at.value(), ctx.rpc_ctx->get_request_body(), reason); + + if (set_error_details(ctx, result, reason)) + { + return; + } + } + + result = set_js_runtime_options_v1(ctx.tx, options); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to set options: {}", ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_json(options, HTTP_STATUS_OK); + } + + void get_runtime_options(ccf::endpoints::EndpointContext& ctx) + { + ccf::JSRuntimeOptions options; + + auto result = get_js_runtime_options_v1(options, ctx.tx); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get runtime options: {}", + ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_json(options, HTTP_STATUS_OK); + } + public: - // NOLINTNEXTLINE(readability-function-cognitive-complexity) ProgrammabilityHandlers(ccf::AbstractNodeContext& context) : ccf::js::DynamicJSEndpointRegistry( context, @@ -293,21 +638,7 @@ namespace programmabilityapp // This app contains a few hard-coded C++ endpoints, writing to a // C++-controlled table, to show that these can co-exist with JS endpoints auto put = [this](ccf::endpoints::EndpointContext& ctx) { - std::string key; - std::string error; - if (!get_path_param( - ctx.rpc_ctx->get_request_path_params(), "key", key, error)) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_NO_CONTENT, - ccf::errors::InvalidResourceName, - "Missing key"); - return; - } - - auto* records_handle = ctx.tx.template rw(PRIVATE_RECORDS); - records_handle->put(key, ctx.rpc_ctx->get_request_body()); - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + this->put(ctx); }; make_endpoint( "/records/{key}", HTTP_PUT, put, {ccf::user_cert_auth_policy}) @@ -315,35 +646,7 @@ namespace programmabilityapp .install(); auto get = [this](ccf::endpoints::ReadOnlyEndpointContext& ctx) { - std::string key; - std::string error; - if (!get_path_param( - ctx.rpc_ctx->get_request_path_params(), "key", key, error)) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_NO_CONTENT, - ccf::errors::InvalidResourceName, - "Missing key"); - return; - } - - auto* records_handle = ctx.tx.template ro(PRIVATE_RECORDS); - auto record = records_handle->get(key); - - if (record.has_value()) - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_header( - ccf::http::headers::CONTENT_TYPE, - ccf::http::headervalues::contenttype::TEXT); - ctx.rpc_ctx->set_response_body(record.value()); - return; - } - - ctx.rpc_ctx->set_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::InvalidResourceName, - "No such key"); + this->get(ctx); }; make_read_only_endpoint( "/records/{key}", HTTP_GET, get, {ccf::user_cert_auth_policy}) @@ -351,18 +654,7 @@ namespace programmabilityapp .install(); auto post = [](ccf::endpoints::EndpointContext& ctx) { - const nlohmann::json body = - ccf::parse_json_safe(ctx.rpc_ctx->get_request_body()); - - const auto records = body.get>(); - - auto* records_handle = ctx.tx.template rw(PRIVATE_RECORDS); - for (const auto& [key, value] : records) - { - const std::vector value_vec(value.begin(), value.end()); - records_handle->put(key, value_vec); - } - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + ProgrammabilityHandlers::post(ctx); }; make_endpoint("/records", HTTP_POST, post, {ccf::user_cert_auth_policy}) .install(); @@ -399,98 +691,7 @@ namespace programmabilityapp }); auto put_custom_endpoints = [this](ccf::endpoints::EndpointContext& ctx) { - const auto user_id = try_get_user_id(ctx); - if (!user_id.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_UNAUTHORIZED, - ccf::errors::InternalError, - "Failed to get user id"); - return; - } - // Authorization Check - nlohmann::json user_data = nullptr; - auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); - if (result == ccf::ApiResult::InternalError) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get user data for user {}: {}", - user_id.value(), - ccf::api_result_to_str(result))); - return; - } - const auto is_admin_it = user_data.find("isAdmin"); - - // Not every user gets to define custom endpoints, only users with - // isAdmin - if ( - !user_data.is_object() || is_admin_it == user_data.end() || - !is_admin_it.value().get()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Only admins may access this endpoint."); - return; - } - // End of Authorization Check - - const auto [format, content, created_at] = get_action_content(ctx); - const auto parsed_content = - ccf::parse_json_safe(content.begin(), content.end()); - const auto parsed_bundle = parsed_content.get(); - - // Make operation auditable - record_action_for_audit_v1( - ctx.tx, - format, - user_id.value(), - fmt::format( - "{} {}", - ctx.rpc_ctx->get_method(), - ctx.rpc_ctx->get_request_path()), - ctx.rpc_ctx->get_request_body()); - - // Ensure signed actions are not replayed - if (format == ccf::ActionFormat::COSE) - { - if (!created_at.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::MissingRequiredHeader, - fmt::format("Missing {} protected header", CREATED_AT_NAME)); - return; - } - ccf::InvalidArgsReason reason = {}; - result = check_action_not_replayed_v1( - ctx.tx, - created_at.value(), - ctx.rpc_ctx->get_request_body(), - reason); - - if (set_error_details(ctx, result, reason)) - { - return; - } - } - - result = install_custom_endpoints_v1(ctx.tx, parsed_bundle); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to install endpoints: {}", - ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + this->put_custom_endpoints(ctx); }; make_endpoint( @@ -502,20 +703,7 @@ namespace programmabilityapp .install(); auto get_custom_endpoints = [this](ccf::endpoints::EndpointContext& ctx) { - ccf::js::Bundle bundle; - - auto result = get_custom_endpoints_v1(bundle, ctx.tx); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get endpoints: {}", ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_json(bundle, HTTP_STATUS_OK); + this->get_custom_endpoints(ctx); }; make_endpoint( @@ -528,43 +716,7 @@ namespace programmabilityapp auto get_custom_endpoints_module = [this](ccf::endpoints::EndpointContext& ctx) { - std::string module_name; - - { - const auto parsed_query = - ccf::http::parse_query(ctx.rpc_ctx->get_request_query()); - - std::string error; - if (!ccf::http::get_query_value( - parsed_query, "module_name", module_name, error)) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidQueryParameterValue, - std::move(error)); - return; - } - } - - std::string code; - - auto result = - get_custom_endpoint_module_v1(code, ctx.tx, module_name); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get module: {}", ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_header( - ccf::http::headers::CONTENT_TYPE, - ccf::http::headervalues::contenttype::JAVASCRIPT); - ctx.rpc_ctx->set_response_body(std::move(code)); + this->get_custom_endpoints_module(ctx); }; make_endpoint( @@ -577,114 +729,7 @@ namespace programmabilityapp auto patch_runtime_options = [this](ccf::endpoints::EndpointContext& ctx) { - const auto user_id = try_get_user_id(ctx); - if (!user_id.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_UNAUTHORIZED, - ccf::errors::InternalError, - "Failed to get user id"); - return; - } - - // Authorization Check - nlohmann::json user_data = nullptr; - auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); - if (result == ccf::ApiResult::InternalError) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get user data for user {}: {}", - user_id.value(), - ccf::api_result_to_str(result))); - return; - } - const auto is_admin_it = user_data.find("isAdmin"); - - // Not every user gets to define custom endpoints, only users with - // isAdmin - if ( - !user_data.is_object() || is_admin_it == user_data.end() || - !is_admin_it.value().get()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Only admins may access this endpoint."); - return; - } - // End of Authorization Check - - // Implement patch semantics. - // - Fetch current options - ccf::JSRuntimeOptions options; - get_js_runtime_options_v1(options, ctx.tx); - - // - Convert current options to JSON - auto j_options = nlohmann::json(options); - - const auto [format, content, created_at] = get_action_content(ctx); - // - Parse content as JSON options - const auto arg_content = - ccf::parse_json_safe(content.begin(), content.end()); - - // - Merge, to overwrite current options with anything from body. Note - // that nulls mean deletions, which results in resetting to a default - // value - j_options.merge_patch(arg_content); - - // - Parse patched options from JSON - options = j_options.get(); - - // Make operation auditable - record_action_for_audit_v1( - ctx.tx, - format, - user_id.value(), - fmt::format( - "{} {}", - ctx.rpc_ctx->get_method(), - ctx.rpc_ctx->get_request_path()), - ctx.rpc_ctx->get_request_body()); - - // Ensure signed actions are not replayed - if (format == ccf::ActionFormat::COSE) - { - if (!created_at.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::MissingRequiredHeader, - fmt::format("Missing {} protected header", CREATED_AT_NAME)); - return; - } - ccf::InvalidArgsReason reason = {}; - result = check_action_not_replayed_v1( - ctx.tx, - created_at.value(), - ctx.rpc_ctx->get_request_body(), - reason); - - if (set_error_details(ctx, result, reason)) - { - return; - } - } - - result = set_js_runtime_options_v1(ctx.tx, options); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to set options: {}", ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_json(options, HTTP_STATUS_OK); + this->patch_runtime_options(ctx); }; make_endpoint( "/custom_endpoints/runtime_options", @@ -694,21 +739,7 @@ namespace programmabilityapp .install(); auto get_runtime_options = [this](ccf::endpoints::EndpointContext& ctx) { - ccf::JSRuntimeOptions options; - - auto result = get_js_runtime_options_v1(options, ctx.tx); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get runtime options: {}", - ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_json(options, HTTP_STATUS_OK); + this->get_runtime_options(ctx); }; make_endpoint( "/custom_endpoints/runtime_options", From 058f5850729c4e0c7765c6927676f08b94e3f8a6 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Fri, 2 Oct 2026 21:02:59 +0100 Subject: [PATCH 7/9] Move node endpoint handlers out of NodeEndpoints::init_handlers (#8491) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/node/rpc/node_frontend.h | 2640 ++++++++++++++++++---------------- 1 file changed, 1408 insertions(+), 1232 deletions(-) diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 882b39eb438..bf00960b87f 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -467,319 +467,1394 @@ namespace ccf } } - public: - NodeEndpoints(NetworkState& network_, ccf::AbstractNodeContext& context_) : - CommonEndpointRegistry(get_actor_prefix(ActorsType::nodes), context_), - network(network_), - node_operation(*context_.get_subsystem()) - { - openapi_info.title = "CCF Public Node API"; - openapi_info.description = - "This API provides public, uncredentialed access to service and node " - "state."; - openapi_info.document_version = "5.0.8"; - } - - // NOLINTNEXTLINE(readability-function-cognitive-complexity) - void init_handlers() override + // Endpoint handlers registered by init_handlers(), via forwarding + // lambdas. Kept out of init_handlers() so each handler's cognitive + // complexity is measured on its own. + template + auto accept(T& args, const nlohmann::json& params) { - CommonEndpointRegistry::init_handlers(); + const auto in = params.get(); - const auto self_cert_auth_policy = - std::make_shared(this->context); + // Not part of network => Internal error + if ( + !this->node_operation.is_part_of_network() && + !this->node_operation.is_part_of_public_network() && + !this->node_operation.is_reading_private_ledger()) + { + const std::string payload = + "Target node should be part of network to accept new nodes."; + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + payload); + } - auto accept = [this](auto& args, const nlohmann::json& params) { - const auto in = params.get(); + // No service => Internal error + auto service = args.tx.rw(this->network.service); + auto active_service = service->get(); + if (!active_service.has_value()) + { + const std::string payload = + "No service is available to accept new node."; + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + payload); + } - // Not part of network => Internal error - if ( - !this->node_operation.is_part_of_network() && - !this->node_operation.is_part_of_public_network() && - !this->node_operation.is_reading_private_ledger()) + auto* current_consensus = get_consensus(); + const auto should_redirect_to_primary = + current_consensus != nullptr && !this->node_operation.can_replicate(); + auto redirect_to_primary = [&]() { + auto primary_id = current_consensus->primary(); + if (primary_id.has_value()) { + const auto address = node::get_redirect_address_for_node( + args, args.tx, primary_id.value()); + if (!address.has_value()) + { + LOG_INFO_FMT( + "Join request rejected: no redirect address for " + "primary {}", + primary_id.value()); + return already_populated_response(); + } + + args.rpc_ctx->set_response_header( + http::headers::LOCATION, + fmt::format("https://{}/node/join", address.value())); + const std::string payload = - "Target node should be part of network to accept new nodes."; - LOG_INFO_FMT("Join request rejected: {}", payload); + "Node is not primary; cannot handle write"; + LOG_INFO_FMT( + "Join request redirected to primary {} at {}: {}", + primary_id.value(), + address.value(), + payload); return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, + HTTP_STATUS_PERMANENT_REDIRECT, + ccf::errors::NodeCannotHandleRequest, payload); } - // No service => Internal error - auto service = args.tx.rw(this->network.service); - auto active_service = service->get(); - if (!active_service.has_value()) + const std::string payload = "Primary unknown"; + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + payload); + }; + + auto nodes = args.tx.ro(network.nodes); + + // If already joined => return equivalent response + auto existing_node_info = check_node_exists( + args.tx, args.rpc_ctx->get_session_context()->caller_cert); + if (existing_node_info.has_value()) + { + JoinNetworkNodeToNode::Out rep; + + // If the node already exists, return network secrets if is already + // trusted. Otherwise, only return its status + auto node_info = nodes->get(existing_node_info->node_id); + auto node_status = + node_info->status; // NOLINT(bugprone-unchecked-optional-access) + rep.node_status = node_status; + rep.node_id = existing_node_info->node_id; + if (node_status == NodeStatus::TRUSTED) { - const std::string payload = - "No service is available to accept new node."; - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - payload); + rep.network_info = JoinNetworkNodeToNode::Out::NetworkInfo( + node_operation.is_part_of_public_network(), + node_operation.get_last_recovered_signed_idx(), + this->network.ledger_secrets->get( + args.tx, existing_node_info->ledger_secret_seqno), + *this->network.identity, + active_service->status, + existing_node_info->endorsed_certificate, + node_operation.get_cose_signatures_config()); + + LOG_DEBUG_FMT( + "Join request accepted: {} already marked as TRUSTED", + existing_node_info->node_id); + return make_success(rep); } - auto* current_consensus = get_consensus(); - const auto should_redirect_to_primary = - current_consensus != nullptr && !this->node_operation.can_replicate(); - auto redirect_to_primary = [&]() { - auto primary_id = current_consensus->primary(); - if (primary_id.has_value()) + if (node_status == NodeStatus::PENDING) + { + const auto pending_node_timeout = get_pending_node_timeout(); + if (!pending_node_timeout.has_value()) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfiguration subsystem is not available"); + } + + if (pending_node_timeout.value() > std::chrono::milliseconds::zero()) { - const auto address = node::get_redirect_address_for_node( - args, args.tx, primary_id.value()); - if (!address.has_value()) + if (should_redirect_to_primary) { - LOG_INFO_FMT( - "Join request rejected: no redirect address for " - "primary {}", - primary_id.value()); - return already_populated_response(); + return redirect_to_primary(); } - args.rpc_ctx->set_response_header( - http::headers::LOCATION, - fmt::format("https://{}/node/join", address.value())); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + node_info->pending_last_seen = current_time_ms(); + args.tx.rw(network.nodes) + ->put( + existing_node_info->node_id, + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + node_info.value()); + } + + // Only return node status and ID + LOG_DEBUG_FMT( + "Join request accepted: {} already marked as PENDING", + existing_node_info->node_id); + return make_success(rep); + } - const std::string payload = - "Node is not primary; cannot handle write"; - LOG_INFO_FMT( - "Join request redirected to primary {} at {}: {}", - primary_id.value(), - address.value(), - payload); - return make_error( - HTTP_STATUS_PERMANENT_REDIRECT, - ccf::errors::NodeCannotHandleRequest, - payload); + const std::string payload = fmt::format( + "Joining node is not in expected state ({}).", node_status); + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidNodeState, payload); + } + + // Not the primary => Redirect if possible to primary + if (should_redirect_to_primary) + { + return redirect_to_primary(); + } + + // Joiner's snapshot too old => StartupSeqnoIsOld + // (causes joiner to fetch a more recent snapshot) + // + // The joiner always wants to use the most recent snapshot. + // However this will result in the joiner chasing the primary if + // snapshot production period ~= snapshot fetching delay + // + // So we have hysteresis in the fetching constraint: + // If the joiner has already fetched a snapshot: joiner seqno > startup + // snapshot seqno Otherwise: joiner seqno > latest snapshot on disk + // seqno + auto this_startup_seqno = + this->node_operation.get_startup_snapshot_seqno(); + ccf::kv::Version required_seqno = this_startup_seqno; + // If the joiner does not enable fetching, or is a legacy node, + // join_fetch_count is unset and we should use the required bound to + // prevent it chasing the primary. + // Otherwise if this is the first request, use the preferred bound + bool using_preferred_bound = + (in.join_fetch_count.has_value() && in.join_fetch_count.value() == 0); + if (using_preferred_bound) + { + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (node_configuration_subsystem != nullptr) + { + const auto& snapshots_config = + node_configuration_subsystem->get().node_config.snapshots; + const auto latest_committed_snapshot = + snapshots::find_latest_committed_snapshot_in_directory( + snapshots_config.directory); + if (latest_committed_snapshot.has_value()) + { + const auto latest_snapshot_seqno = + snapshots::get_snapshot_idx_from_file_name( + latest_committed_snapshot->filename().string()); + required_seqno = std::max( + required_seqno, + static_cast(latest_snapshot_seqno)); } + } + } + if ( + in.startup_seqno.has_value() && + in.startup_seqno.value() < required_seqno) + { + // Make sure that the joiner's snapshot is more recent than this + // node's snapshot. Otherwise, the joiner may not be given all the + // ledger secrets required to replay historical transactions. + const std::string payload = fmt::format( + "Node requested to join from seqno {} which is older than this " + "node {} {}. A snapshot at least as recent as {} must " + "be used instead.", + in.startup_seqno.value(), + using_preferred_bound ? "latest_on_disk_seqno" : "startup_seqno", + required_seqno, + required_seqno); + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::StartupSeqnoIsOld, payload); + } - const std::string payload = "Primary unknown"; - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - payload); - }; + auto joining_node_status = NodeStatus::PENDING; + // If the service is opening, new nodes are trusted straight away + if ( + active_service->status == ServiceStatus::OPENING || + active_service->status == ServiceStatus::RECOVERING) + { + joining_node_status = NodeStatus::TRUSTED; + } - auto nodes = args.tx.ro(network.nodes); + return add_node( + args.tx, + args.rpc_ctx->get_session_context()->caller_cert, + in, + joining_node_status, + active_service->status); + } + + template + auto remove_expired_pending(T& ctx, nlohmann::json&& /*params*/) + { + const auto pending_node_timeout = get_pending_node_timeout(); + if (!pending_node_timeout.has_value()) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfiguration subsystem is not available"); + } + + if (pending_node_timeout.value() <= std::chrono::milliseconds::zero()) + { + return make_success(true); + } + + const auto now = current_time_ms(); + auto nodes = ctx.tx.rw(network.nodes); + std::map untimestamped_pending_nodes; + std::vector expired_pending_nodes; + nodes->foreach([&](const auto& node_id, const auto& node_info) { + if (node_info.status != NodeStatus::PENDING) + { + return true; + } + + if ( + !node_info.pending_last_seen.has_value() || + node_info.pending_last_seen.value() < 0 || + node_info.pending_last_seen.value() > now) + { + auto updated_node_info = node_info; + updated_node_info.pending_last_seen = now; + untimestamped_pending_nodes.emplace( + node_id, std::move(updated_node_info)); + } + else if ( + now - node_info.pending_last_seen.value() >= + pending_node_timeout.value().count()) + { + expired_pending_nodes.push_back(node_id); + } + + return true; + }); + + for (const auto& [node_id, node_info] : untimestamped_pending_nodes) + { + nodes->put(node_id, node_info); + } + + for (const auto& node_id : expired_pending_nodes) + { + LOG_INFO_FMT("Removing expired Pending node {}", node_id); + InternalTablesAccess::remove_node(ctx.tx, node_id); + } + + return make_success(true); + } + + template + auto set_retired_committed(T& ctx, nlohmann::json&& /*params*/) + { + auto nodes = ctx.tx.rw(network.nodes); + nodes->foreach([&nodes](const auto& node_id, auto node_info) { + auto gc_node = nodes->get_globally_committed(node_id); + if ( + gc_node.has_value() && gc_node->status == ccf::NodeStatus::RETIRED && + !node_info.retired_committed) + { + // Set retired_committed on nodes for which RETIRED status + // has been committed. + node_info.retired_committed = true; + nodes->put(node_id, node_info); + + LOG_DEBUG_FMT("Setting retired_committed on node {}", node_id); + } + return true; + }); + + return make_success(); + } + + template + auto get_state(T& args, nlohmann::json&& /*params*/) + { + GetState::Out result; + auto [s, rts, lrs] = this->node_operation.state(); + result.node_id = this->context.get_node_id(); + result.state = s; + result.recovery_target_seqno = rts; + result.last_recovered_seqno = lrs; + result.startup_seqno = this->node_operation.get_startup_snapshot_seqno(); + + // Read last signed seqno from both raw and COSE signature tables + auto signatures = args.tx.template ro(Tables::SIGNATURES); + auto sig = signatures->get(); + + ccf::kv::Version raw_seqno = 0; + if (sig.has_value()) + { + raw_seqno = sig.value().seqno; + } + + ccf::kv::Version cose_seqno = 0; + auto cose_signatures = + args.tx.template ro(Tables::COSE_SIGNATURES); + auto cose_sig = cose_signatures->get(ccf::IdentityType::CLASSICAL); + if (cose_sig.has_value() && !cose_sig->empty()) + { + auto receipt = ccf::cose::decode_ccf_receipt(cose_sig.value(), false); + auto txid = ccf::TxID::from_str(receipt.phdr.ccf.txid); + if (!txid.has_value()) + { + throw std::logic_error(fmt::format( + "Failed to parse txid from COSE signature: {}", + receipt.phdr.ccf.txid)); + } + cose_seqno = txid->seqno; + } + + result.last_signed_seqno = std::max(raw_seqno, cose_seqno); + + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + } + result.stop_notice = + node_configuration_subsystem->has_received_stop_notice(); + + return make_success(result); + } - // If already joined => return equivalent response - auto existing_node_info = check_node_exists( - args.tx, args.rpc_ctx->get_session_context()->caller_cert); - if (existing_node_info.has_value()) + template + auto get_quote(T& args, nlohmann::json&& /*params*/) + { + QuoteInfo node_quote_info; + const auto result = get_quote_for_this_node_v1(args.tx, node_quote_info); + if (result == ApiResult::OK) + { + Quote q; + q.node_id = context.get_node_id(); + q.raw = node_quote_info.quote; + q.endorsements = node_quote_info.endorsements; + q.format = node_quote_info.format; + q.uvm_endorsements = node_quote_info.uvm_endorsements; + + auto nodes = args.tx.ro(network.nodes); + auto node_info = nodes->get(context.get_node_id()); + if (node_info.has_value() && node_info->code_digest.has_value()) { - JoinNetworkNodeToNode::Out rep; - - // If the node already exists, return network secrets if is already - // trusted. Otherwise, only return its status - auto node_info = nodes->get(existing_node_info->node_id); - auto node_status = node_info->status; - rep.node_status = node_status; - rep.node_id = existing_node_info->node_id; - if (node_status == NodeStatus::TRUSTED) + q.measurement = node_info->code_digest.value(); + } + else + { + auto measurement = + AttestationProvider::get_measurement(node_quote_info); + if (measurement.has_value()) + { + q.measurement = measurement.value().hex_str(); + } + else { - rep.network_info = JoinNetworkNodeToNode::Out::NetworkInfo( - node_operation.is_part_of_public_network(), - node_operation.get_last_recovered_signed_idx(), - this->network.ledger_secrets->get( - args.tx, existing_node_info->ledger_secret_seqno), - *this->network.identity, - active_service->status, - existing_node_info->endorsed_certificate, - node_operation.get_cose_signatures_config()); - - LOG_DEBUG_FMT( - "Join request accepted: {} already marked as TRUSTED", - existing_node_info->node_id); - return make_success(rep); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InvalidQuote, + "Failed to extract code id from node quote."); } + } + + return make_success(q); + } + + if (result == ApiResult::NotFound) + { + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Could not find node quote."); + } + + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format("Error code: {}", ccf::api_result_to_str(result))); + } + + template + auto get_quotes(T& args, nlohmann::json&& /*params*/) + { + GetQuotes::Out result; - if (node_status == NodeStatus::PENDING) + auto nodes = args.tx.ro(network.nodes); + nodes->foreach( + ["es = result.quotes](const auto& node_id, const auto& node_info) { + if (node_info.status == ccf::NodeStatus::TRUSTED) { - const auto pending_node_timeout = get_pending_node_timeout(); - if (!pending_node_timeout.has_value()) + Quote q; + q.node_id = node_id; + q.raw = node_info.quote_info.quote; + q.endorsements = node_info.quote_info.endorsements; + q.format = node_info.quote_info.format; + q.uvm_endorsements = node_info.quote_info.uvm_endorsements; + + if (node_info.code_digest.has_value()) { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfiguration subsystem is not available"); + q.measurement = node_info.code_digest.value(); } - - if ( - pending_node_timeout.value() > std::chrono::milliseconds::zero()) + else { - if (should_redirect_to_primary) + auto measurement = + AttestationProvider::get_measurement(node_info.quote_info); + if (measurement.has_value()) { - return redirect_to_primary(); + q.measurement = measurement.value().hex_str(); } - - node_info->pending_last_seen = current_time_ms(); - args.tx.rw(network.nodes) - ->put(existing_node_info->node_id, node_info.value()); } + quotes.emplace_back(q); + } + return true; + }); + + return make_success(result); + } - // Only return node status and ID - LOG_DEBUG_FMT( - "Join request accepted: {} already marked as PENDING", - existing_node_info->node_id); - return make_success(rep); + template + auto get_attestations(T& args, nlohmann::json&& params) + { + auto res = get_quotes(args, std::move(params)); + const auto* body = std::get_if(&res); + if (body != nullptr) + { + auto result = nlohmann::json::object(); + result["attestations"] = (*body)["quotes"]; + return make_success(result); + } + + return res; + } + + template + auto network_status(T& args, nlohmann::json&& /*params*/) + { + GetNetworkInfo::Out out; + auto service = args.tx.ro(network.service); + auto service_state = service->get(); + if (service_state.has_value()) + { + const auto& service_value = service_state.value(); + out.service_status = service_value.status; + out.service_certificate = service_value.cert; + out.recovery_count = service_value.recovery_count.value_or(0); + out.service_data = service_value.service_data; + out.current_service_create_txid = + service_value.current_service_create_txid; + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + out.current_view = current_consensus->get_view(); + auto primary_id = current_consensus->primary(); + if (primary_id.has_value()) + { + out.primary_id = primary_id.value(); } + } + return make_success(out); + } + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Service state not available."); + } - const std::string payload = fmt::format( - "Joining node is not in expected state ({}).", node_status); - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidNodeState, payload); + template + static auto service_previous_identity(T& args, nlohmann::json&& /*params*/) + { + auto psi_handle = args.tx.template ro( + ccf::Tables::PREVIOUS_SERVICE_IDENTITY); + const auto psi = psi_handle->get(); + if (psi.has_value()) + { + GetServicePreviousIdentity::Out out; + out.previous_service_identity = psi.value(); + return make_success(out); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "This service is not a recovery of a previous service."); + } + + template + auto get_nodes(T& args, nlohmann::json&& /*params*/) + { + const auto parsed_query = + http::parse_query(args.rpc_ctx->get_request_query()); + + std::string error_string; // Ignored - all params are optional + const auto host = http::get_query_value_opt( + parsed_query, "host", error_string); + const auto port = http::get_query_value_opt( + parsed_query, "port", error_string); + const auto status_str = http::get_query_value_opt( + parsed_query, "status", error_string); + + std::optional status; + if (status_str.has_value()) + { + // Convert the query argument to a JSON string, try to parse it as + // a NodeStatus, return an error if this doesn't work + try + { + status = nlohmann::json(status_str.value()).get(); + } + catch (const ccf::JsonParseError& e) + { + return ccf::make_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidQueryParameterValue, + fmt::format( + "Query parameter '{}' is not a valid node status", + status_str.value())); + } + } + + GetNodes::Out out; + + auto nodes = args.tx.ro(this->network.nodes); + auto* current_consensus = get_consensus(); + nodes->foreach([host, port, status, &out, nodes, current_consensus]( + const NodeId& nid, const NodeInfo& ni) { + if (status.has_value() && status.value() != ni.status) + { + return true; } - // Not the primary => Redirect if possible to primary - if (should_redirect_to_primary) - { - return redirect_to_primary(); - } + // Match on any interface + bool is_matched = false; + for (auto const& interface : ni.rpc_interfaces) + { + const auto& [pub_host, pub_port] = + split_net_address(interface.second.published_address); + + if ( + (!host.has_value() || host.value() == pub_host) && + (!port.has_value() || port.value() == pub_port)) + { + is_matched = true; + break; + } + } + + if (!is_matched) + { + return true; + } + + bool is_primary = false; + if (current_consensus != nullptr) + { + is_primary = current_consensus->primary() == nid; + } + + out.nodes.push_back( + {nid, + ni.status, + is_primary, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(nid).value_or(0)}); + return true; + }); + + return make_success(out); + } + + template + auto get_removable_nodes(T& args, nlohmann::json&& /*params*/) + { + GetNodes::Out out; + + auto nodes = args.tx.ro(this->network.nodes); + nodes->foreach( + [&out, nodes](const NodeId& node_id, const NodeInfo& /*ni*/) { + // Only nodes whose retire_committed status is committed can be + // safely removed, because any primary elected from here on would + // consider them retired, and would consequently not need their + // input in any quorum. We must therefore read the KV at its + // globally committed watermark, for the purpose of this RPC. Since + // this transaction does not perform a write, it is safe to do this. + auto node = nodes->get_globally_committed(node_id); + if ( + node.has_value() && node->status == ccf::NodeStatus::RETIRED && + node->retired_committed) + { + out.nodes.push_back( + {node_id, + node->status, + false /* is_primary */, + node->rpc_interfaces, + node->node_data, + nodes->get_version_of_previous_write(node_id).value_or(0)}); + } + return true; + }); + + return make_success(out); + } + + template + auto delete_retired_committed_node(T& args, nlohmann::json&& /*params*/) + { + GetNodes::Out out; + + std::string node_id; + std::string error; + if (!get_path_param( + args.rpc_ctx->get_request_path_params(), "node_id", node_id, error)) + { + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); + } + + auto nodes = args.tx.rw(this->network.nodes); + if (!nodes->has(node_id)) + { + return make_error( + HTTP_STATUS_NOT_FOUND, ccf::errors::ResourceNotFound, "No such node"); + } + + // A node's retirement is only complete when the + // transition of retired_committed is itself committed, + // i.e. when the next eligible primary is guaranteed to + // be aware the retirement is committed. + // As a result, the handler must check node info at the + // current committed level, rather than at the end of the + // local suffix. + // While this transaction does execute a write, it specifically + // deletes the value it reads from. It is therefore safe to + // execute on the basis of a potentially stale read-set, + // which get_globally_committed() typically produces. + auto node = nodes->get_globally_committed(node_id); + if ( + node.has_value() && node->status == ccf::NodeStatus::RETIRED && + node->retired_committed) + { + InternalTablesAccess::remove_node(args.tx, node_id); + } + else + { + return make_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::NodeNotRetiredCommitted, + "Node is not completely retired"); + } + + return make_success(true); + } + + template + auto get_self_signed_certificate(T& /*args*/, nlohmann::json&& /*params*/) + { + return SelfSignedNodeCertificateInfo{ + this->node_operation.get_self_signed_node_certificate()}; + } + + template + auto get_node_info(T& args, nlohmann::json&& /*params*/) + { + std::string node_id; + std::string error; + if (!get_path_param( + args.rpc_ctx->get_request_path_params(), "node_id", node_id, error)) + { + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); + } + + auto nodes = args.tx.ro(this->network.nodes); + auto info = nodes->get(node_id); + + if (!info) + { + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node not found"); + } + + bool is_primary = false; + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto primary = current_consensus->primary(); + if (primary.has_value() && primary.value() == node_id) + { + is_primary = true; + } + } + auto& ni = info.value(); + return make_success(GetNode::Out{ + node_id, + ni.status, + is_primary, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(node_id).value_or(0)}); + } + + template + auto get_self_node(T& args, nlohmann::json&& /*params*/) + { + auto node_id = this->context.get_node_id(); + auto nodes = args.tx.ro(this->network.nodes); + auto info = nodes->get(node_id); + + bool is_primary = false; + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto primary = current_consensus->primary(); + if (primary.has_value() && primary.value() == node_id) + { + is_primary = true; + } + } + + if (info.has_value()) + { + // Answers from the KV are preferred, as they are more up-to-date, + // especially status and node_data. + auto& ni = info.value(); + return make_success(GetNode::Out{ + node_id, + ni.status, + is_primary, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(node_id).value_or(0)}); + } + + // If the node isn't in its KV yet, fall back to configuration + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + } + const auto& node_startup_config = + node_configuration_subsystem->get().node_config; + return make_success(GetNode::Out{ + node_id, + ccf::NodeStatus::PENDING, + is_primary, + node_startup_config.network.rpc_interfaces, + node_configuration_subsystem->get().node_data, + 0}); + } + + template + auto get_primary_node(T& args, nlohmann::json&& /*params*/) + { + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto primary_id = current_consensus->primary(); + if (!primary_id.has_value()) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Primary unknown"); + } + + auto nodes = args.tx.ro(this->network.nodes); + auto info = nodes->get(primary_id.value()); + if (!info) + { + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node not found"); + } + + auto& ni = info.value(); + return make_success(GetNode::Out{ + primary_id.value(), + ni.status, + true, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(primary_id.value()) + .value_or(0)}); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No configured consensus"); + } + + template + auto head_primary(T& args) + { + if (this->node_operation.can_replicate()) + { + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + } + else + { + auto* current_consensus = get_consensus(); + if (current_consensus == nullptr) + { + args.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Consensus not initialised"); + return; + } + + auto primary_id = current_consensus->primary(); + if (!primary_id.has_value()) + { + args.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Primary unknown"); + return; + } + + const auto address = node::get_redirect_address_for_node( + args, args.tx, primary_id.value()); + if (!address.has_value()) + { + return; + } + + args.rpc_ctx->set_response_header( + http::headers::LOCATION, + fmt::format("https://{}/node/primary", address.value())); + args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT); + } + } + + template + auto get_primary(T& args) + { + if (this->node_operation.can_replicate()) + { + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; + } + + args.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node is not primary"); + } + + template + auto get_backup(T& args) + { + if (!this->node_operation.can_replicate()) + { + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; + } + + args.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node is not backup"); + } + + template + auto consensus_config(T& /*args*/, nlohmann::json&& /*params*/) + { + // Query node for configurations, separate current from pending + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto cfg = current_consensus->get_latest_configuration(); + ConsensusConfig cc; + for (auto& [nid, ninfo] : cfg) + { + cc.emplace( + nid.value(), + ConsensusNodeConfig{ + fmt::format("{}:{}", ninfo.hostname, ninfo.port)}); + } + return make_success(cc); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No configured consensus"); + } + + template + auto consensus_state(T& /*args*/, nlohmann::json&& /*params*/) + { + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + return make_success( + ConsensusConfigDetails{current_consensus->get_details()}); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No configured consensus"); + } + + template + auto node_metrics(T& args) + { + NodeMetrics nm; + nm.sessions = node_operation.get_session_metrics(); + + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + args.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + args.rpc_ctx->set_response_body(nlohmann::json(nm).dump()); + } + + template + auto js_metrics(T& args, nlohmann::json&& /*params*/) + { + auto bytecode_map = args.tx.ro(this->network.modules_quickjs_bytecode); + auto version_val = args.tx.ro(this->network.modules_quickjs_version); + uint64_t bytecode_size = 0; + bytecode_map->foreach( + [&bytecode_size](const auto&, const auto& bytecode) { + bytecode_size += bytecode.size(); + return true; + }); + auto js_engine_map = args.tx.ro(this->network.js_engine); + JavaScriptMetrics m; + m.bytecode_size = bytecode_size; + m.bytecode_used = version_val->get() == std::string(ccf::quickjs_version); + + auto options = js_engine_map->get().value_or(ccf::JSRuntimeOptions{}); + m.max_stack_size = options.max_stack_bytes; + m.max_heap_size = options.max_heap_bytes; + m.max_execution_time = options.max_execution_time_ms; + m.max_cached_interpreters = options.max_cached_interpreters; + + return m; + } + + template + static auto version(T& /*args*/, nlohmann::json&& /*params*/) + { + GetVersion::Out result; + result.ccf_version = ccf::ccf_version; + result.quickjs_version = ccf::quickjs_version; + result.unsafe = false; + + return make_success(result); + } + + template + auto create(T& ctx, nlohmann::json&& params) + { + LOG_INFO_FMT("Processing create RPC"); + + bool recovering = node_operation.is_reading_public_ledger(); + + // This endpoint can only be called once, directly from the starting + // node for the genesis or end of public recovery transaction to + // initialise the service + if (!node_operation.is_in_initialised_state() && !recovering) + { + return make_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::InternalError, + "Node is not in initial state."); + } + + const auto in = params.get(); + + if (InternalTablesAccess::is_service_created(ctx.tx, in.service_cert)) + { + return make_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::InternalError, + "Service is already created."); + } + + InternalTablesAccess::create_service( + ctx.tx, in.service_cert, in.create_txid, in.service_data, recovering); + + if (recovering) + { + // Recovery starts with a fresh consensus configuration, so previous + // service nodes can be removed immediately. + InternalTablesAccess::remove_previous_service_nodes(ctx.tx); + } + + // Genesis transaction (i.e. not after recovery) + if (in.genesis_info.has_value()) + { + // Note that it is acceptable to start a network without any member + // having a recovery share. The service will check that at least one + // recovery member is added before the service is opened. + for (const auto& info : in.genesis_info->members) + { + InternalTablesAccess::add_member(ctx.tx, info); + } + + InternalTablesAccess::init_configuration( + ctx.tx, in.genesis_info->service_configuration); + InternalTablesAccess::set_constitution( + ctx.tx, in.genesis_info->constitution); + } + else + { + // On recovery, force a new ledger chunk + auto* tx_ = static_cast(&ctx.tx); + if (tx_ == nullptr) + { + throw std::logic_error("Could not cast tx to CommittableTx"); + } + tx_->set_tx_flag( + ccf::kv::CommittableTx::TxFlag::LEDGER_CHUNK_BEFORE_THIS_TX); + } + + auto endorsed_certificates = + ctx.tx.rw(network.node_endorsed_certificates); + endorsed_certificates->put(in.node_id, in.node_endorsed_certificate); + + NodeInfo node_info = { + in.node_info_network, + {in.quote_info}, + in.public_encryption_key, + NodeStatus::TRUSTED, + std::nullopt, + in.measurement.hex_str(), + in.certificate_signing_request, + in.public_key, + in.node_data}; + InternalTablesAccess::add_node(ctx.tx, in.node_id, node_info); + + if (in.sealing_recovery_data.has_value()) + { + const auto& [sealing_keys, sealing_recovery_name] = + in.sealing_recovery_data.value(); + auto* sealed_recovery_keys = + ctx.tx.template rw(Tables::SEALED_RECOVERY_KEYS); + sealed_recovery_keys->put(in.node_id, sealing_keys); + + auto* local_sealing_node_id_map = + ctx.tx.template rw( + Tables::SEALING_RECOVERY_NAMES); + local_sealing_node_id_map->put(sealing_recovery_name, in.node_id); + } + + node_operation.shuffle_sealed_shares(ctx.tx); + + if ( + in.quote_info.format != QuoteFormat::amd_sev_snp_v1 || + !in.snp_uvm_endorsements.has_value()) + { + // For improved serviceability on SNP, do not record trusted + // measurements if UVM endorsements are available + InternalTablesAccess::trust_node_measurement( + ctx.tx, in.measurement, in.quote_info.format); + } + + switch (in.quote_info.format) + { + case QuoteFormat::insecure_virtual: + { + auto host_data = AttestationProvider::get_host_data(in.quote_info); + if (host_data.has_value()) + { + InternalTablesAccess::trust_node_virtual_host_data( + ctx.tx, host_data.value()); + } + else + { + LOG_FAIL_FMT("Unable to extract host data from virtual quote"); + } + break; + } + + case QuoteFormat::amd_sev_snp_v1: + { + auto host_data = + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + AttestationProvider::get_host_data(in.quote_info).value(); + InternalTablesAccess::trust_node_snp_host_data( + ctx.tx, host_data, in.snp_security_policy); + + InternalTablesAccess::trust_node_uvm_endorsements( + ctx.tx, in.snp_uvm_endorsements, recovering); + + auto attestation = + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + AttestationProvider::get_snp_attestation_report(in.quote_info) + .value(); + InternalTablesAccess::trust_node_snp_tcb_version( + ctx.tx, attestation, recovering); + break; + } + case QuoteFormat::oe_sgx_v1: + { + break; + } + } + + std::optional digest = + ccf::get_create_tx_claims_digest(ctx.tx); + if (digest.has_value()) + { + auto digest_value = digest.value(); + ctx.rpc_ctx->set_claims_digest(std::move(digest_value)); + } + + this->node_operation.recovery_decision_protocol().reset_state(ctx.tx); + this->node_operation.recovery_decision_protocol().try_start( + ctx.tx, recovering); + + LOG_INFO_FMT("Created service"); + return make_success(true); + } + + template + auto refresh_jwt_keys(T& ctx, nlohmann::json&& body) + { + // All errors are server errors since the client is the server. + + auto* current_consensus = get_consensus(); + auto primary_id = current_consensus->primary(); + if (!primary_id.has_value()) + { + LOG_FAIL_FMT("JWT key auto-refresh: primary unknown"); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Primary is unknown"); + } + + const auto& sig_auth_ident = + ctx.template get_caller(); + if (primary_id.value() != sig_auth_ident.node_id) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: request does not originate from primary"); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Request does not originate from primary."); + } + + SetJwtPublicSigningKeys parsed; + try + { + parsed = body.get(); + } + catch (const ccf::JsonParseError& e) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Unable to parse body."); + } + + auto issuers = ctx.tx.ro(this->network.jwt_issuers); + auto issuer_metadata_ = issuers->get(parsed.issuer); + if (!issuer_metadata_.has_value()) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: {} is not a valid issuer", parsed.issuer); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format("{} is not a valid issuer.", parsed.issuer)); + } + auto& issuer_metadata = issuer_metadata_.value(); + + if (!issuer_metadata.auto_refresh) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: {} does not have auto_refresh enabled", + parsed.issuer); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format("{} does not have auto_refresh enabled.", parsed.issuer)); + } + + if (!set_jwt_public_signing_keys( + ctx.tx, + "", + parsed.issuer, + issuer_metadata, + parsed.jwks)) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: error while storing signing keys for issuer " + "{}", + parsed.issuer); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Error while storing signing keys for issuer {}.", parsed.issuer)); + } + + return make_success(true); + } + + template + auto get_jwt_metrics(T& /*args*/, const nlohmann::json& /*params*/) + { + JWTRefreshMetrics metrics; + { + ccf::ds::MutexGuard guard(jwt_refresh_metrics_lock); + metrics = jwt_refresh_metrics; + } + return make_success(metrics); + } + + template + auto service_config_handler(T& args, const nlohmann::json& /*params*/) + { + return make_success(args.tx.ro(network.config)->get()); + } + + template + auto list_indexing_strategies(T& /*args*/, const nlohmann::json& /*params*/) + { + return make_success(this->context.get_indexing_strategies().describe()); + } + + auto get_ready_app(ccf::endpoints::CommandEndpointContext& ctx) + { + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + return; + } + if ( + !node_configuration_subsystem->has_received_stop_notice() && + this->node_operation.is_part_of_network() && + this->node_operation.is_user_frontend_open()) + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + } + else + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); + } + } + + auto get_ready_gov(ccf::endpoints::CommandEndpointContext& ctx) + { + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + return; + } + if ( + !node_configuration_subsystem->has_received_stop_notice() && + this->node_operation.is_accessible_to_members() && + this->node_operation.is_member_frontend_open()) + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + } + else + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); + } + } + + template + auto create_snapshot(T& args, nlohmann::json&& /*params*/) + { + auto* snapshot_create = + args.tx.template rw(ccf::Tables::SNAPSHOT_CREATE); + snapshot_create->touch(); + this->node_operation.trigger_snapshot(args.tx); + return make_success(); + } + + template + auto historical_cache_info( + [[maybe_unused]] T& args, [[maybe_unused]] nlohmann::json&& /*params*/) + { + GetHistoricalCacheInfo::Out result{}; + result.estimated_size = + this->context.get_historical_state().get_estimated_store_cache_size(); + return make_success(result); + } + + public: + NodeEndpoints(NetworkState& network_, ccf::AbstractNodeContext& context_) : + CommonEndpointRegistry(get_actor_prefix(ActorsType::nodes), context_), + network(network_), + node_operation(*context_.get_subsystem()) + { + openapi_info.title = "CCF Public Node API"; + openapi_info.description = + "This API provides public, uncredentialed access to service and node " + "state."; + openapi_info.document_version = "5.0.8"; + } - // Joiner's snapshot too old => StartupSeqnoIsOld - // (causes joiner to fetch a more recent snapshot) - // - // The joiner always wants to use the most recent snapshot. - // However this will result in the joiner chasing the primary if - // snapshot production period ~= snapshot fetching delay - // - // So we have hysteresis in the fetching constraint: - // If the joiner has already fetched a snapshot: joiner seqno > startup - // snapshot seqno Otherwise: joiner seqno > latest snapshot on disk - // seqno - auto this_startup_seqno = - this->node_operation.get_startup_snapshot_seqno(); - ccf::kv::Version required_seqno = this_startup_seqno; - // If the joiner does not enable fetching, or is a legacy node, - // join_fetch_count is unset and we should use the required bound to - // prevent it chasing the primary. - // Otherwise if this is the first request, use the preferred bound - bool using_preferred_bound = - (in.join_fetch_count.has_value() && in.join_fetch_count.value() == 0); - if (using_preferred_bound) - { - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (node_configuration_subsystem != nullptr) - { - const auto& snapshots_config = - node_configuration_subsystem->get().node_config.snapshots; - const auto latest_committed_snapshot = - snapshots::find_latest_committed_snapshot_in_directory( - snapshots_config.directory); - if (latest_committed_snapshot.has_value()) - { - const auto latest_snapshot_seqno = - snapshots::get_snapshot_idx_from_file_name( - latest_committed_snapshot->filename().string()); - required_seqno = std::max( - required_seqno, - static_cast(latest_snapshot_seqno)); - } - } - } - if ( - in.startup_seqno.has_value() && - in.startup_seqno.value() < required_seqno) - { - // Make sure that the joiner's snapshot is more recent than this - // node's snapshot. Otherwise, the joiner may not be given all the - // ledger secrets required to replay historical transactions. - const std::string payload = fmt::format( - "Node requested to join from seqno {} which is older than this " - "node {} {}. A snapshot at least as recent as {} must " - "be used instead.", - in.startup_seqno.value(), - using_preferred_bound ? "latest_on_disk_seqno" : "startup_seqno", - required_seqno, - required_seqno); - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::StartupSeqnoIsOld, payload); - } + void init_handlers() override + { + CommonEndpointRegistry::init_handlers(); - auto joining_node_status = NodeStatus::PENDING; - // If the service is opening, new nodes are trusted straight away - if ( - active_service->status == ServiceStatus::OPENING || - active_service->status == ServiceStatus::RECOVERING) - { - joining_node_status = NodeStatus::TRUSTED; - } + const auto self_cert_auth_policy = + std::make_shared(this->context); - return add_node( - args.tx, - args.rpc_ctx->get_session_context()->caller_cert, - in, - joining_node_status, - active_service->status); + auto accept = [this](auto& args, const nlohmann::json& params) { + return this->accept(args, params); }; make_endpoint("/join", HTTP_POST, json_adapter(accept), no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) .set_openapi_hidden(true) .install(); - auto remove_expired_pending = [this](auto& ctx, nlohmann::json&&) { - const auto pending_node_timeout = get_pending_node_timeout(); - if (!pending_node_timeout.has_value()) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfiguration subsystem is not available"); - } - - if (pending_node_timeout.value() <= std::chrono::milliseconds::zero()) - { - return make_success(true); - } - - const auto now = current_time_ms(); - auto nodes = ctx.tx.rw(network.nodes); - std::map untimestamped_pending_nodes; - std::vector expired_pending_nodes; - nodes->foreach([&](const auto& node_id, const auto& node_info) { - if (node_info.status != NodeStatus::PENDING) - { - return true; - } - - if ( - !node_info.pending_last_seen.has_value() || - node_info.pending_last_seen.value() < 0 || - node_info.pending_last_seen.value() > now) - { - auto updated_node_info = node_info; - updated_node_info.pending_last_seen = now; - untimestamped_pending_nodes.emplace( - node_id, std::move(updated_node_info)); - } - else if ( - now - node_info.pending_last_seen.value() >= - pending_node_timeout.value().count()) - { - expired_pending_nodes.push_back(node_id); - } - - return true; - }); - - for (const auto& [node_id, node_info] : untimestamped_pending_nodes) - { - nodes->put(node_id, node_info); - } - - for (const auto& node_id : expired_pending_nodes) - { - LOG_INFO_FMT("Removing expired Pending node {}", node_id); - InternalTablesAccess::remove_node(ctx.tx, node_id); - } - - return make_success(true); + auto remove_expired_pending = [this](auto& ctx, nlohmann::json&& json) { + return this->remove_expired_pending(ctx, std::move(json)); }; make_endpoint( "network/nodes/remove_expired_pending", @@ -790,26 +1865,8 @@ namespace ccf .set_openapi_hidden(true) .install(); - auto set_retired_committed = [this](auto& ctx, nlohmann::json&&) { - auto nodes = ctx.tx.rw(network.nodes); - nodes->foreach([&nodes](const auto& node_id, auto node_info) { - auto gc_node = nodes->get_globally_committed(node_id); - if ( - gc_node.has_value() && - gc_node->status == ccf::NodeStatus::RETIRED && - !node_info.retired_committed) - { - // Set retired_committed on nodes for which RETIRED status - // has been committed. - node_info.retired_committed = true; - nodes->put(node_id, node_info); - - LOG_DEBUG_FMT("Setting retired_committed on node {}", node_id); - } - return true; - }); - - return make_success(); + auto set_retired_committed = [this](auto& ctx, nlohmann::json&& json) { + return this->set_retired_committed(ctx, std::move(json)); }; make_endpoint( "network/nodes/set_retired_committed", @@ -819,58 +1876,8 @@ namespace ccf .set_openapi_hidden(true) .install(); - auto get_state = [this](auto& args, nlohmann::json&&) { - GetState::Out result; - auto [s, rts, lrs] = this->node_operation.state(); - result.node_id = this->context.get_node_id(); - result.state = s; - result.recovery_target_seqno = rts; - result.last_recovered_seqno = lrs; - result.startup_seqno = - this->node_operation.get_startup_snapshot_seqno(); - - // Read last signed seqno from both raw and COSE signature tables - auto signatures = args.tx.template ro(Tables::SIGNATURES); - auto sig = signatures->get(); - - ccf::kv::Version raw_seqno = 0; - if (sig.has_value()) - { - raw_seqno = sig.value().seqno; - } - - ccf::kv::Version cose_seqno = 0; - auto cose_signatures = - args.tx.template ro(Tables::COSE_SIGNATURES); - auto cose_sig = cose_signatures->get(ccf::IdentityType::CLASSICAL); - if (cose_sig.has_value() && !cose_sig->empty()) - { - auto receipt = ccf::cose::decode_ccf_receipt(cose_sig.value(), false); - auto txid = ccf::TxID::from_str(receipt.phdr.ccf.txid); - if (!txid.has_value()) - { - throw std::logic_error(fmt::format( - "Failed to parse txid from COSE signature: {}", - receipt.phdr.ccf.txid)); - } - cose_seqno = txid->seqno; - } - - result.last_signed_seqno = std::max(raw_seqno, cose_seqno); - - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - } - result.stop_notice = - node_configuration_subsystem->has_received_stop_notice(); - - return make_success(result); + auto get_state = [this](auto& args, nlohmann::json&& json) { + return this->get_state(args, std::move(json)); }; make_read_only_endpoint( "/state", HTTP_GET, json_read_only_adapter(get_state), no_auth_required) @@ -878,57 +1885,8 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_quote = [this](auto& args, nlohmann::json&&) { - QuoteInfo node_quote_info; - const auto result = - get_quote_for_this_node_v1(args.tx, node_quote_info); - if (result == ApiResult::OK) - { - Quote q; - q.node_id = context.get_node_id(); - q.raw = node_quote_info.quote; - q.endorsements = node_quote_info.endorsements; - q.format = node_quote_info.format; - q.uvm_endorsements = node_quote_info.uvm_endorsements; - - auto nodes = args.tx.ro(network.nodes); - auto node_info = nodes->get(context.get_node_id()); - if (node_info.has_value() && node_info->code_digest.has_value()) - { - q.measurement = node_info->code_digest.value(); - } - else - { - auto measurement = - AttestationProvider::get_measurement(node_quote_info); - if (measurement.has_value()) - { - q.measurement = measurement.value().hex_str(); - } - else - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InvalidQuote, - "Failed to extract code id from node quote."); - } - } - - return make_success(q); - } - - if (result == ApiResult::NotFound) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Could not find node quote."); - } - - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format("Error code: {}", ccf::api_result_to_str(result))); + auto get_quote = [this](auto& args, nlohmann::json&& json) { + return this->get_quote(args, std::move(json)); }; make_read_only_endpoint( "/quotes/self", @@ -947,40 +1905,8 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_quotes = [this](auto& args, nlohmann::json&&) { - GetQuotes::Out result; - - auto nodes = args.tx.ro(network.nodes); - nodes->foreach(["es = result.quotes]( - const auto& node_id, const auto& node_info) { - if (node_info.status == ccf::NodeStatus::TRUSTED) - { - Quote q; - q.node_id = node_id; - q.raw = node_info.quote_info.quote; - q.endorsements = node_info.quote_info.endorsements; - q.format = node_info.quote_info.format; - q.uvm_endorsements = node_info.quote_info.uvm_endorsements; - - if (node_info.code_digest.has_value()) - { - q.measurement = node_info.code_digest.value(); - } - else - { - auto measurement = - AttestationProvider::get_measurement(node_info.quote_info); - if (measurement.has_value()) - { - q.measurement = measurement.value().hex_str(); - } - } - quotes.emplace_back(q); - } - return true; - }); - - return make_success(result); + auto get_quotes = [this](auto& args, nlohmann::json&& json) { + return this->get_quotes(args, std::move(json)); }; make_read_only_endpoint( "/quotes", @@ -990,19 +1916,9 @@ namespace ccf .set_auto_schema() .install(); - auto get_attestations = - [get_quotes](auto& args, nlohmann::json&& params) { - auto res = get_quotes(args, std::move(params)); - const auto* body = std::get_if(&res); - if (body != nullptr) - { - auto result = nlohmann::json::object(); - result["attestations"] = (*body)["quotes"]; - return make_success(result); - } - - return res; - }; + auto get_attestations = [this](auto& args, nlohmann::json&& params) { + return this->get_attestations(args, std::move(params)); + }; make_read_only_endpoint( "/attestations", HTTP_GET, @@ -1011,35 +1927,8 @@ namespace ccf .set_auto_schema() .install(); - auto network_status = [this](auto& args, nlohmann::json&&) { - GetNetworkInfo::Out out; - auto service = args.tx.ro(network.service); - auto service_state = service->get(); - if (service_state.has_value()) - { - const auto& service_value = service_state.value(); - out.service_status = service_value.status; - out.service_certificate = service_value.cert; - out.recovery_count = service_value.recovery_count.value_or(0); - out.service_data = service_value.service_data; - out.current_service_create_txid = - service_value.current_service_create_txid; - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - out.current_view = current_consensus->get_view(); - auto primary_id = current_consensus->primary(); - if (primary_id.has_value()) - { - out.primary_id = primary_id.value(); - } - } - return make_success(out); - } - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Service state not available."); + auto network_status = [this](auto& args, nlohmann::json&& json) { + return this->network_status(args, std::move(json)); }; make_read_only_endpoint( "/network", @@ -1049,21 +1938,8 @@ namespace ccf .set_auto_schema() .install(); - auto service_previous_identity = [](auto& args, nlohmann::json&&) { - auto psi_handle = args.tx.template ro( - ccf::Tables::PREVIOUS_SERVICE_IDENTITY); - const auto psi = psi_handle->get(); - if (psi.has_value()) - { - GetServicePreviousIdentity::Out out; - out.previous_service_identity = psi.value(); - return make_success(out); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "This service is not a recovery of a previous service."); + auto service_previous_identity = [](auto& args, nlohmann::json&& json) { + return NodeEndpoints::service_previous_identity(args, std::move(json)); }; make_read_only_endpoint( "/service/previous_identity", @@ -1073,87 +1949,8 @@ namespace ccf .set_auto_schema() .install(); - auto get_nodes = [this](auto& args, nlohmann::json&&) { - const auto parsed_query = - http::parse_query(args.rpc_ctx->get_request_query()); - - std::string error_string; // Ignored - all params are optional - const auto host = http::get_query_value_opt( - parsed_query, "host", error_string); - const auto port = http::get_query_value_opt( - parsed_query, "port", error_string); - const auto status_str = http::get_query_value_opt( - parsed_query, "status", error_string); - - std::optional status; - if (status_str.has_value()) - { - // Convert the query argument to a JSON string, try to parse it as - // a NodeStatus, return an error if this doesn't work - try - { - status = nlohmann::json(status_str.value()).get(); - } - catch (const ccf::JsonParseError& e) - { - return ccf::make_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidQueryParameterValue, - fmt::format( - "Query parameter '{}' is not a valid node status", - status_str.value())); - } - } - - GetNodes::Out out; - - auto nodes = args.tx.ro(this->network.nodes); - auto* current_consensus = get_consensus(); - nodes->foreach([host, port, status, &out, nodes, current_consensus]( - const NodeId& nid, const NodeInfo& ni) { - if (status.has_value() && status.value() != ni.status) - { - return true; - } - - // Match on any interface - bool is_matched = false; - for (auto const& interface : ni.rpc_interfaces) - { - const auto& [pub_host, pub_port] = - split_net_address(interface.second.published_address); - - if ( - (!host.has_value() || host.value() == pub_host) && - (!port.has_value() || port.value() == pub_port)) - { - is_matched = true; - break; - } - } - - if (!is_matched) - { - return true; - } - - bool is_primary = false; - if (current_consensus != nullptr) - { - is_primary = current_consensus->primary() == nid; - } - - out.nodes.push_back( - {nid, - ni.status, - is_primary, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(nid).value_or(0)}); - return true; - }); - - return make_success(out); + auto get_nodes = [this](auto& args, nlohmann::json&& json) { + return this->get_nodes(args, std::move(json)); }; make_read_only_endpoint( "/network/nodes", @@ -1169,35 +1966,8 @@ namespace ccf "status", ccf::endpoints::OptionalParameter) .install(); - auto get_removable_nodes = [this](auto& args, nlohmann::json&&) { - GetNodes::Out out; - - auto nodes = args.tx.ro(this->network.nodes); - nodes->foreach( - [&out, nodes](const NodeId& node_id, const NodeInfo& /*ni*/) { - // Only nodes whose retire_committed status is committed can be - // safely removed, because any primary elected from here on would - // consider them retired, and would consequently not need their - // input in any quorum. We must therefore read the KV at its - // globally committed watermark, for the purpose of this RPC. Since - // this transaction does not perform a write, it is safe to do this. - auto node = nodes->get_globally_committed(node_id); - if ( - node.has_value() && node->status == ccf::NodeStatus::RETIRED && - node->retired_committed) - { - out.nodes.push_back( - {node_id, - node->status, - false /* is_primary */, - node->rpc_interfaces, - node->node_data, - nodes->get_version_of_previous_write(node_id).value_or(0)}); - } - return true; - }); - - return make_success(out); + auto get_removable_nodes = [this](auto& args, nlohmann::json&& json) { + return this->get_removable_nodes(args, std::move(json)); }; make_read_only_endpoint( @@ -1209,57 +1979,8 @@ namespace ccf .install(); auto delete_retired_committed_node = - [this](auto& args, nlohmann::json&&) { - GetNodes::Out out; - - std::string node_id; - std::string error; - if (!get_path_param( - args.rpc_ctx->get_request_path_params(), - "node_id", - node_id, - error)) - { - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); - } - - auto nodes = args.tx.rw(this->network.nodes); - if (!nodes->has(node_id)) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No such node"); - } - - // A node's retirement is only complete when the - // transition of retired_committed is itself committed, - // i.e. when the next eligible primary is guaranteed to - // be aware the retirement is committed. - // As a result, the handler must check node info at the - // current committed level, rather than at the end of the - // local suffix. - // While this transaction does execute a write, it specifically - // deletes the value it reads from. It is therefore safe to - // execute on the basis of a potentially stale read-set, - // which get_globally_committed() typically produces. - auto node = nodes->get_globally_committed(node_id); - if ( - node.has_value() && node->status == ccf::NodeStatus::RETIRED && - node->retired_committed) - { - InternalTablesAccess::remove_node(args.tx, node_id); - } - else - { - return make_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::NodeNotRetiredCommitted, - "Node is not completely retired"); - } - - return make_success(true); + [this](auto& args, nlohmann::json&& json) { + return this->delete_retired_committed_node(args, std::move(json)); }; make_endpoint( @@ -1271,61 +1992,20 @@ namespace ccf .install(); auto get_self_signed_certificate = - [this](auto& /*args*/, nlohmann::json&&) { - return SelfSignedNodeCertificateInfo{ - this->node_operation.get_self_signed_node_certificate()}; + [this](auto& args, nlohmann::json&& json) { + return this->get_self_signed_certificate(args, std::move(json)); }; - make_command_endpoint( - "/self_signed_certificate", - HTTP_GET, - json_command_adapter(get_self_signed_certificate), - no_auth_required) - .set_forwarding_required(endpoints::ForwardingRequired::Never) - .set_auto_schema() - .install(); - - auto get_node_info = [this](auto& args, nlohmann::json&&) { - std::string node_id; - std::string error; - if (!get_path_param( - args.rpc_ctx->get_request_path_params(), - "node_id", - node_id, - error)) - { - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); - } - - auto nodes = args.tx.ro(this->network.nodes); - auto info = nodes->get(node_id); - - if (!info) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node not found"); - } - - bool is_primary = false; - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto primary = current_consensus->primary(); - if (primary.has_value() && primary.value() == node_id) - { - is_primary = true; - } - } - auto& ni = info.value(); - return make_success(GetNode::Out{ - node_id, - ni.status, - is_primary, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(node_id).value_or(0)}); + make_command_endpoint( + "/self_signed_certificate", + HTTP_GET, + json_command_adapter(get_self_signed_certificate), + no_auth_required) + .set_forwarding_required(endpoints::ForwardingRequired::Never) + .set_auto_schema() + .install(); + + auto get_node_info = [this](auto& args, nlohmann::json&& json) { + return this->get_node_info(args, std::move(json)); }; make_read_only_endpoint( "/network/nodes/{node_id}", @@ -1335,55 +2015,8 @@ namespace ccf .set_auto_schema() .install(); - auto get_self_node = [this](auto& args, nlohmann::json&&) { - auto node_id = this->context.get_node_id(); - auto nodes = args.tx.ro(this->network.nodes); - auto info = nodes->get(node_id); - - bool is_primary = false; - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto primary = current_consensus->primary(); - if (primary.has_value() && primary.value() == node_id) - { - is_primary = true; - } - } - - if (info.has_value()) - { - // Answers from the KV are preferred, as they are more up-to-date, - // especially status and node_data. - auto& ni = info.value(); - return make_success(GetNode::Out{ - node_id, - ni.status, - is_primary, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(node_id).value_or(0)}); - } - - // If the node isn't in its KV yet, fall back to configuration - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - } - const auto& node_startup_config = - node_configuration_subsystem->get().node_config; - return make_success(GetNode::Out{ - node_id, - ccf::NodeStatus::PENDING, - is_primary, - node_startup_config.network.rpc_interfaces, - node_configuration_subsystem->get().node_data, - 0}); + auto get_self_node = [this](auto& args, nlohmann::json&& json) { + return this->get_self_node(args, std::move(json)); }; make_read_only_endpoint( "/network/nodes/self", @@ -1394,44 +2027,8 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_primary_node = [this](auto& args, nlohmann::json&&) { - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto primary_id = current_consensus->primary(); - if (!primary_id.has_value()) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Primary unknown"); - } - - auto nodes = args.tx.ro(this->network.nodes); - auto info = nodes->get(primary_id.value()); - if (!info) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node not found"); - } - - auto& ni = info.value(); - return make_success(GetNode::Out{ - primary_id.value(), - ni.status, - true, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(primary_id.value()) - .value_or(0)}); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No configured consensus"); + auto get_primary_node = [this](auto& args, nlohmann::json&& json) { + return this->get_primary_node(args, std::move(json)); }; make_read_only_endpoint( "/network/nodes/primary", @@ -1441,46 +2038,7 @@ namespace ccf .set_auto_schema() .install(); - auto head_primary = [this](auto& args) { - if (this->node_operation.can_replicate()) - { - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - } - else - { - auto* current_consensus = get_consensus(); - if (current_consensus == nullptr) - { - args.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Consensus not initialised"); - return; - } - - auto primary_id = current_consensus->primary(); - if (!primary_id.has_value()) - { - args.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Primary unknown"); - return; - } - - const auto address = node::get_redirect_address_for_node( - args, args.tx, primary_id.value()); - if (!address.has_value()) - { - return; - } - - args.rpc_ctx->set_response_header( - http::headers::LOCATION, - fmt::format("https://{}/node/primary", address.value())); - args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT); - } - }; + auto head_primary = [this](auto& args) { this->head_primary(args); }; make_read_only_endpoint( "/primary", HTTP_HEAD, head_primary, no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) @@ -1489,60 +2047,19 @@ namespace ccf "Redirect to the current primary node.") .install(); - auto get_primary = [this](auto& args) { - if (this->node_operation.can_replicate()) - { - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - return; - } - - args.rpc_ctx->set_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node is not primary"); - }; + auto get_primary = [this](auto& args) { this->get_primary(args); }; make_read_only_endpoint( "/primary", HTTP_GET, get_primary, no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_backup = [this](auto& args) { - if (!this->node_operation.can_replicate()) - { - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - return; - } - - args.rpc_ctx->set_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node is not backup"); - }; + auto get_backup = [this](auto& args) { this->get_backup(args); }; make_read_only_endpoint("/backup", HTTP_GET, get_backup, no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto consensus_config = [this](auto& /*args*/, nlohmann::json&&) { - // Query node for configurations, separate current from pending - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto cfg = current_consensus->get_latest_configuration(); - ConsensusConfig cc; - for (auto& [nid, ninfo] : cfg) - { - cc.emplace( - nid.value(), - ConsensusNodeConfig{ - fmt::format("{}:{}", ninfo.hostname, ninfo.port)}); - } - return make_success(cc); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No configured consensus"); + auto consensus_config = [this](auto& args, nlohmann::json&& json) { + return this->consensus_config(args, std::move(json)); }; make_command_endpoint( @@ -1554,18 +2071,8 @@ namespace ccf .set_auto_schema() .install(); - auto consensus_state = [this](auto& /*args*/, nlohmann::json&&) { - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - return make_success( - ConsensusConfigDetails{current_consensus->get_details()}); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No configured consensus"); + auto consensus_state = [this](auto& args, nlohmann::json&& json) { + return this->consensus_state(args, std::move(json)); }; make_command_endpoint( @@ -1577,15 +2084,7 @@ namespace ccf .set_auto_schema() .install(); - auto node_metrics = [this](auto& args) { - NodeMetrics nm; - nm.sessions = node_operation.get_session_metrics(); - - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - args.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); - args.rpc_ctx->set_response_body(nlohmann::json(nm).dump()); - }; + auto node_metrics = [this](auto& args) { this->node_metrics(args); }; make_command_endpoint( "/metrics", HTTP_GET, node_metrics, no_auth_required) @@ -1593,28 +2092,8 @@ namespace ccf .set_auto_schema() .install(); - auto js_metrics = [this](auto& args, nlohmann::json&&) { - auto bytecode_map = args.tx.ro(this->network.modules_quickjs_bytecode); - auto version_val = args.tx.ro(this->network.modules_quickjs_version); - uint64_t bytecode_size = 0; - bytecode_map->foreach( - [&bytecode_size](const auto&, const auto& bytecode) { - bytecode_size += bytecode.size(); - return true; - }); - auto js_engine_map = args.tx.ro(this->network.js_engine); - JavaScriptMetrics m; - m.bytecode_size = bytecode_size; - m.bytecode_used = - version_val->get() == std::string(ccf::quickjs_version); - - auto options = js_engine_map->get().value_or(ccf::JSRuntimeOptions{}); - m.max_stack_size = options.max_stack_bytes; - m.max_heap_size = options.max_heap_bytes; - m.max_execution_time = options.max_execution_time_ms; - m.max_cached_interpreters = options.max_cached_interpreters; - - return m; + auto js_metrics = [this](auto& args, nlohmann::json&& json) { + return this->js_metrics(args, std::move(json)); }; make_read_only_endpoint( @@ -1625,13 +2104,8 @@ namespace ccf .set_auto_schema() .install(); - auto version = [](auto&, nlohmann::json&&) { - GetVersion::Out result; - result.ccf_version = ccf::ccf_version; - result.quickjs_version = ccf::quickjs_version; - result.unsafe = false; - - return make_success(result); + auto version = [](auto& args, nlohmann::json&& json) { + return NodeEndpoints::version(args, std::move(json)); }; make_command_endpoint( @@ -1641,165 +2115,7 @@ namespace ccf .install(); auto create = [this](auto& ctx, nlohmann::json&& params) { - LOG_INFO_FMT("Processing create RPC"); - - bool recovering = node_operation.is_reading_public_ledger(); - - // This endpoint can only be called once, directly from the starting - // node for the genesis or end of public recovery transaction to - // initialise the service - if (!node_operation.is_in_initialised_state() && !recovering) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::InternalError, - "Node is not in initial state."); - } - - const auto in = params.get(); - - if (InternalTablesAccess::is_service_created(ctx.tx, in.service_cert)) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::InternalError, - "Service is already created."); - } - - InternalTablesAccess::create_service( - ctx.tx, in.service_cert, in.create_txid, in.service_data, recovering); - - if (recovering) - { - // Recovery starts with a fresh consensus configuration, so previous - // service nodes can be removed immediately. - InternalTablesAccess::remove_previous_service_nodes(ctx.tx); - } - - // Genesis transaction (i.e. not after recovery) - if (in.genesis_info.has_value()) - { - // Note that it is acceptable to start a network without any member - // having a recovery share. The service will check that at least one - // recovery member is added before the service is opened. - for (const auto& info : in.genesis_info->members) - { - InternalTablesAccess::add_member(ctx.tx, info); - } - - InternalTablesAccess::init_configuration( - ctx.tx, in.genesis_info->service_configuration); - InternalTablesAccess::set_constitution( - ctx.tx, in.genesis_info->constitution); - } - else - { - // On recovery, force a new ledger chunk - auto* tx_ = static_cast(&ctx.tx); - if (tx_ == nullptr) - { - throw std::logic_error("Could not cast tx to CommittableTx"); - } - tx_->set_tx_flag( - ccf::kv::CommittableTx::TxFlag::LEDGER_CHUNK_BEFORE_THIS_TX); - } - - auto endorsed_certificates = - ctx.tx.rw(network.node_endorsed_certificates); - endorsed_certificates->put(in.node_id, in.node_endorsed_certificate); - - NodeInfo node_info = { - in.node_info_network, - {in.quote_info}, - in.public_encryption_key, - NodeStatus::TRUSTED, - std::nullopt, - in.measurement.hex_str(), - in.certificate_signing_request, - in.public_key, - in.node_data}; - InternalTablesAccess::add_node(ctx.tx, in.node_id, node_info); - - if (in.sealing_recovery_data.has_value()) - { - const auto& [sealing_keys, sealing_recovery_name] = - in.sealing_recovery_data.value(); - auto* sealed_recovery_keys = ctx.tx.template rw( - Tables::SEALED_RECOVERY_KEYS); - sealed_recovery_keys->put(in.node_id, sealing_keys); - - auto* local_sealing_node_id_map = - ctx.tx.template rw( - Tables::SEALING_RECOVERY_NAMES); - local_sealing_node_id_map->put(sealing_recovery_name, in.node_id); - } - - node_operation.shuffle_sealed_shares(ctx.tx); - - if ( - in.quote_info.format != QuoteFormat::amd_sev_snp_v1 || - !in.snp_uvm_endorsements.has_value()) - { - // For improved serviceability on SNP, do not record trusted - // measurements if UVM endorsements are available - InternalTablesAccess::trust_node_measurement( - ctx.tx, in.measurement, in.quote_info.format); - } - - switch (in.quote_info.format) - { - case QuoteFormat::insecure_virtual: - { - auto host_data = AttestationProvider::get_host_data(in.quote_info); - if (host_data.has_value()) - { - InternalTablesAccess::trust_node_virtual_host_data( - ctx.tx, host_data.value()); - } - else - { - LOG_FAIL_FMT("Unable to extract host data from virtual quote"); - } - break; - } - - case QuoteFormat::amd_sev_snp_v1: - { - auto host_data = - AttestationProvider::get_host_data(in.quote_info).value(); - InternalTablesAccess::trust_node_snp_host_data( - ctx.tx, host_data, in.snp_security_policy); - - InternalTablesAccess::trust_node_uvm_endorsements( - ctx.tx, in.snp_uvm_endorsements, recovering); - - auto attestation = - AttestationProvider::get_snp_attestation_report(in.quote_info) - .value(); - InternalTablesAccess::trust_node_snp_tcb_version( - ctx.tx, attestation, recovering); - break; - } - case QuoteFormat::oe_sgx_v1: - { - break; - } - } - - std::optional digest = - ccf::get_create_tx_claims_digest(ctx.tx); - if (digest.has_value()) - { - auto digest_value = digest.value(); - ctx.rpc_ctx->set_claims_digest(std::move(digest_value)); - } - - this->node_operation.recovery_decision_protocol().reset_state(ctx.tx); - this->node_operation.recovery_decision_protocol().try_start( - ctx.tx, recovering); - - LOG_INFO_FMT("Created service"); - return make_success(true); + return this->create(ctx, std::move(params)); }; make_endpoint( "/create", HTTP_POST, json_adapter(create), {self_cert_auth_policy}) @@ -1808,89 +2124,7 @@ namespace ccf // Only called from node. See node_state.h. auto refresh_jwt_keys = [this](auto& ctx, nlohmann::json&& body) { - // All errors are server errors since the client is the server. - - auto* current_consensus = get_consensus(); - auto primary_id = current_consensus->primary(); - if (!primary_id.has_value()) - { - LOG_FAIL_FMT("JWT key auto-refresh: primary unknown"); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Primary is unknown"); - } - - const auto& sig_auth_ident = - ctx.template get_caller(); - if (primary_id.value() != sig_auth_ident.node_id) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: request does not originate from primary"); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Request does not originate from primary."); - } - - SetJwtPublicSigningKeys parsed; - try - { - parsed = body.get(); - } - catch (const ccf::JsonParseError& e) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Unable to parse body."); - } - - auto issuers = ctx.tx.ro(this->network.jwt_issuers); - auto issuer_metadata_ = issuers->get(parsed.issuer); - if (!issuer_metadata_.has_value()) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: {} is not a valid issuer", parsed.issuer); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format("{} is not a valid issuer.", parsed.issuer)); - } - auto& issuer_metadata = issuer_metadata_.value(); - - if (!issuer_metadata.auto_refresh) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: {} does not have auto_refresh enabled", - parsed.issuer); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "{} does not have auto_refresh enabled.", parsed.issuer)); - } - - if (!set_jwt_public_signing_keys( - ctx.tx, - "", - parsed.issuer, - issuer_metadata, - parsed.jwks)) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: error while storing signing keys for issuer " - "{}", - parsed.issuer); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Error while storing signing keys for issuer {}.", - parsed.issuer)); - } - - return make_success(true); + return this->refresh_jwt_keys(ctx, std::move(body)); }; make_endpoint( "/jwt_keys/refresh", @@ -1900,15 +2134,9 @@ namespace ccf .set_openapi_hidden(true) .install(); - auto get_jwt_metrics = - [this](auto& /*args*/, const nlohmann::json& /*params*/) { - JWTRefreshMetrics metrics; - { - ccf::ds::MutexGuard guard(jwt_refresh_metrics_lock); - metrics = jwt_refresh_metrics; - } - return make_success(metrics); - }; + auto get_jwt_metrics = [this](auto& args, const nlohmann::json& params) { + return this->get_jwt_metrics(args, params); + }; make_read_only_endpoint( "/jwt_keys/refresh/metrics", HTTP_GET, @@ -1918,8 +2146,8 @@ namespace ccf .install(); auto service_config_handler = - [this](auto& args, const nlohmann::json& /*params*/) { - return make_success(args.tx.ro(network.config)->get()); + [this](auto& args, const nlohmann::json& params) { + return this->service_config_handler(args, params); }; make_endpoint( "/service/configuration", @@ -1930,11 +2158,10 @@ namespace ccf .set_auto_schema() .install(); - auto list_indexing_strategies = [this]( - auto& /*args*/, - const nlohmann::json& /*params*/) { - return make_success(this->context.get_indexing_strategies().describe()); - }; + auto list_indexing_strategies = + [this](auto& args, const nlohmann::json& params) { + return this->list_indexing_strategies(args, params); + }; make_endpoint( "/index/strategies", @@ -1946,28 +2173,7 @@ namespace ccf .install(); auto get_ready_app = [this](ccf::endpoints::CommandEndpointContext& ctx) { - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - return; - } - if ( - !node_configuration_subsystem->has_received_stop_notice() && - this->node_operation.is_part_of_network() && - this->node_operation.is_user_frontend_open()) - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); - } - else - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); - } - return; + this->get_ready_app(ctx); }; make_command_endpoint( "/ready/app", HTTP_GET, get_ready_app, no_auth_required) @@ -1979,28 +2185,7 @@ namespace ccf .install(); auto get_ready_gov = [this](ccf::endpoints::CommandEndpointContext& ctx) { - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - return; - } - if ( - !node_configuration_subsystem->has_received_stop_notice() && - this->node_operation.is_accessible_to_members() && - this->node_operation.is_member_frontend_open()) - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); - } - else - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); - } - return; + this->get_ready_gov(ctx); }; make_command_endpoint( "/ready/gov", HTTP_GET, get_ready_gov, no_auth_required) @@ -2011,12 +2196,8 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto create_snapshot = [this](auto& args, nlohmann::json&&) { - auto* snapshot_create = args.tx.template rw( - ccf::Tables::SNAPSHOT_CREATE); - snapshot_create->touch(); - this->node_operation.trigger_snapshot(args.tx); - return make_success(); + auto create_snapshot = [this](auto& args, nlohmann::json&& json) { + return this->create_snapshot(args, std::move(json)); }; make_endpoint( "/snapshot:create", @@ -2032,13 +2213,8 @@ namespace ccf ccf::node::init_file_serving_handlers(*this, context); - auto historical_cache_info = [this]( - [[maybe_unused]] auto& args, - [[maybe_unused]] nlohmann::json&&) { - GetHistoricalCacheInfo::Out result{}; - result.estimated_size = - this->context.get_historical_state().get_estimated_store_cache_size(); - return make_success(result); + auto historical_cache_info = [this](auto& args, nlohmann::json&& json) { + return this->historical_cache_info(args, std::move(json)); }; make_read_only_endpoint( "/historical_cache", From 508793b57340f8bfdf0cf097a8cf62964ae9d0f2 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 5 Oct 2026 11:45:54 +0100 Subject: [PATCH 8/9] Remove redundant handler extraction comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/apps/programmability/programmability.cpp | 3 --- src/node/gov/handlers/acks.h | 3 --- src/node/rpc/node_frontend.h | 3 --- 3 files changed, 9 deletions(-) diff --git a/samples/apps/programmability/programmability.cpp b/samples/apps/programmability/programmability.cpp index 06a8feca762..159a34f83ab 100644 --- a/samples/apps/programmability/programmability.cpp +++ b/samples/apps/programmability/programmability.cpp @@ -276,9 +276,6 @@ namespace programmabilityapp } } - // Endpoint handlers registered by the constructor, via forwarding - // lambdas. Kept out of the constructor so each handler's complexity is - // measured on its own. void put(ccf::endpoints::EndpointContext& ctx) { std::string key; diff --git a/src/node/gov/handlers/acks.h b/src/node/gov/handlers/acks.h index e2a842bfc59..120f1b17f12 100644 --- a/src/node/gov/handlers/acks.h +++ b/src/node/gov/handlers/acks.h @@ -26,9 +26,6 @@ namespace ccf::gov::endpoints namespace detail { - // Endpoint handlers registered by init_ack_handlers(), via forwarding - // lambdas. Kept out of the registration function so each handler's - // complexity is measured on its own. template inline void get_state_digest(Ctx& ctx, ApiVersion api_version) { diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index bf00960b87f..2801a2a5145 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -467,9 +467,6 @@ namespace ccf } } - // Endpoint handlers registered by init_handlers(), via forwarding - // lambdas. Kept out of init_handlers() so each handler's cognitive - // complexity is measured on its own. template auto accept(T& args, const nlohmann::json& params) { From 8731f00b3980681e07705b65d56d45c9a6af9b1b Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 5 Oct 2026 13:39:22 +0100 Subject: [PATCH 9/9] Keep programmability PR scoped to the sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/node/rpc/node_frontend.h | 2623 ++++++++++++++++------------------ 1 file changed, 1225 insertions(+), 1398 deletions(-) diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 2801a2a5145..882b39eb438 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -467,1391 +467,319 @@ namespace ccf } } - template - auto accept(T& args, const nlohmann::json& params) + public: + NodeEndpoints(NetworkState& network_, ccf::AbstractNodeContext& context_) : + CommonEndpointRegistry(get_actor_prefix(ActorsType::nodes), context_), + network(network_), + node_operation(*context_.get_subsystem()) { - const auto in = params.get(); - - // Not part of network => Internal error - if ( - !this->node_operation.is_part_of_network() && - !this->node_operation.is_part_of_public_network() && - !this->node_operation.is_reading_private_ledger()) - { - const std::string payload = - "Target node should be part of network to accept new nodes."; - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - payload); - } + openapi_info.title = "CCF Public Node API"; + openapi_info.description = + "This API provides public, uncredentialed access to service and node " + "state."; + openapi_info.document_version = "5.0.8"; + } - // No service => Internal error - auto service = args.tx.rw(this->network.service); - auto active_service = service->get(); - if (!active_service.has_value()) - { - const std::string payload = - "No service is available to accept new node."; - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - payload); - } + // NOLINTNEXTLINE(readability-function-cognitive-complexity) + void init_handlers() override + { + CommonEndpointRegistry::init_handlers(); - auto* current_consensus = get_consensus(); - const auto should_redirect_to_primary = - current_consensus != nullptr && !this->node_operation.can_replicate(); - auto redirect_to_primary = [&]() { - auto primary_id = current_consensus->primary(); - if (primary_id.has_value()) - { - const auto address = node::get_redirect_address_for_node( - args, args.tx, primary_id.value()); - if (!address.has_value()) - { - LOG_INFO_FMT( - "Join request rejected: no redirect address for " - "primary {}", - primary_id.value()); - return already_populated_response(); - } + const auto self_cert_auth_policy = + std::make_shared(this->context); - args.rpc_ctx->set_response_header( - http::headers::LOCATION, - fmt::format("https://{}/node/join", address.value())); + auto accept = [this](auto& args, const nlohmann::json& params) { + const auto in = params.get(); + // Not part of network => Internal error + if ( + !this->node_operation.is_part_of_network() && + !this->node_operation.is_part_of_public_network() && + !this->node_operation.is_reading_private_ledger()) + { const std::string payload = - "Node is not primary; cannot handle write"; - LOG_INFO_FMT( - "Join request redirected to primary {} at {}: {}", - primary_id.value(), - address.value(), - payload); + "Target node should be part of network to accept new nodes."; + LOG_INFO_FMT("Join request rejected: {}", payload); return make_error( - HTTP_STATUS_PERMANENT_REDIRECT, - ccf::errors::NodeCannotHandleRequest, + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, payload); } - const std::string payload = "Primary unknown"; - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - payload); - }; - - auto nodes = args.tx.ro(network.nodes); - - // If already joined => return equivalent response - auto existing_node_info = check_node_exists( - args.tx, args.rpc_ctx->get_session_context()->caller_cert); - if (existing_node_info.has_value()) - { - JoinNetworkNodeToNode::Out rep; - - // If the node already exists, return network secrets if is already - // trusted. Otherwise, only return its status - auto node_info = nodes->get(existing_node_info->node_id); - auto node_status = - node_info->status; // NOLINT(bugprone-unchecked-optional-access) - rep.node_status = node_status; - rep.node_id = existing_node_info->node_id; - if (node_status == NodeStatus::TRUSTED) + // No service => Internal error + auto service = args.tx.rw(this->network.service); + auto active_service = service->get(); + if (!active_service.has_value()) { - rep.network_info = JoinNetworkNodeToNode::Out::NetworkInfo( - node_operation.is_part_of_public_network(), - node_operation.get_last_recovered_signed_idx(), - this->network.ledger_secrets->get( - args.tx, existing_node_info->ledger_secret_seqno), - *this->network.identity, - active_service->status, - existing_node_info->endorsed_certificate, - node_operation.get_cose_signatures_config()); - - LOG_DEBUG_FMT( - "Join request accepted: {} already marked as TRUSTED", - existing_node_info->node_id); - return make_success(rep); + const std::string payload = + "No service is available to accept new node."; + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + payload); } - if (node_status == NodeStatus::PENDING) - { - const auto pending_node_timeout = get_pending_node_timeout(); - if (!pending_node_timeout.has_value()) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfiguration subsystem is not available"); - } - - if (pending_node_timeout.value() > std::chrono::milliseconds::zero()) + auto* current_consensus = get_consensus(); + const auto should_redirect_to_primary = + current_consensus != nullptr && !this->node_operation.can_replicate(); + auto redirect_to_primary = [&]() { + auto primary_id = current_consensus->primary(); + if (primary_id.has_value()) { - if (should_redirect_to_primary) + const auto address = node::get_redirect_address_for_node( + args, args.tx, primary_id.value()); + if (!address.has_value()) { - return redirect_to_primary(); + LOG_INFO_FMT( + "Join request rejected: no redirect address for " + "primary {}", + primary_id.value()); + return already_populated_response(); } - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - node_info->pending_last_seen = current_time_ms(); - args.tx.rw(network.nodes) - ->put( - existing_node_info->node_id, - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - node_info.value()); - } - - // Only return node status and ID - LOG_DEBUG_FMT( - "Join request accepted: {} already marked as PENDING", - existing_node_info->node_id); - return make_success(rep); - } - - const std::string payload = fmt::format( - "Joining node is not in expected state ({}).", node_status); - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidNodeState, payload); - } - - // Not the primary => Redirect if possible to primary - if (should_redirect_to_primary) - { - return redirect_to_primary(); - } + args.rpc_ctx->set_response_header( + http::headers::LOCATION, + fmt::format("https://{}/node/join", address.value())); - // Joiner's snapshot too old => StartupSeqnoIsOld - // (causes joiner to fetch a more recent snapshot) - // - // The joiner always wants to use the most recent snapshot. - // However this will result in the joiner chasing the primary if - // snapshot production period ~= snapshot fetching delay - // - // So we have hysteresis in the fetching constraint: - // If the joiner has already fetched a snapshot: joiner seqno > startup - // snapshot seqno Otherwise: joiner seqno > latest snapshot on disk - // seqno - auto this_startup_seqno = - this->node_operation.get_startup_snapshot_seqno(); - ccf::kv::Version required_seqno = this_startup_seqno; - // If the joiner does not enable fetching, or is a legacy node, - // join_fetch_count is unset and we should use the required bound to - // prevent it chasing the primary. - // Otherwise if this is the first request, use the preferred bound - bool using_preferred_bound = - (in.join_fetch_count.has_value() && in.join_fetch_count.value() == 0); - if (using_preferred_bound) - { - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (node_configuration_subsystem != nullptr) - { - const auto& snapshots_config = - node_configuration_subsystem->get().node_config.snapshots; - const auto latest_committed_snapshot = - snapshots::find_latest_committed_snapshot_in_directory( - snapshots_config.directory); - if (latest_committed_snapshot.has_value()) - { - const auto latest_snapshot_seqno = - snapshots::get_snapshot_idx_from_file_name( - latest_committed_snapshot->filename().string()); - required_seqno = std::max( - required_seqno, - static_cast(latest_snapshot_seqno)); + const std::string payload = + "Node is not primary; cannot handle write"; + LOG_INFO_FMT( + "Join request redirected to primary {} at {}: {}", + primary_id.value(), + address.value(), + payload); + return make_error( + HTTP_STATUS_PERMANENT_REDIRECT, + ccf::errors::NodeCannotHandleRequest, + payload); } - } - } - if ( - in.startup_seqno.has_value() && - in.startup_seqno.value() < required_seqno) - { - // Make sure that the joiner's snapshot is more recent than this - // node's snapshot. Otherwise, the joiner may not be given all the - // ledger secrets required to replay historical transactions. - const std::string payload = fmt::format( - "Node requested to join from seqno {} which is older than this " - "node {} {}. A snapshot at least as recent as {} must " - "be used instead.", - in.startup_seqno.value(), - using_preferred_bound ? "latest_on_disk_seqno" : "startup_seqno", - required_seqno, - required_seqno); - LOG_INFO_FMT("Join request rejected: {}", payload); - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::StartupSeqnoIsOld, payload); - } - - auto joining_node_status = NodeStatus::PENDING; - // If the service is opening, new nodes are trusted straight away - if ( - active_service->status == ServiceStatus::OPENING || - active_service->status == ServiceStatus::RECOVERING) - { - joining_node_status = NodeStatus::TRUSTED; - } - - return add_node( - args.tx, - args.rpc_ctx->get_session_context()->caller_cert, - in, - joining_node_status, - active_service->status); - } - - template - auto remove_expired_pending(T& ctx, nlohmann::json&& /*params*/) - { - const auto pending_node_timeout = get_pending_node_timeout(); - if (!pending_node_timeout.has_value()) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfiguration subsystem is not available"); - } - - if (pending_node_timeout.value() <= std::chrono::milliseconds::zero()) - { - return make_success(true); - } - - const auto now = current_time_ms(); - auto nodes = ctx.tx.rw(network.nodes); - std::map untimestamped_pending_nodes; - std::vector expired_pending_nodes; - nodes->foreach([&](const auto& node_id, const auto& node_info) { - if (node_info.status != NodeStatus::PENDING) - { - return true; - } - - if ( - !node_info.pending_last_seen.has_value() || - node_info.pending_last_seen.value() < 0 || - node_info.pending_last_seen.value() > now) - { - auto updated_node_info = node_info; - updated_node_info.pending_last_seen = now; - untimestamped_pending_nodes.emplace( - node_id, std::move(updated_node_info)); - } - else if ( - now - node_info.pending_last_seen.value() >= - pending_node_timeout.value().count()) - { - expired_pending_nodes.push_back(node_id); - } - - return true; - }); - - for (const auto& [node_id, node_info] : untimestamped_pending_nodes) - { - nodes->put(node_id, node_info); - } - - for (const auto& node_id : expired_pending_nodes) - { - LOG_INFO_FMT("Removing expired Pending node {}", node_id); - InternalTablesAccess::remove_node(ctx.tx, node_id); - } - - return make_success(true); - } - - template - auto set_retired_committed(T& ctx, nlohmann::json&& /*params*/) - { - auto nodes = ctx.tx.rw(network.nodes); - nodes->foreach([&nodes](const auto& node_id, auto node_info) { - auto gc_node = nodes->get_globally_committed(node_id); - if ( - gc_node.has_value() && gc_node->status == ccf::NodeStatus::RETIRED && - !node_info.retired_committed) - { - // Set retired_committed on nodes for which RETIRED status - // has been committed. - node_info.retired_committed = true; - nodes->put(node_id, node_info); - - LOG_DEBUG_FMT("Setting retired_committed on node {}", node_id); - } - return true; - }); - - return make_success(); - } - - template - auto get_state(T& args, nlohmann::json&& /*params*/) - { - GetState::Out result; - auto [s, rts, lrs] = this->node_operation.state(); - result.node_id = this->context.get_node_id(); - result.state = s; - result.recovery_target_seqno = rts; - result.last_recovered_seqno = lrs; - result.startup_seqno = this->node_operation.get_startup_snapshot_seqno(); - - // Read last signed seqno from both raw and COSE signature tables - auto signatures = args.tx.template ro(Tables::SIGNATURES); - auto sig = signatures->get(); - - ccf::kv::Version raw_seqno = 0; - if (sig.has_value()) - { - raw_seqno = sig.value().seqno; - } - - ccf::kv::Version cose_seqno = 0; - auto cose_signatures = - args.tx.template ro(Tables::COSE_SIGNATURES); - auto cose_sig = cose_signatures->get(ccf::IdentityType::CLASSICAL); - if (cose_sig.has_value() && !cose_sig->empty()) - { - auto receipt = ccf::cose::decode_ccf_receipt(cose_sig.value(), false); - auto txid = ccf::TxID::from_str(receipt.phdr.ccf.txid); - if (!txid.has_value()) - { - throw std::logic_error(fmt::format( - "Failed to parse txid from COSE signature: {}", - receipt.phdr.ccf.txid)); - } - cose_seqno = txid->seqno; - } - - result.last_signed_seqno = std::max(raw_seqno, cose_seqno); - - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - } - result.stop_notice = - node_configuration_subsystem->has_received_stop_notice(); - - return make_success(result); - } - template - auto get_quote(T& args, nlohmann::json&& /*params*/) - { - QuoteInfo node_quote_info; - const auto result = get_quote_for_this_node_v1(args.tx, node_quote_info); - if (result == ApiResult::OK) - { - Quote q; - q.node_id = context.get_node_id(); - q.raw = node_quote_info.quote; - q.endorsements = node_quote_info.endorsements; - q.format = node_quote_info.format; - q.uvm_endorsements = node_quote_info.uvm_endorsements; + const std::string payload = "Primary unknown"; + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + payload); + }; auto nodes = args.tx.ro(network.nodes); - auto node_info = nodes->get(context.get_node_id()); - if (node_info.has_value() && node_info->code_digest.has_value()) - { - q.measurement = node_info->code_digest.value(); - } - else + + // If already joined => return equivalent response + auto existing_node_info = check_node_exists( + args.tx, args.rpc_ctx->get_session_context()->caller_cert); + if (existing_node_info.has_value()) { - auto measurement = - AttestationProvider::get_measurement(node_quote_info); - if (measurement.has_value()) - { - q.measurement = measurement.value().hex_str(); - } - else + JoinNetworkNodeToNode::Out rep; + + // If the node already exists, return network secrets if is already + // trusted. Otherwise, only return its status + auto node_info = nodes->get(existing_node_info->node_id); + auto node_status = node_info->status; + rep.node_status = node_status; + rep.node_id = existing_node_info->node_id; + if (node_status == NodeStatus::TRUSTED) { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InvalidQuote, - "Failed to extract code id from node quote."); + rep.network_info = JoinNetworkNodeToNode::Out::NetworkInfo( + node_operation.is_part_of_public_network(), + node_operation.get_last_recovered_signed_idx(), + this->network.ledger_secrets->get( + args.tx, existing_node_info->ledger_secret_seqno), + *this->network.identity, + active_service->status, + existing_node_info->endorsed_certificate, + node_operation.get_cose_signatures_config()); + + LOG_DEBUG_FMT( + "Join request accepted: {} already marked as TRUSTED", + existing_node_info->node_id); + return make_success(rep); } - } - - return make_success(q); - } - - if (result == ApiResult::NotFound) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Could not find node quote."); - } - - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format("Error code: {}", ccf::api_result_to_str(result))); - } - - template - auto get_quotes(T& args, nlohmann::json&& /*params*/) - { - GetQuotes::Out result; - auto nodes = args.tx.ro(network.nodes); - nodes->foreach( - ["es = result.quotes](const auto& node_id, const auto& node_info) { - if (node_info.status == ccf::NodeStatus::TRUSTED) + if (node_status == NodeStatus::PENDING) { - Quote q; - q.node_id = node_id; - q.raw = node_info.quote_info.quote; - q.endorsements = node_info.quote_info.endorsements; - q.format = node_info.quote_info.format; - q.uvm_endorsements = node_info.quote_info.uvm_endorsements; - - if (node_info.code_digest.has_value()) + const auto pending_node_timeout = get_pending_node_timeout(); + if (!pending_node_timeout.has_value()) { - q.measurement = node_info.code_digest.value(); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfiguration subsystem is not available"); } - else + + if ( + pending_node_timeout.value() > std::chrono::milliseconds::zero()) { - auto measurement = - AttestationProvider::get_measurement(node_info.quote_info); - if (measurement.has_value()) + if (should_redirect_to_primary) { - q.measurement = measurement.value().hex_str(); + return redirect_to_primary(); } - } - quotes.emplace_back(q); - } - return true; - }); - - return make_success(result); - } - - template - auto get_attestations(T& args, nlohmann::json&& params) - { - auto res = get_quotes(args, std::move(params)); - const auto* body = std::get_if(&res); - if (body != nullptr) - { - auto result = nlohmann::json::object(); - result["attestations"] = (*body)["quotes"]; - return make_success(result); - } - - return res; - } - - template - auto network_status(T& args, nlohmann::json&& /*params*/) - { - GetNetworkInfo::Out out; - auto service = args.tx.ro(network.service); - auto service_state = service->get(); - if (service_state.has_value()) - { - const auto& service_value = service_state.value(); - out.service_status = service_value.status; - out.service_certificate = service_value.cert; - out.recovery_count = service_value.recovery_count.value_or(0); - out.service_data = service_value.service_data; - out.current_service_create_txid = - service_value.current_service_create_txid; - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - out.current_view = current_consensus->get_view(); - auto primary_id = current_consensus->primary(); - if (primary_id.has_value()) - { - out.primary_id = primary_id.value(); - } - } - return make_success(out); - } - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Service state not available."); - } - - template - static auto service_previous_identity(T& args, nlohmann::json&& /*params*/) - { - auto psi_handle = args.tx.template ro( - ccf::Tables::PREVIOUS_SERVICE_IDENTITY); - const auto psi = psi_handle->get(); - if (psi.has_value()) - { - GetServicePreviousIdentity::Out out; - out.previous_service_identity = psi.value(); - return make_success(out); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "This service is not a recovery of a previous service."); - } - - template - auto get_nodes(T& args, nlohmann::json&& /*params*/) - { - const auto parsed_query = - http::parse_query(args.rpc_ctx->get_request_query()); - - std::string error_string; // Ignored - all params are optional - const auto host = http::get_query_value_opt( - parsed_query, "host", error_string); - const auto port = http::get_query_value_opt( - parsed_query, "port", error_string); - const auto status_str = http::get_query_value_opt( - parsed_query, "status", error_string); - - std::optional status; - if (status_str.has_value()) - { - // Convert the query argument to a JSON string, try to parse it as - // a NodeStatus, return an error if this doesn't work - try - { - status = nlohmann::json(status_str.value()).get(); - } - catch (const ccf::JsonParseError& e) - { - return ccf::make_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidQueryParameterValue, - fmt::format( - "Query parameter '{}' is not a valid node status", - status_str.value())); - } - } - - GetNodes::Out out; - - auto nodes = args.tx.ro(this->network.nodes); - auto* current_consensus = get_consensus(); - nodes->foreach([host, port, status, &out, nodes, current_consensus]( - const NodeId& nid, const NodeInfo& ni) { - if (status.has_value() && status.value() != ni.status) - { - return true; - } - - // Match on any interface - bool is_matched = false; - for (auto const& interface : ni.rpc_interfaces) - { - const auto& [pub_host, pub_port] = - split_net_address(interface.second.published_address); - - if ( - (!host.has_value() || host.value() == pub_host) && - (!port.has_value() || port.value() == pub_port)) - { - is_matched = true; - break; - } - } - - if (!is_matched) - { - return true; - } - - bool is_primary = false; - if (current_consensus != nullptr) - { - is_primary = current_consensus->primary() == nid; - } - - out.nodes.push_back( - {nid, - ni.status, - is_primary, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(nid).value_or(0)}); - return true; - }); - - return make_success(out); - } - - template - auto get_removable_nodes(T& args, nlohmann::json&& /*params*/) - { - GetNodes::Out out; - - auto nodes = args.tx.ro(this->network.nodes); - nodes->foreach( - [&out, nodes](const NodeId& node_id, const NodeInfo& /*ni*/) { - // Only nodes whose retire_committed status is committed can be - // safely removed, because any primary elected from here on would - // consider them retired, and would consequently not need their - // input in any quorum. We must therefore read the KV at its - // globally committed watermark, for the purpose of this RPC. Since - // this transaction does not perform a write, it is safe to do this. - auto node = nodes->get_globally_committed(node_id); - if ( - node.has_value() && node->status == ccf::NodeStatus::RETIRED && - node->retired_committed) - { - out.nodes.push_back( - {node_id, - node->status, - false /* is_primary */, - node->rpc_interfaces, - node->node_data, - nodes->get_version_of_previous_write(node_id).value_or(0)}); - } - return true; - }); - - return make_success(out); - } - - template - auto delete_retired_committed_node(T& args, nlohmann::json&& /*params*/) - { - GetNodes::Out out; - - std::string node_id; - std::string error; - if (!get_path_param( - args.rpc_ctx->get_request_path_params(), "node_id", node_id, error)) - { - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); - } - - auto nodes = args.tx.rw(this->network.nodes); - if (!nodes->has(node_id)) - { - return make_error( - HTTP_STATUS_NOT_FOUND, ccf::errors::ResourceNotFound, "No such node"); - } - - // A node's retirement is only complete when the - // transition of retired_committed is itself committed, - // i.e. when the next eligible primary is guaranteed to - // be aware the retirement is committed. - // As a result, the handler must check node info at the - // current committed level, rather than at the end of the - // local suffix. - // While this transaction does execute a write, it specifically - // deletes the value it reads from. It is therefore safe to - // execute on the basis of a potentially stale read-set, - // which get_globally_committed() typically produces. - auto node = nodes->get_globally_committed(node_id); - if ( - node.has_value() && node->status == ccf::NodeStatus::RETIRED && - node->retired_committed) - { - InternalTablesAccess::remove_node(args.tx, node_id); - } - else - { - return make_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::NodeNotRetiredCommitted, - "Node is not completely retired"); - } - - return make_success(true); - } - - template - auto get_self_signed_certificate(T& /*args*/, nlohmann::json&& /*params*/) - { - return SelfSignedNodeCertificateInfo{ - this->node_operation.get_self_signed_node_certificate()}; - } - - template - auto get_node_info(T& args, nlohmann::json&& /*params*/) - { - std::string node_id; - std::string error; - if (!get_path_param( - args.rpc_ctx->get_request_path_params(), "node_id", node_id, error)) - { - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); - } - - auto nodes = args.tx.ro(this->network.nodes); - auto info = nodes->get(node_id); - - if (!info) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node not found"); - } - - bool is_primary = false; - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto primary = current_consensus->primary(); - if (primary.has_value() && primary.value() == node_id) - { - is_primary = true; - } - } - auto& ni = info.value(); - return make_success(GetNode::Out{ - node_id, - ni.status, - is_primary, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(node_id).value_or(0)}); - } - - template - auto get_self_node(T& args, nlohmann::json&& /*params*/) - { - auto node_id = this->context.get_node_id(); - auto nodes = args.tx.ro(this->network.nodes); - auto info = nodes->get(node_id); - - bool is_primary = false; - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto primary = current_consensus->primary(); - if (primary.has_value() && primary.value() == node_id) - { - is_primary = true; - } - } - - if (info.has_value()) - { - // Answers from the KV are preferred, as they are more up-to-date, - // especially status and node_data. - auto& ni = info.value(); - return make_success(GetNode::Out{ - node_id, - ni.status, - is_primary, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(node_id).value_or(0)}); - } - - // If the node isn't in its KV yet, fall back to configuration - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - } - const auto& node_startup_config = - node_configuration_subsystem->get().node_config; - return make_success(GetNode::Out{ - node_id, - ccf::NodeStatus::PENDING, - is_primary, - node_startup_config.network.rpc_interfaces, - node_configuration_subsystem->get().node_data, - 0}); - } - - template - auto get_primary_node(T& args, nlohmann::json&& /*params*/) - { - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto primary_id = current_consensus->primary(); - if (!primary_id.has_value()) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Primary unknown"); - } - - auto nodes = args.tx.ro(this->network.nodes); - auto info = nodes->get(primary_id.value()); - if (!info) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node not found"); - } - - auto& ni = info.value(); - return make_success(GetNode::Out{ - primary_id.value(), - ni.status, - true, - ni.rpc_interfaces, - ni.node_data, - nodes->get_version_of_previous_write(primary_id.value()) - .value_or(0)}); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No configured consensus"); - } - - template - auto head_primary(T& args) - { - if (this->node_operation.can_replicate()) - { - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - } - else - { - auto* current_consensus = get_consensus(); - if (current_consensus == nullptr) - { - args.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Consensus not initialised"); - return; - } - - auto primary_id = current_consensus->primary(); - if (!primary_id.has_value()) - { - args.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Primary unknown"); - return; - } - - const auto address = node::get_redirect_address_for_node( - args, args.tx, primary_id.value()); - if (!address.has_value()) - { - return; - } - - args.rpc_ctx->set_response_header( - http::headers::LOCATION, - fmt::format("https://{}/node/primary", address.value())); - args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT); - } - } - - template - auto get_primary(T& args) - { - if (this->node_operation.can_replicate()) - { - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - return; - } - - args.rpc_ctx->set_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node is not primary"); - } - - template - auto get_backup(T& args) - { - if (!this->node_operation.can_replicate()) - { - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - return; - } - - args.rpc_ctx->set_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "Node is not backup"); - } - - template - auto consensus_config(T& /*args*/, nlohmann::json&& /*params*/) - { - // Query node for configurations, separate current from pending - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - auto cfg = current_consensus->get_latest_configuration(); - ConsensusConfig cc; - for (auto& [nid, ninfo] : cfg) - { - cc.emplace( - nid.value(), - ConsensusNodeConfig{ - fmt::format("{}:{}", ninfo.hostname, ninfo.port)}); - } - return make_success(cc); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No configured consensus"); - } - - template - auto consensus_state(T& /*args*/, nlohmann::json&& /*params*/) - { - auto* current_consensus = get_consensus(); - if (current_consensus != nullptr) - { - return make_success( - ConsensusConfigDetails{current_consensus->get_details()}); - } - - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No configured consensus"); - } - - template - auto node_metrics(T& args) - { - NodeMetrics nm; - nm.sessions = node_operation.get_session_metrics(); - - args.rpc_ctx->set_response_status(HTTP_STATUS_OK); - args.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); - args.rpc_ctx->set_response_body(nlohmann::json(nm).dump()); - } - - template - auto js_metrics(T& args, nlohmann::json&& /*params*/) - { - auto bytecode_map = args.tx.ro(this->network.modules_quickjs_bytecode); - auto version_val = args.tx.ro(this->network.modules_quickjs_version); - uint64_t bytecode_size = 0; - bytecode_map->foreach( - [&bytecode_size](const auto&, const auto& bytecode) { - bytecode_size += bytecode.size(); - return true; - }); - auto js_engine_map = args.tx.ro(this->network.js_engine); - JavaScriptMetrics m; - m.bytecode_size = bytecode_size; - m.bytecode_used = version_val->get() == std::string(ccf::quickjs_version); - - auto options = js_engine_map->get().value_or(ccf::JSRuntimeOptions{}); - m.max_stack_size = options.max_stack_bytes; - m.max_heap_size = options.max_heap_bytes; - m.max_execution_time = options.max_execution_time_ms; - m.max_cached_interpreters = options.max_cached_interpreters; - - return m; - } - - template - static auto version(T& /*args*/, nlohmann::json&& /*params*/) - { - GetVersion::Out result; - result.ccf_version = ccf::ccf_version; - result.quickjs_version = ccf::quickjs_version; - result.unsafe = false; - - return make_success(result); - } - - template - auto create(T& ctx, nlohmann::json&& params) - { - LOG_INFO_FMT("Processing create RPC"); - - bool recovering = node_operation.is_reading_public_ledger(); - - // This endpoint can only be called once, directly from the starting - // node for the genesis or end of public recovery transaction to - // initialise the service - if (!node_operation.is_in_initialised_state() && !recovering) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::InternalError, - "Node is not in initial state."); - } - - const auto in = params.get(); - - if (InternalTablesAccess::is_service_created(ctx.tx, in.service_cert)) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::InternalError, - "Service is already created."); - } - - InternalTablesAccess::create_service( - ctx.tx, in.service_cert, in.create_txid, in.service_data, recovering); - - if (recovering) - { - // Recovery starts with a fresh consensus configuration, so previous - // service nodes can be removed immediately. - InternalTablesAccess::remove_previous_service_nodes(ctx.tx); - } - - // Genesis transaction (i.e. not after recovery) - if (in.genesis_info.has_value()) - { - // Note that it is acceptable to start a network without any member - // having a recovery share. The service will check that at least one - // recovery member is added before the service is opened. - for (const auto& info : in.genesis_info->members) - { - InternalTablesAccess::add_member(ctx.tx, info); - } - - InternalTablesAccess::init_configuration( - ctx.tx, in.genesis_info->service_configuration); - InternalTablesAccess::set_constitution( - ctx.tx, in.genesis_info->constitution); - } - else - { - // On recovery, force a new ledger chunk - auto* tx_ = static_cast(&ctx.tx); - if (tx_ == nullptr) - { - throw std::logic_error("Could not cast tx to CommittableTx"); - } - tx_->set_tx_flag( - ccf::kv::CommittableTx::TxFlag::LEDGER_CHUNK_BEFORE_THIS_TX); - } - - auto endorsed_certificates = - ctx.tx.rw(network.node_endorsed_certificates); - endorsed_certificates->put(in.node_id, in.node_endorsed_certificate); - - NodeInfo node_info = { - in.node_info_network, - {in.quote_info}, - in.public_encryption_key, - NodeStatus::TRUSTED, - std::nullopt, - in.measurement.hex_str(), - in.certificate_signing_request, - in.public_key, - in.node_data}; - InternalTablesAccess::add_node(ctx.tx, in.node_id, node_info); - - if (in.sealing_recovery_data.has_value()) - { - const auto& [sealing_keys, sealing_recovery_name] = - in.sealing_recovery_data.value(); - auto* sealed_recovery_keys = - ctx.tx.template rw(Tables::SEALED_RECOVERY_KEYS); - sealed_recovery_keys->put(in.node_id, sealing_keys); - - auto* local_sealing_node_id_map = - ctx.tx.template rw( - Tables::SEALING_RECOVERY_NAMES); - local_sealing_node_id_map->put(sealing_recovery_name, in.node_id); - } - - node_operation.shuffle_sealed_shares(ctx.tx); - - if ( - in.quote_info.format != QuoteFormat::amd_sev_snp_v1 || - !in.snp_uvm_endorsements.has_value()) - { - // For improved serviceability on SNP, do not record trusted - // measurements if UVM endorsements are available - InternalTablesAccess::trust_node_measurement( - ctx.tx, in.measurement, in.quote_info.format); - } - - switch (in.quote_info.format) - { - case QuoteFormat::insecure_virtual: - { - auto host_data = AttestationProvider::get_host_data(in.quote_info); - if (host_data.has_value()) - { - InternalTablesAccess::trust_node_virtual_host_data( - ctx.tx, host_data.value()); - } - else - { - LOG_FAIL_FMT("Unable to extract host data from virtual quote"); - } - break; - } - - case QuoteFormat::amd_sev_snp_v1: - { - auto host_data = - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - AttestationProvider::get_host_data(in.quote_info).value(); - InternalTablesAccess::trust_node_snp_host_data( - ctx.tx, host_data, in.snp_security_policy); - - InternalTablesAccess::trust_node_uvm_endorsements( - ctx.tx, in.snp_uvm_endorsements, recovering); - - auto attestation = - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - AttestationProvider::get_snp_attestation_report(in.quote_info) - .value(); - InternalTablesAccess::trust_node_snp_tcb_version( - ctx.tx, attestation, recovering); - break; - } - case QuoteFormat::oe_sgx_v1: - { - break; - } - } - - std::optional digest = - ccf::get_create_tx_claims_digest(ctx.tx); - if (digest.has_value()) - { - auto digest_value = digest.value(); - ctx.rpc_ctx->set_claims_digest(std::move(digest_value)); - } - - this->node_operation.recovery_decision_protocol().reset_state(ctx.tx); - this->node_operation.recovery_decision_protocol().try_start( - ctx.tx, recovering); - - LOG_INFO_FMT("Created service"); - return make_success(true); - } - - template - auto refresh_jwt_keys(T& ctx, nlohmann::json&& body) - { - // All errors are server errors since the client is the server. - - auto* current_consensus = get_consensus(); - auto primary_id = current_consensus->primary(); - if (!primary_id.has_value()) - { - LOG_FAIL_FMT("JWT key auto-refresh: primary unknown"); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Primary is unknown"); - } - - const auto& sig_auth_ident = - ctx.template get_caller(); - if (primary_id.value() != sig_auth_ident.node_id) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: request does not originate from primary"); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Request does not originate from primary."); - } - - SetJwtPublicSigningKeys parsed; - try - { - parsed = body.get(); - } - catch (const ccf::JsonParseError& e) - { - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "Unable to parse body."); - } - - auto issuers = ctx.tx.ro(this->network.jwt_issuers); - auto issuer_metadata_ = issuers->get(parsed.issuer); - if (!issuer_metadata_.has_value()) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: {} is not a valid issuer", parsed.issuer); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format("{} is not a valid issuer.", parsed.issuer)); - } - auto& issuer_metadata = issuer_metadata_.value(); - - if (!issuer_metadata.auto_refresh) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: {} does not have auto_refresh enabled", - parsed.issuer); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format("{} does not have auto_refresh enabled.", parsed.issuer)); - } - - if (!set_jwt_public_signing_keys( - ctx.tx, - "", - parsed.issuer, - issuer_metadata, - parsed.jwks)) - { - LOG_FAIL_FMT( - "JWT key auto-refresh: error while storing signing keys for issuer " - "{}", - parsed.issuer); - return make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Error while storing signing keys for issuer {}.", parsed.issuer)); - } - - return make_success(true); - } - - template - auto get_jwt_metrics(T& /*args*/, const nlohmann::json& /*params*/) - { - JWTRefreshMetrics metrics; - { - ccf::ds::MutexGuard guard(jwt_refresh_metrics_lock); - metrics = jwt_refresh_metrics; - } - return make_success(metrics); - } - - template - auto service_config_handler(T& args, const nlohmann::json& /*params*/) - { - return make_success(args.tx.ro(network.config)->get()); - } - - template - auto list_indexing_strategies(T& /*args*/, const nlohmann::json& /*params*/) - { - return make_success(this->context.get_indexing_strategies().describe()); - } - - auto get_ready_app(ccf::endpoints::CommandEndpointContext& ctx) - { - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - return; - } - if ( - !node_configuration_subsystem->has_received_stop_notice() && - this->node_operation.is_part_of_network() && - this->node_operation.is_user_frontend_open()) - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); - } - else - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); - } - } - auto get_ready_gov(ccf::endpoints::CommandEndpointContext& ctx) - { - auto node_configuration_subsystem = - this->context.get_subsystem(); - if (!node_configuration_subsystem) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - "NodeConfigurationSubsystem is not available"); - return; - } - if ( - !node_configuration_subsystem->has_received_stop_notice() && - this->node_operation.is_accessible_to_members() && - this->node_operation.is_member_frontend_open()) - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); - } - else - { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); - } - } + node_info->pending_last_seen = current_time_ms(); + args.tx.rw(network.nodes) + ->put(existing_node_info->node_id, node_info.value()); + } - template - auto create_snapshot(T& args, nlohmann::json&& /*params*/) - { - auto* snapshot_create = - args.tx.template rw(ccf::Tables::SNAPSHOT_CREATE); - snapshot_create->touch(); - this->node_operation.trigger_snapshot(args.tx); - return make_success(); - } + // Only return node status and ID + LOG_DEBUG_FMT( + "Join request accepted: {} already marked as PENDING", + existing_node_info->node_id); + return make_success(rep); + } - template - auto historical_cache_info( - [[maybe_unused]] T& args, [[maybe_unused]] nlohmann::json&& /*params*/) - { - GetHistoricalCacheInfo::Out result{}; - result.estimated_size = - this->context.get_historical_state().get_estimated_store_cache_size(); - return make_success(result); - } + const std::string payload = fmt::format( + "Joining node is not in expected state ({}).", node_status); + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidNodeState, payload); + } - public: - NodeEndpoints(NetworkState& network_, ccf::AbstractNodeContext& context_) : - CommonEndpointRegistry(get_actor_prefix(ActorsType::nodes), context_), - network(network_), - node_operation(*context_.get_subsystem()) - { - openapi_info.title = "CCF Public Node API"; - openapi_info.description = - "This API provides public, uncredentialed access to service and node " - "state."; - openapi_info.document_version = "5.0.8"; - } + // Not the primary => Redirect if possible to primary + if (should_redirect_to_primary) + { + return redirect_to_primary(); + } - void init_handlers() override - { - CommonEndpointRegistry::init_handlers(); + // Joiner's snapshot too old => StartupSeqnoIsOld + // (causes joiner to fetch a more recent snapshot) + // + // The joiner always wants to use the most recent snapshot. + // However this will result in the joiner chasing the primary if + // snapshot production period ~= snapshot fetching delay + // + // So we have hysteresis in the fetching constraint: + // If the joiner has already fetched a snapshot: joiner seqno > startup + // snapshot seqno Otherwise: joiner seqno > latest snapshot on disk + // seqno + auto this_startup_seqno = + this->node_operation.get_startup_snapshot_seqno(); + ccf::kv::Version required_seqno = this_startup_seqno; + // If the joiner does not enable fetching, or is a legacy node, + // join_fetch_count is unset and we should use the required bound to + // prevent it chasing the primary. + // Otherwise if this is the first request, use the preferred bound + bool using_preferred_bound = + (in.join_fetch_count.has_value() && in.join_fetch_count.value() == 0); + if (using_preferred_bound) + { + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (node_configuration_subsystem != nullptr) + { + const auto& snapshots_config = + node_configuration_subsystem->get().node_config.snapshots; + const auto latest_committed_snapshot = + snapshots::find_latest_committed_snapshot_in_directory( + snapshots_config.directory); + if (latest_committed_snapshot.has_value()) + { + const auto latest_snapshot_seqno = + snapshots::get_snapshot_idx_from_file_name( + latest_committed_snapshot->filename().string()); + required_seqno = std::max( + required_seqno, + static_cast(latest_snapshot_seqno)); + } + } + } + if ( + in.startup_seqno.has_value() && + in.startup_seqno.value() < required_seqno) + { + // Make sure that the joiner's snapshot is more recent than this + // node's snapshot. Otherwise, the joiner may not be given all the + // ledger secrets required to replay historical transactions. + const std::string payload = fmt::format( + "Node requested to join from seqno {} which is older than this " + "node {} {}. A snapshot at least as recent as {} must " + "be used instead.", + in.startup_seqno.value(), + using_preferred_bound ? "latest_on_disk_seqno" : "startup_seqno", + required_seqno, + required_seqno); + LOG_INFO_FMT("Join request rejected: {}", payload); + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::StartupSeqnoIsOld, payload); + } - const auto self_cert_auth_policy = - std::make_shared(this->context); + auto joining_node_status = NodeStatus::PENDING; + // If the service is opening, new nodes are trusted straight away + if ( + active_service->status == ServiceStatus::OPENING || + active_service->status == ServiceStatus::RECOVERING) + { + joining_node_status = NodeStatus::TRUSTED; + } - auto accept = [this](auto& args, const nlohmann::json& params) { - return this->accept(args, params); + return add_node( + args.tx, + args.rpc_ctx->get_session_context()->caller_cert, + in, + joining_node_status, + active_service->status); }; make_endpoint("/join", HTTP_POST, json_adapter(accept), no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) .set_openapi_hidden(true) .install(); - auto remove_expired_pending = [this](auto& ctx, nlohmann::json&& json) { - return this->remove_expired_pending(ctx, std::move(json)); + auto remove_expired_pending = [this](auto& ctx, nlohmann::json&&) { + const auto pending_node_timeout = get_pending_node_timeout(); + if (!pending_node_timeout.has_value()) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfiguration subsystem is not available"); + } + + if (pending_node_timeout.value() <= std::chrono::milliseconds::zero()) + { + return make_success(true); + } + + const auto now = current_time_ms(); + auto nodes = ctx.tx.rw(network.nodes); + std::map untimestamped_pending_nodes; + std::vector expired_pending_nodes; + nodes->foreach([&](const auto& node_id, const auto& node_info) { + if (node_info.status != NodeStatus::PENDING) + { + return true; + } + + if ( + !node_info.pending_last_seen.has_value() || + node_info.pending_last_seen.value() < 0 || + node_info.pending_last_seen.value() > now) + { + auto updated_node_info = node_info; + updated_node_info.pending_last_seen = now; + untimestamped_pending_nodes.emplace( + node_id, std::move(updated_node_info)); + } + else if ( + now - node_info.pending_last_seen.value() >= + pending_node_timeout.value().count()) + { + expired_pending_nodes.push_back(node_id); + } + + return true; + }); + + for (const auto& [node_id, node_info] : untimestamped_pending_nodes) + { + nodes->put(node_id, node_info); + } + + for (const auto& node_id : expired_pending_nodes) + { + LOG_INFO_FMT("Removing expired Pending node {}", node_id); + InternalTablesAccess::remove_node(ctx.tx, node_id); + } + + return make_success(true); }; make_endpoint( "network/nodes/remove_expired_pending", @@ -1862,8 +790,26 @@ namespace ccf .set_openapi_hidden(true) .install(); - auto set_retired_committed = [this](auto& ctx, nlohmann::json&& json) { - return this->set_retired_committed(ctx, std::move(json)); + auto set_retired_committed = [this](auto& ctx, nlohmann::json&&) { + auto nodes = ctx.tx.rw(network.nodes); + nodes->foreach([&nodes](const auto& node_id, auto node_info) { + auto gc_node = nodes->get_globally_committed(node_id); + if ( + gc_node.has_value() && + gc_node->status == ccf::NodeStatus::RETIRED && + !node_info.retired_committed) + { + // Set retired_committed on nodes for which RETIRED status + // has been committed. + node_info.retired_committed = true; + nodes->put(node_id, node_info); + + LOG_DEBUG_FMT("Setting retired_committed on node {}", node_id); + } + return true; + }); + + return make_success(); }; make_endpoint( "network/nodes/set_retired_committed", @@ -1873,8 +819,58 @@ namespace ccf .set_openapi_hidden(true) .install(); - auto get_state = [this](auto& args, nlohmann::json&& json) { - return this->get_state(args, std::move(json)); + auto get_state = [this](auto& args, nlohmann::json&&) { + GetState::Out result; + auto [s, rts, lrs] = this->node_operation.state(); + result.node_id = this->context.get_node_id(); + result.state = s; + result.recovery_target_seqno = rts; + result.last_recovered_seqno = lrs; + result.startup_seqno = + this->node_operation.get_startup_snapshot_seqno(); + + // Read last signed seqno from both raw and COSE signature tables + auto signatures = args.tx.template ro(Tables::SIGNATURES); + auto sig = signatures->get(); + + ccf::kv::Version raw_seqno = 0; + if (sig.has_value()) + { + raw_seqno = sig.value().seqno; + } + + ccf::kv::Version cose_seqno = 0; + auto cose_signatures = + args.tx.template ro(Tables::COSE_SIGNATURES); + auto cose_sig = cose_signatures->get(ccf::IdentityType::CLASSICAL); + if (cose_sig.has_value() && !cose_sig->empty()) + { + auto receipt = ccf::cose::decode_ccf_receipt(cose_sig.value(), false); + auto txid = ccf::TxID::from_str(receipt.phdr.ccf.txid); + if (!txid.has_value()) + { + throw std::logic_error(fmt::format( + "Failed to parse txid from COSE signature: {}", + receipt.phdr.ccf.txid)); + } + cose_seqno = txid->seqno; + } + + result.last_signed_seqno = std::max(raw_seqno, cose_seqno); + + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + } + result.stop_notice = + node_configuration_subsystem->has_received_stop_notice(); + + return make_success(result); }; make_read_only_endpoint( "/state", HTTP_GET, json_read_only_adapter(get_state), no_auth_required) @@ -1882,8 +878,57 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_quote = [this](auto& args, nlohmann::json&& json) { - return this->get_quote(args, std::move(json)); + auto get_quote = [this](auto& args, nlohmann::json&&) { + QuoteInfo node_quote_info; + const auto result = + get_quote_for_this_node_v1(args.tx, node_quote_info); + if (result == ApiResult::OK) + { + Quote q; + q.node_id = context.get_node_id(); + q.raw = node_quote_info.quote; + q.endorsements = node_quote_info.endorsements; + q.format = node_quote_info.format; + q.uvm_endorsements = node_quote_info.uvm_endorsements; + + auto nodes = args.tx.ro(network.nodes); + auto node_info = nodes->get(context.get_node_id()); + if (node_info.has_value() && node_info->code_digest.has_value()) + { + q.measurement = node_info->code_digest.value(); + } + else + { + auto measurement = + AttestationProvider::get_measurement(node_quote_info); + if (measurement.has_value()) + { + q.measurement = measurement.value().hex_str(); + } + else + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InvalidQuote, + "Failed to extract code id from node quote."); + } + } + + return make_success(q); + } + + if (result == ApiResult::NotFound) + { + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Could not find node quote."); + } + + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format("Error code: {}", ccf::api_result_to_str(result))); }; make_read_only_endpoint( "/quotes/self", @@ -1902,8 +947,40 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_quotes = [this](auto& args, nlohmann::json&& json) { - return this->get_quotes(args, std::move(json)); + auto get_quotes = [this](auto& args, nlohmann::json&&) { + GetQuotes::Out result; + + auto nodes = args.tx.ro(network.nodes); + nodes->foreach(["es = result.quotes]( + const auto& node_id, const auto& node_info) { + if (node_info.status == ccf::NodeStatus::TRUSTED) + { + Quote q; + q.node_id = node_id; + q.raw = node_info.quote_info.quote; + q.endorsements = node_info.quote_info.endorsements; + q.format = node_info.quote_info.format; + q.uvm_endorsements = node_info.quote_info.uvm_endorsements; + + if (node_info.code_digest.has_value()) + { + q.measurement = node_info.code_digest.value(); + } + else + { + auto measurement = + AttestationProvider::get_measurement(node_info.quote_info); + if (measurement.has_value()) + { + q.measurement = measurement.value().hex_str(); + } + } + quotes.emplace_back(q); + } + return true; + }); + + return make_success(result); }; make_read_only_endpoint( "/quotes", @@ -1913,9 +990,19 @@ namespace ccf .set_auto_schema() .install(); - auto get_attestations = [this](auto& args, nlohmann::json&& params) { - return this->get_attestations(args, std::move(params)); - }; + auto get_attestations = + [get_quotes](auto& args, nlohmann::json&& params) { + auto res = get_quotes(args, std::move(params)); + const auto* body = std::get_if(&res); + if (body != nullptr) + { + auto result = nlohmann::json::object(); + result["attestations"] = (*body)["quotes"]; + return make_success(result); + } + + return res; + }; make_read_only_endpoint( "/attestations", HTTP_GET, @@ -1924,8 +1011,35 @@ namespace ccf .set_auto_schema() .install(); - auto network_status = [this](auto& args, nlohmann::json&& json) { - return this->network_status(args, std::move(json)); + auto network_status = [this](auto& args, nlohmann::json&&) { + GetNetworkInfo::Out out; + auto service = args.tx.ro(network.service); + auto service_state = service->get(); + if (service_state.has_value()) + { + const auto& service_value = service_state.value(); + out.service_status = service_value.status; + out.service_certificate = service_value.cert; + out.recovery_count = service_value.recovery_count.value_or(0); + out.service_data = service_value.service_data; + out.current_service_create_txid = + service_value.current_service_create_txid; + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + out.current_view = current_consensus->get_view(); + auto primary_id = current_consensus->primary(); + if (primary_id.has_value()) + { + out.primary_id = primary_id.value(); + } + } + return make_success(out); + } + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Service state not available."); }; make_read_only_endpoint( "/network", @@ -1935,8 +1049,21 @@ namespace ccf .set_auto_schema() .install(); - auto service_previous_identity = [](auto& args, nlohmann::json&& json) { - return NodeEndpoints::service_previous_identity(args, std::move(json)); + auto service_previous_identity = [](auto& args, nlohmann::json&&) { + auto psi_handle = args.tx.template ro( + ccf::Tables::PREVIOUS_SERVICE_IDENTITY); + const auto psi = psi_handle->get(); + if (psi.has_value()) + { + GetServicePreviousIdentity::Out out; + out.previous_service_identity = psi.value(); + return make_success(out); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "This service is not a recovery of a previous service."); }; make_read_only_endpoint( "/service/previous_identity", @@ -1946,8 +1073,87 @@ namespace ccf .set_auto_schema() .install(); - auto get_nodes = [this](auto& args, nlohmann::json&& json) { - return this->get_nodes(args, std::move(json)); + auto get_nodes = [this](auto& args, nlohmann::json&&) { + const auto parsed_query = + http::parse_query(args.rpc_ctx->get_request_query()); + + std::string error_string; // Ignored - all params are optional + const auto host = http::get_query_value_opt( + parsed_query, "host", error_string); + const auto port = http::get_query_value_opt( + parsed_query, "port", error_string); + const auto status_str = http::get_query_value_opt( + parsed_query, "status", error_string); + + std::optional status; + if (status_str.has_value()) + { + // Convert the query argument to a JSON string, try to parse it as + // a NodeStatus, return an error if this doesn't work + try + { + status = nlohmann::json(status_str.value()).get(); + } + catch (const ccf::JsonParseError& e) + { + return ccf::make_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidQueryParameterValue, + fmt::format( + "Query parameter '{}' is not a valid node status", + status_str.value())); + } + } + + GetNodes::Out out; + + auto nodes = args.tx.ro(this->network.nodes); + auto* current_consensus = get_consensus(); + nodes->foreach([host, port, status, &out, nodes, current_consensus]( + const NodeId& nid, const NodeInfo& ni) { + if (status.has_value() && status.value() != ni.status) + { + return true; + } + + // Match on any interface + bool is_matched = false; + for (auto const& interface : ni.rpc_interfaces) + { + const auto& [pub_host, pub_port] = + split_net_address(interface.second.published_address); + + if ( + (!host.has_value() || host.value() == pub_host) && + (!port.has_value() || port.value() == pub_port)) + { + is_matched = true; + break; + } + } + + if (!is_matched) + { + return true; + } + + bool is_primary = false; + if (current_consensus != nullptr) + { + is_primary = current_consensus->primary() == nid; + } + + out.nodes.push_back( + {nid, + ni.status, + is_primary, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(nid).value_or(0)}); + return true; + }); + + return make_success(out); }; make_read_only_endpoint( "/network/nodes", @@ -1963,8 +1169,35 @@ namespace ccf "status", ccf::endpoints::OptionalParameter) .install(); - auto get_removable_nodes = [this](auto& args, nlohmann::json&& json) { - return this->get_removable_nodes(args, std::move(json)); + auto get_removable_nodes = [this](auto& args, nlohmann::json&&) { + GetNodes::Out out; + + auto nodes = args.tx.ro(this->network.nodes); + nodes->foreach( + [&out, nodes](const NodeId& node_id, const NodeInfo& /*ni*/) { + // Only nodes whose retire_committed status is committed can be + // safely removed, because any primary elected from here on would + // consider them retired, and would consequently not need their + // input in any quorum. We must therefore read the KV at its + // globally committed watermark, for the purpose of this RPC. Since + // this transaction does not perform a write, it is safe to do this. + auto node = nodes->get_globally_committed(node_id); + if ( + node.has_value() && node->status == ccf::NodeStatus::RETIRED && + node->retired_committed) + { + out.nodes.push_back( + {node_id, + node->status, + false /* is_primary */, + node->rpc_interfaces, + node->node_data, + nodes->get_version_of_previous_write(node_id).value_or(0)}); + } + return true; + }); + + return make_success(out); }; make_read_only_endpoint( @@ -1976,8 +1209,57 @@ namespace ccf .install(); auto delete_retired_committed_node = - [this](auto& args, nlohmann::json&& json) { - return this->delete_retired_committed_node(args, std::move(json)); + [this](auto& args, nlohmann::json&&) { + GetNodes::Out out; + + std::string node_id; + std::string error; + if (!get_path_param( + args.rpc_ctx->get_request_path_params(), + "node_id", + node_id, + error)) + { + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); + } + + auto nodes = args.tx.rw(this->network.nodes); + if (!nodes->has(node_id)) + { + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No such node"); + } + + // A node's retirement is only complete when the + // transition of retired_committed is itself committed, + // i.e. when the next eligible primary is guaranteed to + // be aware the retirement is committed. + // As a result, the handler must check node info at the + // current committed level, rather than at the end of the + // local suffix. + // While this transaction does execute a write, it specifically + // deletes the value it reads from. It is therefore safe to + // execute on the basis of a potentially stale read-set, + // which get_globally_committed() typically produces. + auto node = nodes->get_globally_committed(node_id); + if ( + node.has_value() && node->status == ccf::NodeStatus::RETIRED && + node->retired_committed) + { + InternalTablesAccess::remove_node(args.tx, node_id); + } + else + { + return make_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::NodeNotRetiredCommitted, + "Node is not completely retired"); + } + + return make_success(true); }; make_endpoint( @@ -1989,8 +1271,9 @@ namespace ccf .install(); auto get_self_signed_certificate = - [this](auto& args, nlohmann::json&& json) { - return this->get_self_signed_certificate(args, std::move(json)); + [this](auto& /*args*/, nlohmann::json&&) { + return SelfSignedNodeCertificateInfo{ + this->node_operation.get_self_signed_node_certificate()}; }; make_command_endpoint( "/self_signed_certificate", @@ -2001,8 +1284,48 @@ namespace ccf .set_auto_schema() .install(); - auto get_node_info = [this](auto& args, nlohmann::json&& json) { - return this->get_node_info(args, std::move(json)); + auto get_node_info = [this](auto& args, nlohmann::json&&) { + std::string node_id; + std::string error; + if (!get_path_param( + args.rpc_ctx->get_request_path_params(), + "node_id", + node_id, + error)) + { + return make_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); + } + + auto nodes = args.tx.ro(this->network.nodes); + auto info = nodes->get(node_id); + + if (!info) + { + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node not found"); + } + + bool is_primary = false; + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto primary = current_consensus->primary(); + if (primary.has_value() && primary.value() == node_id) + { + is_primary = true; + } + } + auto& ni = info.value(); + return make_success(GetNode::Out{ + node_id, + ni.status, + is_primary, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(node_id).value_or(0)}); }; make_read_only_endpoint( "/network/nodes/{node_id}", @@ -2012,8 +1335,55 @@ namespace ccf .set_auto_schema() .install(); - auto get_self_node = [this](auto& args, nlohmann::json&& json) { - return this->get_self_node(args, std::move(json)); + auto get_self_node = [this](auto& args, nlohmann::json&&) { + auto node_id = this->context.get_node_id(); + auto nodes = args.tx.ro(this->network.nodes); + auto info = nodes->get(node_id); + + bool is_primary = false; + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto primary = current_consensus->primary(); + if (primary.has_value() && primary.value() == node_id) + { + is_primary = true; + } + } + + if (info.has_value()) + { + // Answers from the KV are preferred, as they are more up-to-date, + // especially status and node_data. + auto& ni = info.value(); + return make_success(GetNode::Out{ + node_id, + ni.status, + is_primary, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(node_id).value_or(0)}); + } + + // If the node isn't in its KV yet, fall back to configuration + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + } + const auto& node_startup_config = + node_configuration_subsystem->get().node_config; + return make_success(GetNode::Out{ + node_id, + ccf::NodeStatus::PENDING, + is_primary, + node_startup_config.network.rpc_interfaces, + node_configuration_subsystem->get().node_data, + 0}); }; make_read_only_endpoint( "/network/nodes/self", @@ -2024,8 +1394,44 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_primary_node = [this](auto& args, nlohmann::json&& json) { - return this->get_primary_node(args, std::move(json)); + auto get_primary_node = [this](auto& args, nlohmann::json&&) { + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto primary_id = current_consensus->primary(); + if (!primary_id.has_value()) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Primary unknown"); + } + + auto nodes = args.tx.ro(this->network.nodes); + auto info = nodes->get(primary_id.value()); + if (!info) + { + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node not found"); + } + + auto& ni = info.value(); + return make_success(GetNode::Out{ + primary_id.value(), + ni.status, + true, + ni.rpc_interfaces, + ni.node_data, + nodes->get_version_of_previous_write(primary_id.value()) + .value_or(0)}); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No configured consensus"); }; make_read_only_endpoint( "/network/nodes/primary", @@ -2035,7 +1441,46 @@ namespace ccf .set_auto_schema() .install(); - auto head_primary = [this](auto& args) { this->head_primary(args); }; + auto head_primary = [this](auto& args) { + if (this->node_operation.can_replicate()) + { + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + } + else + { + auto* current_consensus = get_consensus(); + if (current_consensus == nullptr) + { + args.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Consensus not initialised"); + return; + } + + auto primary_id = current_consensus->primary(); + if (!primary_id.has_value()) + { + args.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Primary unknown"); + return; + } + + const auto address = node::get_redirect_address_for_node( + args, args.tx, primary_id.value()); + if (!address.has_value()) + { + return; + } + + args.rpc_ctx->set_response_header( + http::headers::LOCATION, + fmt::format("https://{}/node/primary", address.value())); + args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT); + } + }; make_read_only_endpoint( "/primary", HTTP_HEAD, head_primary, no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) @@ -2044,19 +1489,60 @@ namespace ccf "Redirect to the current primary node.") .install(); - auto get_primary = [this](auto& args) { this->get_primary(args); }; + auto get_primary = [this](auto& args) { + if (this->node_operation.can_replicate()) + { + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; + } + + args.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node is not primary"); + }; make_read_only_endpoint( "/primary", HTTP_GET, get_primary, no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto get_backup = [this](auto& args) { this->get_backup(args); }; + auto get_backup = [this](auto& args) { + if (!this->node_operation.can_replicate()) + { + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; + } + + args.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "Node is not backup"); + }; make_read_only_endpoint("/backup", HTTP_GET, get_backup, no_auth_required) .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto consensus_config = [this](auto& args, nlohmann::json&& json) { - return this->consensus_config(args, std::move(json)); + auto consensus_config = [this](auto& /*args*/, nlohmann::json&&) { + // Query node for configurations, separate current from pending + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + auto cfg = current_consensus->get_latest_configuration(); + ConsensusConfig cc; + for (auto& [nid, ninfo] : cfg) + { + cc.emplace( + nid.value(), + ConsensusNodeConfig{ + fmt::format("{}:{}", ninfo.hostname, ninfo.port)}); + } + return make_success(cc); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No configured consensus"); }; make_command_endpoint( @@ -2068,8 +1554,18 @@ namespace ccf .set_auto_schema() .install(); - auto consensus_state = [this](auto& args, nlohmann::json&& json) { - return this->consensus_state(args, std::move(json)); + auto consensus_state = [this](auto& /*args*/, nlohmann::json&&) { + auto* current_consensus = get_consensus(); + if (current_consensus != nullptr) + { + return make_success( + ConsensusConfigDetails{current_consensus->get_details()}); + } + + return make_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No configured consensus"); }; make_command_endpoint( @@ -2081,7 +1577,15 @@ namespace ccf .set_auto_schema() .install(); - auto node_metrics = [this](auto& args) { this->node_metrics(args); }; + auto node_metrics = [this](auto& args) { + NodeMetrics nm; + nm.sessions = node_operation.get_session_metrics(); + + args.rpc_ctx->set_response_status(HTTP_STATUS_OK); + args.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + args.rpc_ctx->set_response_body(nlohmann::json(nm).dump()); + }; make_command_endpoint( "/metrics", HTTP_GET, node_metrics, no_auth_required) @@ -2089,8 +1593,28 @@ namespace ccf .set_auto_schema() .install(); - auto js_metrics = [this](auto& args, nlohmann::json&& json) { - return this->js_metrics(args, std::move(json)); + auto js_metrics = [this](auto& args, nlohmann::json&&) { + auto bytecode_map = args.tx.ro(this->network.modules_quickjs_bytecode); + auto version_val = args.tx.ro(this->network.modules_quickjs_version); + uint64_t bytecode_size = 0; + bytecode_map->foreach( + [&bytecode_size](const auto&, const auto& bytecode) { + bytecode_size += bytecode.size(); + return true; + }); + auto js_engine_map = args.tx.ro(this->network.js_engine); + JavaScriptMetrics m; + m.bytecode_size = bytecode_size; + m.bytecode_used = + version_val->get() == std::string(ccf::quickjs_version); + + auto options = js_engine_map->get().value_or(ccf::JSRuntimeOptions{}); + m.max_stack_size = options.max_stack_bytes; + m.max_heap_size = options.max_heap_bytes; + m.max_execution_time = options.max_execution_time_ms; + m.max_cached_interpreters = options.max_cached_interpreters; + + return m; }; make_read_only_endpoint( @@ -2101,8 +1625,13 @@ namespace ccf .set_auto_schema() .install(); - auto version = [](auto& args, nlohmann::json&& json) { - return NodeEndpoints::version(args, std::move(json)); + auto version = [](auto&, nlohmann::json&&) { + GetVersion::Out result; + result.ccf_version = ccf::ccf_version; + result.quickjs_version = ccf::quickjs_version; + result.unsafe = false; + + return make_success(result); }; make_command_endpoint( @@ -2112,7 +1641,165 @@ namespace ccf .install(); auto create = [this](auto& ctx, nlohmann::json&& params) { - return this->create(ctx, std::move(params)); + LOG_INFO_FMT("Processing create RPC"); + + bool recovering = node_operation.is_reading_public_ledger(); + + // This endpoint can only be called once, directly from the starting + // node for the genesis or end of public recovery transaction to + // initialise the service + if (!node_operation.is_in_initialised_state() && !recovering) + { + return make_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::InternalError, + "Node is not in initial state."); + } + + const auto in = params.get(); + + if (InternalTablesAccess::is_service_created(ctx.tx, in.service_cert)) + { + return make_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::InternalError, + "Service is already created."); + } + + InternalTablesAccess::create_service( + ctx.tx, in.service_cert, in.create_txid, in.service_data, recovering); + + if (recovering) + { + // Recovery starts with a fresh consensus configuration, so previous + // service nodes can be removed immediately. + InternalTablesAccess::remove_previous_service_nodes(ctx.tx); + } + + // Genesis transaction (i.e. not after recovery) + if (in.genesis_info.has_value()) + { + // Note that it is acceptable to start a network without any member + // having a recovery share. The service will check that at least one + // recovery member is added before the service is opened. + for (const auto& info : in.genesis_info->members) + { + InternalTablesAccess::add_member(ctx.tx, info); + } + + InternalTablesAccess::init_configuration( + ctx.tx, in.genesis_info->service_configuration); + InternalTablesAccess::set_constitution( + ctx.tx, in.genesis_info->constitution); + } + else + { + // On recovery, force a new ledger chunk + auto* tx_ = static_cast(&ctx.tx); + if (tx_ == nullptr) + { + throw std::logic_error("Could not cast tx to CommittableTx"); + } + tx_->set_tx_flag( + ccf::kv::CommittableTx::TxFlag::LEDGER_CHUNK_BEFORE_THIS_TX); + } + + auto endorsed_certificates = + ctx.tx.rw(network.node_endorsed_certificates); + endorsed_certificates->put(in.node_id, in.node_endorsed_certificate); + + NodeInfo node_info = { + in.node_info_network, + {in.quote_info}, + in.public_encryption_key, + NodeStatus::TRUSTED, + std::nullopt, + in.measurement.hex_str(), + in.certificate_signing_request, + in.public_key, + in.node_data}; + InternalTablesAccess::add_node(ctx.tx, in.node_id, node_info); + + if (in.sealing_recovery_data.has_value()) + { + const auto& [sealing_keys, sealing_recovery_name] = + in.sealing_recovery_data.value(); + auto* sealed_recovery_keys = ctx.tx.template rw( + Tables::SEALED_RECOVERY_KEYS); + sealed_recovery_keys->put(in.node_id, sealing_keys); + + auto* local_sealing_node_id_map = + ctx.tx.template rw( + Tables::SEALING_RECOVERY_NAMES); + local_sealing_node_id_map->put(sealing_recovery_name, in.node_id); + } + + node_operation.shuffle_sealed_shares(ctx.tx); + + if ( + in.quote_info.format != QuoteFormat::amd_sev_snp_v1 || + !in.snp_uvm_endorsements.has_value()) + { + // For improved serviceability on SNP, do not record trusted + // measurements if UVM endorsements are available + InternalTablesAccess::trust_node_measurement( + ctx.tx, in.measurement, in.quote_info.format); + } + + switch (in.quote_info.format) + { + case QuoteFormat::insecure_virtual: + { + auto host_data = AttestationProvider::get_host_data(in.quote_info); + if (host_data.has_value()) + { + InternalTablesAccess::trust_node_virtual_host_data( + ctx.tx, host_data.value()); + } + else + { + LOG_FAIL_FMT("Unable to extract host data from virtual quote"); + } + break; + } + + case QuoteFormat::amd_sev_snp_v1: + { + auto host_data = + AttestationProvider::get_host_data(in.quote_info).value(); + InternalTablesAccess::trust_node_snp_host_data( + ctx.tx, host_data, in.snp_security_policy); + + InternalTablesAccess::trust_node_uvm_endorsements( + ctx.tx, in.snp_uvm_endorsements, recovering); + + auto attestation = + AttestationProvider::get_snp_attestation_report(in.quote_info) + .value(); + InternalTablesAccess::trust_node_snp_tcb_version( + ctx.tx, attestation, recovering); + break; + } + case QuoteFormat::oe_sgx_v1: + { + break; + } + } + + std::optional digest = + ccf::get_create_tx_claims_digest(ctx.tx); + if (digest.has_value()) + { + auto digest_value = digest.value(); + ctx.rpc_ctx->set_claims_digest(std::move(digest_value)); + } + + this->node_operation.recovery_decision_protocol().reset_state(ctx.tx); + this->node_operation.recovery_decision_protocol().try_start( + ctx.tx, recovering); + + LOG_INFO_FMT("Created service"); + return make_success(true); }; make_endpoint( "/create", HTTP_POST, json_adapter(create), {self_cert_auth_policy}) @@ -2121,7 +1808,89 @@ namespace ccf // Only called from node. See node_state.h. auto refresh_jwt_keys = [this](auto& ctx, nlohmann::json&& body) { - return this->refresh_jwt_keys(ctx, std::move(body)); + // All errors are server errors since the client is the server. + + auto* current_consensus = get_consensus(); + auto primary_id = current_consensus->primary(); + if (!primary_id.has_value()) + { + LOG_FAIL_FMT("JWT key auto-refresh: primary unknown"); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Primary is unknown"); + } + + const auto& sig_auth_ident = + ctx.template get_caller(); + if (primary_id.value() != sig_auth_ident.node_id) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: request does not originate from primary"); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Request does not originate from primary."); + } + + SetJwtPublicSigningKeys parsed; + try + { + parsed = body.get(); + } + catch (const ccf::JsonParseError& e) + { + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "Unable to parse body."); + } + + auto issuers = ctx.tx.ro(this->network.jwt_issuers); + auto issuer_metadata_ = issuers->get(parsed.issuer); + if (!issuer_metadata_.has_value()) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: {} is not a valid issuer", parsed.issuer); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format("{} is not a valid issuer.", parsed.issuer)); + } + auto& issuer_metadata = issuer_metadata_.value(); + + if (!issuer_metadata.auto_refresh) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: {} does not have auto_refresh enabled", + parsed.issuer); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "{} does not have auto_refresh enabled.", parsed.issuer)); + } + + if (!set_jwt_public_signing_keys( + ctx.tx, + "", + parsed.issuer, + issuer_metadata, + parsed.jwks)) + { + LOG_FAIL_FMT( + "JWT key auto-refresh: error while storing signing keys for issuer " + "{}", + parsed.issuer); + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Error while storing signing keys for issuer {}.", + parsed.issuer)); + } + + return make_success(true); }; make_endpoint( "/jwt_keys/refresh", @@ -2131,9 +1900,15 @@ namespace ccf .set_openapi_hidden(true) .install(); - auto get_jwt_metrics = [this](auto& args, const nlohmann::json& params) { - return this->get_jwt_metrics(args, params); - }; + auto get_jwt_metrics = + [this](auto& /*args*/, const nlohmann::json& /*params*/) { + JWTRefreshMetrics metrics; + { + ccf::ds::MutexGuard guard(jwt_refresh_metrics_lock); + metrics = jwt_refresh_metrics; + } + return make_success(metrics); + }; make_read_only_endpoint( "/jwt_keys/refresh/metrics", HTTP_GET, @@ -2143,8 +1918,8 @@ namespace ccf .install(); auto service_config_handler = - [this](auto& args, const nlohmann::json& params) { - return this->service_config_handler(args, params); + [this](auto& args, const nlohmann::json& /*params*/) { + return make_success(args.tx.ro(network.config)->get()); }; make_endpoint( "/service/configuration", @@ -2155,10 +1930,11 @@ namespace ccf .set_auto_schema() .install(); - auto list_indexing_strategies = - [this](auto& args, const nlohmann::json& params) { - return this->list_indexing_strategies(args, params); - }; + auto list_indexing_strategies = [this]( + auto& /*args*/, + const nlohmann::json& /*params*/) { + return make_success(this->context.get_indexing_strategies().describe()); + }; make_endpoint( "/index/strategies", @@ -2170,7 +1946,28 @@ namespace ccf .install(); auto get_ready_app = [this](ccf::endpoints::CommandEndpointContext& ctx) { - this->get_ready_app(ctx); + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + return; + } + if ( + !node_configuration_subsystem->has_received_stop_notice() && + this->node_operation.is_part_of_network() && + this->node_operation.is_user_frontend_open()) + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + } + else + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); + } + return; }; make_command_endpoint( "/ready/app", HTTP_GET, get_ready_app, no_auth_required) @@ -2182,7 +1979,28 @@ namespace ccf .install(); auto get_ready_gov = [this](ccf::endpoints::CommandEndpointContext& ctx) { - this->get_ready_gov(ctx); + auto node_configuration_subsystem = + this->context.get_subsystem(); + if (!node_configuration_subsystem) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + "NodeConfigurationSubsystem is not available"); + return; + } + if ( + !node_configuration_subsystem->has_received_stop_notice() && + this->node_operation.is_accessible_to_members() && + this->node_operation.is_member_frontend_open()) + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + } + else + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_SERVICE_UNAVAILABLE); + } + return; }; make_command_endpoint( "/ready/gov", HTTP_GET, get_ready_gov, no_auth_required) @@ -2193,8 +2011,12 @@ namespace ccf .set_forwarding_required(endpoints::ForwardingRequired::Never) .install(); - auto create_snapshot = [this](auto& args, nlohmann::json&& json) { - return this->create_snapshot(args, std::move(json)); + auto create_snapshot = [this](auto& args, nlohmann::json&&) { + auto* snapshot_create = args.tx.template rw( + ccf::Tables::SNAPSHOT_CREATE); + snapshot_create->touch(); + this->node_operation.trigger_snapshot(args.tx); + return make_success(); }; make_endpoint( "/snapshot:create", @@ -2210,8 +2032,13 @@ namespace ccf ccf::node::init_file_serving_handlers(*this, context); - auto historical_cache_info = [this](auto& args, nlohmann::json&& json) { - return this->historical_cache_info(args, std::move(json)); + auto historical_cache_info = [this]( + [[maybe_unused]] auto& args, + [[maybe_unused]] nlohmann::json&&) { + GetHistoricalCacheInfo::Out result{}; + result.estimated_size = + this->context.get_historical_state().get_estimated_store_cache_size(); + return make_success(result); }; make_read_only_endpoint( "/historical_cache",