in src/node/rpc/node_frontend.h [327:1547]
void init_handlers() override
{
CommonEndpointRegistry::init_handlers();
auto accept = [this](auto& args, const nlohmann::json& params) {
const auto in = params.get<JoinNetworkNodeToNode::In>();
if (
!this->context.get_node_state().is_part_of_network() &&
!this->context.get_node_state().is_part_of_public_network() &&
!this->context.get_node_state().is_reading_private_ledger())
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Target node should be part of network to accept new nodes.");
}
if (this->network.consensus_type != in.consensus_type)
{
return make_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::ConsensusTypeMismatch,
fmt::format(
"Node requested to join with consensus type {} but "
"current consensus type is {}.",
in.consensus_type,
this->network.consensus_type));
}
// If the joiner and this node both started from a snapshot, make sure
// that the joiner's snapshot is more recent than this node's snapshot
auto this_startup_seqno =
this->context.get_node_state().get_startup_snapshot_seqno();
if (
this_startup_seqno.has_value() && in.startup_seqno.has_value() &&
this_startup_seqno.value() > in.startup_seqno.value())
{
return make_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::StartupSnapshotIsOld,
fmt::format(
"Node requested to join from snapshot at seqno {} which is "
"older "
"than this node startup seqno {}",
in.startup_seqno.value(),
this_startup_seqno.value()));
}
auto nodes = args.tx.rw(this->network.nodes);
auto service = args.tx.rw(this->network.service);
auto active_service = service->get();
if (!active_service.has_value())
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"No service is available to accept new node.");
}
auto config = args.tx.ro(network.config);
auto service_config = config->get();
auto reconfiguration_type =
service_config->reconfiguration_type.value_or(
ReconfigurationType::ONE_TRANSACTION);
if (active_service->status == ServiceStatus::OPENING)
{
// If the service is opening, new nodes are trusted straight away
NodeStatus joining_node_status = NodeStatus::TRUSTED;
// If the node is already trusted, return network secrets
auto existing_node_info = check_node_exists(
args.tx, args.rpc_ctx->session->caller_cert, joining_node_status);
if (existing_node_info.has_value())
{
JoinNetworkNodeToNode::Out rep;
rep.node_status = joining_node_status;
rep.network_info = JoinNetworkNodeToNode::Out::NetworkInfo(
context.get_node_state().is_part_of_public_network(),
context.get_node_state().get_last_recovered_signed_idx(),
this->network.consensus_type,
reconfiguration_type,
this->network.ledger_secrets->get(
args.tx, existing_node_info->ledger_secret_seqno),
*this->network.identity.get(),
active_service->status,
existing_node_info->endorsed_certificate);
return make_success(rep);
}
if (
consensus != nullptr && consensus->type() == ConsensusType::CFT &&
!this->context.get_node_state().can_replicate())
{
auto primary_id = consensus->primary();
if (primary_id.has_value())
{
auto nodes = args.tx.ro(this->network.nodes);
auto info = nodes->get(primary_id.value());
if (info)
{
auto& interface_id = args.rpc_ctx->session->interface_id;
if (!interface_id.has_value())
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Cannot redirect non-RPC request.");
}
const auto& address =
info->rpc_interfaces[interface_id.value()].published_address;
args.rpc_ctx->set_response_header(
http::headers::LOCATION,
fmt::format("https://{}/node/join", address));
return make_error(
HTTP_STATUS_PERMANENT_REDIRECT,
ccf::errors::NodeCannotHandleRequest,
"Node is not primary; cannot handle write");
}
}
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
}
return add_node(
args.tx,
args.rpc_ctx->session->caller_cert,
in,
joining_node_status,
active_service->status,
reconfiguration_type);
}
// If the service is open, new nodes are first added as pending and
// then only trusted via member governance. It is expected that a new
// node polls the network to retrieve the network secrets until it is
// trusted
auto existing_node_info =
check_node_exists(args.tx, args.rpc_ctx->session->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;
rep.node_status = node_status;
if (is_taking_part_in_acking(node_status))
{
rep.network_info = JoinNetworkNodeToNode::Out::NetworkInfo(
context.get_node_state().is_part_of_public_network(),
context.get_node_state().get_last_recovered_signed_idx(),
this->network.consensus_type,
reconfiguration_type,
this->network.ledger_secrets->get(
args.tx, existing_node_info->ledger_secret_seqno),
*this->network.identity.get(),
active_service->status,
existing_node_info->endorsed_certificate);
return make_success(rep);
}
else if (node_status == NodeStatus::PENDING)
{
// Only return node status and ID
return make_success(rep);
}
else
{
return make_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::InvalidNodeState,
fmt::format(
"Joining node is not in expected state ({}).", node_status));
}
}
else
{
if (
consensus != nullptr && consensus->type() == ConsensusType::CFT &&
!this->context.get_node_state().can_replicate())
{
auto primary_id = consensus->primary();
if (primary_id.has_value())
{
auto nodes = args.tx.ro(this->network.nodes);
auto info = nodes->get(primary_id.value());
if (info)
{
auto& interface_id = args.rpc_ctx->session->interface_id;
if (!interface_id.has_value())
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Cannot redirect non-RPC request.");
}
const auto& address =
info->rpc_interfaces[interface_id.value()].published_address;
args.rpc_ctx->set_response_header(
http::headers::LOCATION,
fmt::format("https://{}/node/join", address));
return make_error(
HTTP_STATUS_PERMANENT_REDIRECT,
ccf::errors::NodeCannotHandleRequest,
"Node is not primary; cannot handle write");
}
}
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
}
// If the node does not exist, add it to the KV in state pending
return add_node(
args.tx,
args.rpc_ctx->session->caller_cert,
in,
NodeStatus::PENDING,
active_service->status,
reconfiguration_type);
}
};
make_endpoint("/join", HTTP_POST, json_adapter(accept), no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_openapi_hidden(true)
.install();
auto remove_retired_nodes = [this](auto& ctx, nlohmann::json&&) {
// This endpoint should only be called internally once it is certain
// that all nodes recorded as Retired will no longer issue transactions.
auto nodes = ctx.tx.rw(network.nodes);
auto node_endorsed_certificates =
ctx.tx.rw(network.node_endorsed_certificates);
nodes->foreach([this, &nodes, &node_endorsed_certificates](
const auto& node_id, const auto& node_info) {
if (
node_info.status == ccf::NodeStatus::RETIRED &&
node_id != this->context.get_node_state().get_node_id())
{
nodes->remove(node_id);
node_endorsed_certificates->remove(node_id);
LOG_DEBUG_FMT("Removing retired node {}", node_id);
}
return true;
});
return make_success();
};
make_endpoint(
"network/nodes/retired",
HTTP_DELETE,
json_adapter(remove_retired_nodes),
{std::make_shared<NodeCertAuthnPolicy>()})
.set_openapi_hidden(true)
.install();
auto get_state = [this](auto& args, nlohmann::json&&) {
GetState::Out result;
auto [s, rts, lrs] = this->context.get_node_state().state();
result.node_id = this->context.get_node_state().get_node_id();
result.state = s;
result.recovery_target_seqno = rts;
result.last_recovered_seqno = lrs;
result.startup_seqno =
this->context.get_node_state().get_startup_snapshot_seqno().value_or(
0);
auto signatures = args.tx.template ro<Signatures>(Tables::SIGNATURES);
auto sig = signatures->get();
if (!sig.has_value())
{
result.last_signed_seqno = 0;
}
else
{
result.last_signed_seqno = sig.value().seqno;
}
return result;
};
make_read_only_endpoint(
"/state", HTTP_GET, json_read_only_adapter(get_state), no_auth_required)
.set_auto_schema<GetState>()
.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_state().get_node_id();
q.raw = node_quote_info.quote;
q.endorsements = node_quote_info.endorsements;
q.format = node_quote_info.format;
#ifdef GET_QUOTE
// get_code_id attempts to re-validate the quote to extract mrenclave
// and the Open Enclave is insufficiently flexible to allow quotes
// with expired collateral to be parsed at all. Recent nodes therefore
// cache their code digest on startup, and this code attempts to fetch
// that value when possible and only call the unreliable get_code_id
// otherwise.
auto nodes = args.tx.ro(network.nodes);
auto node_info = nodes->get(context.get_node_state().get_node_id());
if (node_info.has_value() && node_info->code_digest.has_value())
{
q.mrenclave = node_info->code_digest.value();
}
else
{
auto code_id =
EnclaveAttestationProvider::get_code_id(node_quote_info);
if (code_id.has_value())
{
q.mrenclave = ds::to_hex(code_id.value().data);
}
else
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InvalidQuote,
"Failed to extract code id from node quote.");
}
}
#endif
return make_success(q);
}
else if (result == ApiResult::NotFound)
{
return make_error(
HTTP_STATUS_NOT_FOUND,
ccf::errors::ResourceNotFound,
"Could not find node quote.");
}
else
{
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",
HTTP_GET,
json_read_only_adapter(get_quote),
no_auth_required)
.set_auto_schema<void, Quote>()
.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 ||
node_info.status == ccf::NodeStatus::LEARNER)
{
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;
#ifdef GET_QUOTE
// get_code_id attempts to re-validate the quote to extract
// mrenclave and the Open Enclave is insufficiently flexible to
// allow quotes with expired collateral to be parsed at all. Recent
// nodes therefore cache their code digest on startup, and this code
// attempts to fetch that value when possible and only call the
// unreliable get_code_id otherwise.
if (node_info.code_digest.has_value())
{
q.mrenclave = node_info.code_digest.value();
}
else
{
auto code_id =
EnclaveAttestationProvider::get_code_id(node_info.quote_info);
if (code_id.has_value())
{
q.mrenclave = ds::to_hex(code_id.value().data);
}
}
#endif
quotes.emplace_back(q);
}
return true;
});
return make_success(result);
};
make_read_only_endpoint(
"/quotes",
HTTP_GET,
json_read_only_adapter(get_quotes),
no_auth_required)
.set_auto_schema<GetQuotes>()
.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;
if (consensus != nullptr)
{
out.current_view = consensus->get_view();
auto primary_id = consensus->primary();
if (primary_id.has_value() && !consensus->view_change_in_progress())
{
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",
HTTP_GET,
json_read_only_adapter(network_status),
no_auth_required)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.set_auto_schema<void, GetNetworkInfo::Out>()
.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<std::string>(
parsed_query, "host", error_string);
const auto port = http::get_query_value_opt<std::string>(
parsed_query, "port", error_string);
const auto status_str = http::get_query_value_opt<std::string>(
parsed_query, "status", error_string);
std::optional<NodeStatus> 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<NodeStatus>();
}
catch (const 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);
nodes->foreach([this, host, port, status, &out](
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 (consensus != nullptr)
{
is_primary = consensus->primary() == nid;
}
out.nodes.push_back({nid, ni.status, is_primary, ni.rpc_interfaces});
return true;
});
return make_success(out);
};
make_read_only_endpoint(
"/network/nodes",
HTTP_GET,
json_read_only_adapter(get_nodes),
no_auth_required)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Primary)
.set_auto_schema<void, GetNodes::Out>()
.add_query_parameter<std::string>(
"host", ccf::endpoints::OptionalParameter)
.add_query_parameter<std::string>(
"port", ccf::endpoints::OptionalParameter)
.add_query_parameter<std::string>(
"status", ccf::endpoints::OptionalParameter)
.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;
if (consensus != nullptr)
{
auto primary = 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});
};
make_read_only_endpoint(
"/network/nodes/{node_id}",
HTTP_GET,
json_read_only_adapter(get_node_info),
no_auth_required)
.set_auto_schema<void, GetNode::Out>()
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto get_self_node = [this](auto& args) {
auto node_id = this->context.get_node_state().get_node_id();
auto nodes = args.tx.ro(this->network.nodes);
auto info = nodes->get(node_id);
if (info)
{
auto& interface_id = args.rpc_ctx->session->interface_id;
if (!interface_id.has_value())
{
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Cannot redirect non-RPC request.");
return;
}
const auto& address =
info->rpc_interfaces[interface_id.value()].published_address;
args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
args.rpc_ctx->set_response_header(
http::headers::LOCATION,
fmt::format(
"https://{}/node/network/nodes/{}", address, node_id.value()));
return;
}
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Node info not available");
return;
};
make_read_only_endpoint(
"/network/nodes/self", HTTP_GET, get_self_node, no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto get_primary_node = [this](auto& args) {
if (consensus != nullptr)
{
auto node_id = this->context.get_node_state().get_node_id();
auto primary_id = consensus->primary();
if (!primary_id.has_value())
{
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
return;
}
auto nodes = args.tx.ro(this->network.nodes);
auto info = nodes->get(node_id);
auto info_primary = nodes->get(primary_id.value());
if (info && info_primary)
{
auto& interface_id = args.rpc_ctx->session->interface_id;
if (!interface_id.has_value())
{
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Cannot redirect non-RPC request.");
return;
}
const auto& address =
info->rpc_interfaces[interface_id.value()].published_address;
args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
args.rpc_ctx->set_response_header(
http::headers::LOCATION,
fmt::format(
"https://{}/node/network/nodes/{}",
address,
primary_id->value()));
return;
}
}
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
return;
};
make_read_only_endpoint(
"/network/nodes/primary", HTTP_GET, get_primary_node, no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto is_primary = [this](auto& args) {
if (this->context.get_node_state().can_replicate())
{
args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
}
else
{
args.rpc_ctx->set_response_status(HTTP_STATUS_PERMANENT_REDIRECT);
if (consensus != nullptr)
{
auto primary_id = consensus->primary();
if (!primary_id.has_value())
{
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
return;
}
auto nodes = args.tx.ro(this->network.nodes);
auto info = nodes->get(primary_id.value());
if (info)
{
auto& interface_id = args.rpc_ctx->session->interface_id;
if (!interface_id.has_value())
{
args.rpc_ctx->set_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Cannot redirect non-RPC request.");
return;
}
const auto& address =
info->rpc_interfaces[interface_id.value()].published_address;
args.rpc_ctx->set_response_header(
http::headers::LOCATION,
fmt::format("https://{}/node/primary", address));
}
}
}
};
make_read_only_endpoint(
"/primary", HTTP_HEAD, is_primary, no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto consensus_config = [this](auto& args, nlohmann::json&&) {
// Query node for configurations, separate current from pending
if (consensus != nullptr)
{
auto cfg = 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);
}
else
{
return make_error(
HTTP_STATUS_NOT_FOUND,
ccf::errors::ResourceNotFound,
"No configured consensus");
}
};
make_command_endpoint(
"/config",
HTTP_GET,
json_command_adapter(consensus_config),
no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_auto_schema<void, ConsensusConfig>()
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto consensus_state = [this](auto& args, nlohmann::json&&) {
if (consensus != nullptr)
{
return make_success(ConsensusConfigDetails{consensus->get_details()});
}
else
{
return make_error(
HTTP_STATUS_NOT_FOUND,
ccf::errors::ResourceNotFound,
"No configured consensus");
}
};
make_command_endpoint(
"/consensus",
HTTP_GET,
json_command_adapter(consensus_state),
no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_auto_schema<void, ConsensusConfigDetails>()
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto memory_usage = [](auto& args) {
// Do not attempt to call oe_allocator_mallinfo when used from
// unit tests such as the frontend_test
#ifdef INSIDE_ENCLAVE
oe_mallinfo_t info;
auto rc = oe_allocator_mallinfo(&info);
if (rc == OE_OK)
{
MemoryUsage::Out mu(info);
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(mu).dump());
return;
}
#endif
args.rpc_ctx->set_response_status(HTTP_STATUS_INTERNAL_SERVER_ERROR);
args.rpc_ctx->set_response_body("Failed to read memory usage");
};
make_command_endpoint("/memory", HTTP_GET, memory_usage, no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.set_auto_schema<MemoryUsage>()
.install();
auto node_metrics = [this](auto& args) {
NodeMetrics nm;
nm.sessions = context.get_node_state().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)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.set_auto_schema<void, NodeMetrics>()
.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;
});
JavaScriptMetrics m;
m.bytecode_size = bytecode_size;
m.bytecode_used =
version_val->get() == std::string(ccf::quickjs_version);
return m;
};
make_read_only_endpoint(
"/js_metrics",
HTTP_GET,
json_read_only_adapter(js_metrics),
no_auth_required)
.set_auto_schema<void, JavaScriptMetrics>()
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto jwt_metrics = [this](auto&, nlohmann::json&&) {
JWTMetrics m;
// Attempts are recorded by the key refresh code itself, registering
// before each call to each issuer's keys
m.attempts = context.get_node_state().get_jwt_attempts();
// Success is marked by the fact that the key succeeded and called
// our internal "jwt_keys/refresh" endpoint.
auto e = fully_qualified_endpoints["/jwt_keys/refresh"][HTTP_POST];
auto metric = get_metrics_for_endpoint(e);
m.successes = metric.calls - (metric.failures + metric.errors);
return m;
};
make_read_only_endpoint(
"/jwt_metrics",
HTTP_GET,
json_read_only_adapter(jwt_metrics),
no_auth_required)
.set_auto_schema<void, JWTMetrics>()
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto version = [this](auto&, nlohmann::json&&) {
GetVersion::Out result;
result.ccf_version = ccf::ccf_version;
result.quickjs_version = ccf::quickjs_version;
return make_success(result);
};
make_command_endpoint(
"/version", HTTP_GET, json_command_adapter(version), no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_auto_schema<GetVersion>()
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
auto create = [this](auto& ctx, nlohmann::json&& params) {
LOG_DEBUG_FMT("Processing create RPC");
// 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 (
network.consensus_type == ConsensusType::CFT &&
!context.get_node_state().is_in_initialised_state() &&
!context.get_node_state().is_reading_public_ledger())
{
return make_error(
HTTP_STATUS_FORBIDDEN,
ccf::errors::InternalError,
"Node is not in initial state.");
}
const auto in = params.get<CreateNetworkNodeToNode::In>();
GenesisGenerator g(this->network, ctx.tx);
if (g.is_service_created(in.service_cert))
{
return make_error(
HTTP_STATUS_FORBIDDEN,
ccf::errors::InternalError,
"Service is already created.");
}
g.create_service(in.service_cert);
// Retire all nodes, in case there are any (i.e. post recovery)
g.retire_active_nodes();
NodeInfo node_info = {
in.node_info_network,
{in.quote_info},
in.public_encryption_key,
NodeStatus::TRUSTED,
std::nullopt,
ds::to_hex(in.code_digest.data),
in.certificate_signing_request,
in.public_key};
// 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)
{
g.add_member(info);
}
if (
in.genesis_info->service_configuration.consensus ==
ConsensusType::BFT &&
(!in.genesis_info->service_configuration.reconfiguration_type
.has_value() ||
in.genesis_info->service_configuration.reconfiguration_type
.value() != ReconfigurationType::TWO_TRANSACTION))
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"BFT consensus requires two-transaction reconfiguration.");
}
g.init_configuration(in.genesis_info->service_configuration);
g.set_constitution(in.genesis_info->constitution);
}
auto endorsed_certificates =
ctx.tx.rw(network.node_endorsed_certificates);
endorsed_certificates->put(in.node_id, in.node_endorsed_certificate);
g.add_node(in.node_id, node_info);
#ifdef GET_QUOTE
g.trust_node_code_id(in.code_digest);
#endif
LOG_INFO_FMT("Created service");
return make_success(true);
};
make_endpoint(
"/create", HTTP_POST, json_adapter(create), no_auth_required)
.set_openapi_hidden(true)
.install();
// 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.
if (!consensus)
{
LOG_FAIL_FMT("JWT key auto-refresh: no consensus available");
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"No consensus available.");
}
auto primary_id = 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<ccf::NodeCertAuthnIdentity>();
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<SetJwtPublicSigningKeys>();
}
catch (const JsonParseError& e)
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Unable to parse body.");
}
auto issuers = ctx.tx.rw(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,
"<auto-refresh>",
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",
HTTP_POST,
json_adapter(refresh_jwt_keys),
{std::make_shared<NodeCertAuthnPolicy>()})
.set_openapi_hidden(true)
.install();
auto update_resharing = [this](auto& args, const nlohmann::json& params) {
const auto in = params.get<UpdateResharing::In>();
auto resharings = args.tx.rw(network.resharings);
bool exists = false;
resharings->foreach(
[rid = in.rid, &exists](
const kv::ReconfigurationId& trid, const ResharingResult& result) {
if (trid == rid)
{
exists = true;
return false;
}
return true;
});
if (exists)
{
return make_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::ResharingAlreadyCompleted,
fmt::format(
"resharing for configuration {} already completed.", in.rid));
}
// For now, just pretend that we're done.
ResharingResult rr;
rr.reconfiguration_id = in.rid;
rr.seqno = 0;
resharings->put(in.rid, rr);
return make_success(true);
};
make_endpoint(
"/update-resharing",
HTTP_POST,
json_adapter(update_resharing),
{std::make_shared<NodeCertAuthnPolicy>()})
.set_forwarding_required(endpoints::ForwardingRequired::Always)
.set_openapi_hidden(true)
.install();
auto orc_handler = [this](auto& args, const nlohmann::json& params) {
const auto in = params.get<ObservedReconfigurationCommit::In>();
if (consensus->type() != ConsensusType::BFT)
{
auto primary_id = consensus->primary();
if (!primary_id.has_value())
{
return make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
"Primary unknown");
}
if (primary_id.value() != context.get_node_state().get_node_id())
{
return make_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::NodeCannotHandleRequest,
"Only the primary accepts ORCs.");
}
}
auto nodes_in_config = consensus->orc(in.reconfiguration_id, in.from);
if (nodes_in_config.has_value())
{
LOG_DEBUG_FMT(
"Configurations: sufficient number of ORCs, updating nodes in "
"configuration #{}.",
in.reconfiguration_id);
auto nodes = args.tx.rw(network.nodes);
nodes->foreach(
[&nodes, &nodes_in_config](const auto& nid, const auto& node_info) {
if (
node_info.status == NodeStatus::RETIRING &&
nodes_in_config->find(nid) == nodes_in_config->end())
{
auto updated_info = node_info;
updated_info.status = NodeStatus::RETIRED;
nodes->put(nid, updated_info);
}
else if (
node_info.status == NodeStatus::LEARNER &&
nodes_in_config->find(nid) != nodes_in_config->end())
{
auto updated_info = node_info;
updated_info.status = NodeStatus::TRUSTED;
nodes->put(nid, updated_info);
}
return true;
});
}
return make_success(true);
};
make_endpoint(
"/orc",
HTTP_POST,
json_adapter(orc_handler),
{std::make_shared<NodeCertAuthnPolicy>()})
.set_forwarding_required(endpoints::ForwardingRequired::Always)
.set_openapi_hidden(true)
.install();
auto service_config_handler =
[this](auto& args, const nlohmann::json& params) {
return make_success(args.tx.ro(network.config)->get());
};
make_endpoint(
"/service/configuration",
HTTP_GET,
json_adapter(service_config_handler),
no_auth_required)
.set_forwarding_required(endpoints::ForwardingRequired::Never)
.set_auto_schema<void, ServiceConfiguration>()
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.install();
}