Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add free-form node_data field #3662

Merged
merged 11 commits into from
Mar 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- Nodes now have a free-form `node_data` field, to match users and members. This can be set when the node is launched, or modified by governance. It is intended to store correlation IDs describing the node's deployment, such as a VM name or Pod identifier.

## [2.0.0-rc4]

### Added
Expand Down
4 changes: 4 additions & 0 deletions doc/host_config_schema/cchost_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@
},
"description": "This section includes configuration for the node x509 identity certificate"
},
"node_data_json_file": {
"type": "string",
"description": "Path to file (JSON) containing initial node data. It is intended to store correlation IDs describing the node's deployment, such as a VM name or Pod identifier."
},
"ledger": {
"type": "object",
"properties": {
Expand Down
12 changes: 10 additions & 2 deletions doc/schemas/node_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@
},
"GetNode__NodeInfo": {
"properties": {
"last_written": {
"$ref": "#/components/schemas/uint64"
},
"node_data": {
"$ref": "#/components/schemas/json"
},
"node_id": {
"$ref": "#/components/schemas/NodeId"
},
Expand All @@ -257,7 +263,9 @@
"node_id",
"status",
"primary",
"rpc_interfaces"
"rpc_interfaces",
"node_data",
"last_written"
],
"type": "object"
},
Expand Down Expand Up @@ -738,7 +746,7 @@
"info": {
"description": "This API provides public, uncredentialed access to service and node state.",
"title": "CCF Public Node API",
"version": "2.14.1"
"version": "2.15.0"
},
"openapi": "3.0.0",
"paths": {
Expand Down
7 changes: 6 additions & 1 deletion include/ccf/service/node_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ namespace ccf
/// Public key
std::optional<crypto::Pem> public_key = std::nullopt;

/// Free-form user data, can be used to store operator correlation
/// IDs/labels for the node for example
nlohmann::json node_data = nullptr;
achamayou marked this conversation as resolved.
Show resolved Hide resolved

/**
* Fields below are deprecated
*/
Expand All @@ -75,7 +79,8 @@ namespace ccf
ledger_secret_seqno,
code_digest,
certificate_signing_request,
public_key);
public_key,
node_data);
}

FMT_BEGIN_NAMESPACE
Expand Down
20 changes: 19 additions & 1 deletion samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,25 @@ const actions = new Map([
}
),
],

[
"set_node_data",
new Action(
function (args) {
checkEntityId(args.node_id, "node_id");
},
function (args) {
let node_id = ccf.strToBuf(args.node_id);
let nodes_info = ccf.kv["public:ccf.gov.nodes.info"];
let node_info = nodes_info.get(node_id);
if (node_info === undefined) {
throw new Error(`Node ${node_id} does not exist`);
}
let ni = ccf.bufToJsonCompatible(node_info);
ni.node_data = args.node_data;
nodes_info.set(node_id, ccf.jsonCompatibleToBuf(ni));
}
),
],
[
"transition_node_to_trusted",
new Action(
Expand Down
3 changes: 3 additions & 0 deletions src/common/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ struct StartupConfig : CCFConfig
// Only if starting or recovering
size_t initial_service_certificate_validity_days = 1;

nlohmann::json node_data = nullptr;

struct Start
{
std::vector<ccf::NewMember> members;
Expand Down Expand Up @@ -177,6 +179,7 @@ DECLARE_JSON_REQUIRED_FIELDS(
startup_host_time,
snapshot_tx_interval,
initial_service_certificate_validity_days,
node_data,
start,
join,
recover);
Expand Down
2 changes: 2 additions & 0 deletions src/host/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ namespace host
ds::TimeString slow_io_logging_threshold = {"10ms"};
std::optional<std::string> node_client_interface = std::nullopt;
ds::TimeString client_connection_timeout = {"2000ms"};
std::optional<std::string> node_data_json_file = std::nullopt;

struct OutputFiles
{
Expand Down Expand Up @@ -210,6 +211,7 @@ namespace host
slow_io_logging_threshold,
node_client_interface,
client_connection_timeout,
node_data_json_file,
achamayou marked this conversation as resolved.
Show resolved Hide resolved
output_files,
ledger,
snapshots,
Expand Down
6 changes: 6 additions & 0 deletions src/host/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,12 @@ int main(int argc, char** argv)
startup_config.worker_threads = config.worker_threads;
startup_config.node_certificate = config.node_certificate;

if (config.node_data_json_file.has_value())
{
startup_config.node_data =
files::slurp_json(config.node_data_json_file.value());
}

auto startup_host_time = std::chrono::system_clock::now();
LOG_INFO_FMT("Startup host time: {}", startup_host_time);

Expand Down
2 changes: 2 additions & 0 deletions src/node/node_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,7 @@ namespace ccf
join_params.startup_seqno = startup_seqno;
join_params.certificate_signing_request = node_sign_kp->create_csr(
config.node_certificate.subject_name, subject_alt_names);
join_params.node_data = config.node_data;

LOG_DEBUG_FMT(
"Sending join request to {}", config.join.target_rpc_address);
Expand Down Expand Up @@ -1729,6 +1730,7 @@ namespace ccf
create_params.public_encryption_key = node_encrypt_kp->public_key_pem();
create_params.code_digest = node_code_id;
create_params.node_info_network = config.network;
create_params.node_data = config.node_data;

const auto body = serdes::pack(create_params, serdes::Pack::Text);

Expand Down
2 changes: 2 additions & 0 deletions src/node/rpc/call_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ namespace ccf
NodeStatus status;
bool primary;
ccf::NodeInfoNetwork::RpcInterfaces rpc_interfaces;
nlohmann::json node_data;
ccf::SeqNo last_written;
};

using Out = NodeInfo;
Expand Down
2 changes: 2 additions & 0 deletions src/node/rpc/node_call_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ namespace ccf
crypto::Pem public_encryption_key;
CodeDigest code_digest;
NodeInfoNetwork node_info_network;
nlohmann::json node_data;

// Only set on genesis transaction, but not on recovery
std::optional<StartupConfig::Start> genesis_info = std::nullopt;
Expand All @@ -76,6 +77,7 @@ namespace ccf
ConsensusType consensus_type = ConsensusType::CFT;
std::optional<kv::Version> startup_seqno = std::nullopt;
std::optional<crypto::Pem> certificate_signing_request = std::nullopt;
nlohmann::json node_data = nullptr;
};

struct Out
Expand Down
24 changes: 18 additions & 6 deletions src/node/rpc/node_frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ namespace ccf
ledger_secret_seqno,
ds::to_hex(code_digest.data),
in.certificate_signing_request,
client_public_key_pem};
client_public_key_pem,
in.node_data};

// Because the certificate signature scheme is non-deterministic, only
// self-signed node certificate is recorded in the node info table
Expand Down Expand Up @@ -347,7 +348,7 @@ namespace ccf
openapi_info.description =
"This API provides public, uncredentialed access to service and node "
"state.";
openapi_info.document_version = "2.14.1";
openapi_info.document_version = "2.15.0";
}

void init_handlers() override
Expand Down Expand Up @@ -842,7 +843,7 @@ namespace ccf
GetNodes::Out out;

auto nodes = args.tx.ro(this->network.nodes);
nodes->foreach([this, host, port, status, &out](
nodes->foreach([this, host, port, status, &out, nodes](
const NodeId& nid, const NodeInfo& ni) {
if (status.has_value() && status.value() != ni.status)
{
Expand Down Expand Up @@ -876,7 +877,13 @@ namespace ccf
is_primary = consensus->primary() == nid;
}

out.nodes.push_back({nid, ni.status, is_primary, ni.rpc_interfaces});
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)});
eddyashton marked this conversation as resolved.
Show resolved Hide resolved
return true;
});

Expand Down Expand Up @@ -932,8 +939,13 @@ namespace ccf
}
}
auto& ni = info.value();
return make_success(
GetNode::Out{node_id, ni.status, is_primary, ni.rpc_interfaces});
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}",
Expand Down
13 changes: 10 additions & 3 deletions src/node/rpc/serialization.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace ccf
consensus_type,
startup_seqno)
DECLARE_JSON_OPTIONAL_FIELDS(
JoinNetworkNodeToNode::In, certificate_signing_request)
JoinNetworkNodeToNode::In, certificate_signing_request, node_data)

DECLARE_JSON_ENUM(
ccf::IdentityType,
Expand Down Expand Up @@ -72,7 +72,8 @@ namespace ccf
public_encryption_key,
code_digest,
node_info_network)
DECLARE_JSON_OPTIONAL_FIELDS(CreateNetworkNodeToNode::In, genesis_info)
DECLARE_JSON_OPTIONAL_FIELDS(
CreateNetworkNodeToNode::In, genesis_info, node_data)

DECLARE_JSON_TYPE(GetCommit::Out)
DECLARE_JSON_REQUIRED_FIELDS(GetCommit::Out, transaction_id)
Expand All @@ -90,7 +91,13 @@ namespace ccf

DECLARE_JSON_TYPE(GetNode::NodeInfo)
DECLARE_JSON_REQUIRED_FIELDS(
GetNode::NodeInfo, node_id, status, primary, rpc_interfaces)
GetNode::NodeInfo,
node_id,
status,
primary,
rpc_interfaces,
node_data,
last_written)

DECLARE_JSON_TYPE(GetNodes::Out)
DECLARE_JSON_REQUIRED_FIELDS(GetNodes::Out, nodes)
Expand Down
1 change: 1 addition & 0 deletions tests/config.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"curve_id": "{{ curve_id }}",
"initial_validity_days": {{ initial_node_cert_validity_days }}
},
"node_data_json_file": {{ node_data_json_file|tojson }},
"command": {
"type": "{{ start_type }}",
"start":
Expand Down
68 changes: 67 additions & 1 deletion tests/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import governance_js
from infra.runner import ConcurrentRunner
import governance_history
import tempfile

from loguru import logger as LOG

Expand Down Expand Up @@ -190,6 +191,71 @@ def test_no_quote(network, args):
return network


@reqs.description("Test node data set at node construction, and updated by governance")
def test_node_data(network, args):
with tempfile.NamedTemporaryFile(mode="w+") as ntf:
primary, _ = network.find_primary()
with primary.client() as c:

def get_nodes():
r = c.get("/node/network/nodes")
assert r.status_code == 200, (r.status_code, r.body.text())
return {
node_info["node_id"]: node_info
for node_info in r.body.json()["nodes"]
}

new_node_data = {"my_id": "0xdeadbeef", "location": "The Moon"}
json.dump(new_node_data, ntf)
ntf.flush()
untrusted_node = network.create_node(
infra.interfaces.HostSpec(
rpc_interfaces={
infra.interfaces.PRIMARY_RPC_INTERFACE: infra.interfaces.RPCInterface(
endorsement=infra.interfaces.Endorsement(authority="Node")
)
}
),
node_data_json_file=ntf.name,
)

# NB: This new node joins but is never trusted
network.join_node(untrusted_node, args.package, args)

nodes = get_nodes()
assert untrusted_node.node_id in nodes, nodes
new_node_info = nodes[untrusted_node.node_id]
assert new_node_info["node_data"] == new_node_data, new_node_info

# Set modified node data
new_node_data["previous_locations"] = [new_node_data["location"]]
new_node_data["location"] = "Secret Base"

network.consortium.set_node_data(
primary, untrusted_node.node_id, new_node_data
)

nodes = get_nodes()
assert untrusted_node.node_id in nodes, nodes
new_node_info = nodes[untrusted_node.node_id]
assert new_node_info["node_data"] == new_node_data, new_node_info

# Set modified node data on trusted primary
primary_node_data = "Some plain JSON string"
network.consortium.set_node_data(
primary, primary.node_id, primary_node_data
)

nodes = get_nodes()
assert primary.node_id in nodes, nodes
primary_node_info = nodes[primary.node_id]
assert (
primary_node_info["node_data"] == primary_node_data
), primary_node_info

return network


@reqs.description("Check member data")
def test_member_data(network, args):
assert args.initial_operator_count > 0
Expand Down Expand Up @@ -462,6 +528,7 @@ def gov(args):
test_user(network, args)
test_jinja_templates(network, args)
test_no_quote(network, args)
test_node_data(network, args)
test_ack_state_digest_update(network, args)
test_invalid_client_signature(network, args)
test_each_node_cert_renewal(network, args)
Expand All @@ -484,7 +551,6 @@ def js_gov(args):
governance_js.test_vote_failure_reporting(network, args)
governance_js.test_operator_proposals_and_votes(network, args)
governance_js.test_apply(network, args)
governance_js.test_actions(network, args)
governance_js.test_set_constitution(network, args)


Expand Down
Loading