Skip to content

Commit 58b1ce4

Browse files
authored
Stricter NodeInfoNetwork handling (#7576)
1 parent cfa69ad commit 58b1ce4

2 files changed

Lines changed: 61 additions & 63 deletions

File tree

include/ccf/service/node_info_network.h

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -206,27 +206,12 @@ namespace ccf
206206
return fmt::format("{}:{}", host, port);
207207
}
208208

209-
// The JSON representation of a NodeInfoNetwork is the union of a
210-
// NodeInfoNetwork_v1 and a NodeInfoNetwork_v2. It contains the fields of
211-
// both, so can be read as (or from!) either
209+
// All NodeInfoNetwork read that may lead to re-serialization for
210+
// write purposes must be v2 by now, so we only serialize as v2.
211+
// If any v1 NodeInfoNetwork is read at all, it must be for historical
212+
// query purposes, and does not need to be re-serialized to v2.
212213
inline void to_json(nlohmann::json& j, const NodeInfoNetwork& nin)
213214
{
214-
{
215-
NodeInfoNetwork_v1 v1;
216-
std::tie(v1.nodehost, v1.nodeport) =
217-
split_net_address(nin.node_to_node_interface.bind_address);
218-
219-
if (!nin.rpc_interfaces.empty())
220-
{
221-
const auto& primary_interface = nin.rpc_interfaces.begin()->second;
222-
std::tie(v1.rpchost, v1.rpcport) =
223-
split_net_address(primary_interface.bind_address);
224-
std::tie(v1.pubhost, v1.pubport) =
225-
split_net_address(primary_interface.published_address);
226-
}
227-
to_json(j, v1);
228-
}
229-
230215
to_json(j, (const NodeInfoNetwork_v2&)nin);
231216
}
232217

@@ -243,6 +228,14 @@ namespace ccf
243228
NodeInfoNetwork_v1 v1;
244229
try
245230
{
231+
if (
232+
j.contains("rpc_interfaces") || j.contains("node_to_node_interface"))
233+
{
234+
// If these v2 fields are present, rethrow the error - the JSON is
235+
// malformed for v2. Only proceed to parse as v1 if these fields are
236+
// absent and the NodeInfoNetwork is a pure v1.
237+
throw jpe;
238+
}
246239
from_json(j, v1);
247240
}
248241
catch (const ccf::JsonParseError& _)
@@ -255,6 +248,9 @@ namespace ccf
255248

256249
nin.node_to_node_interface.bind_address =
257250
make_net_address(v1.nodehost, v1.nodeport);
251+
// If published address is not explicitly set, default to bind address
252+
nin.node_to_node_interface.published_address =
253+
make_net_address(v1.nodehost, v1.nodeport);
258254

259255
NodeInfoNetwork::NetInterface primary_interface;
260256
primary_interface.bind_address = make_net_address(v1.rpchost, v1.rpcport);

src/node/test/node_info_json.cpp

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ TEST_CASE("Multiple versions of NodeInfoNetwork")
3131

3232
ccf::NodeInfoNetwork_v1 v1;
3333
std::tie(v1.nodehost, v1.nodeport) = ccf::split_net_address(node);
34-
std::tie(v1.rpchost, v1.nodeport) = ccf::split_net_address(rpc_a);
34+
std::tie(v1.rpchost, v1.rpcport) = ccf::split_net_address(rpc_a);
3535
std::tie(v1.pubhost, v1.pubport) = ccf::split_net_address(rpc_b);
3636

3737
{
@@ -41,56 +41,58 @@ TEST_CASE("Multiple versions of NodeInfoNetwork")
4141
REQUIRE(current == converted);
4242
}
4343

44+
// to_json(NodeInfoNetwork) NEVER writes v1 fields now
45+
// No current node should be using v1 anymore
4446
{
45-
INFO("Old format survives round-trip through current");
47+
INFO("Old format loses old fields when converted to new format");
4648
nlohmann::json j = v1;
47-
const auto intermediate = j.get<ccf::NodeInfoNetwork>();
48-
nlohmann::json j2 = intermediate;
49-
const auto converted = j2.get<ccf::NodeInfoNetwork_v1>();
50-
51-
// Manual equality check - not implementing it now for a deprecated format
52-
REQUIRE(v1.nodehost == converted.nodehost);
53-
REQUIRE(v1.nodeport == converted.nodeport);
54-
REQUIRE(v1.rpchost == converted.rpchost);
55-
REQUIRE(v1.rpcport == converted.rpcport);
56-
REQUIRE(v1.pubhost == converted.pubhost);
57-
REQUIRE(v1.pubport == converted.pubport);
49+
nlohmann::json converted;
50+
to_json(converted, j.get<ccf::NodeInfoNetwork>());
51+
const auto dumped_converted = converted.dump();
52+
const auto deserialized_converted = nlohmann::json::parse(dumped_converted);
53+
54+
// v1 fields are not present anymore
55+
REQUIRE(!deserialized_converted.contains("nodehost"));
56+
REQUIRE(!deserialized_converted.contains("nodeport"));
57+
REQUIRE(!deserialized_converted.contains("rpchost"));
58+
REQUIRE(!deserialized_converted.contains("rpcport"));
59+
REQUIRE(!deserialized_converted.contains("pubhost"));
60+
REQUIRE(!deserialized_converted.contains("pubport"));
61+
62+
const auto new_converted =
63+
deserialized_converted.get<ccf::NodeInfoNetwork>();
64+
65+
// v2 fields have been constructed correctly
66+
REQUIRE(
67+
new_converted.node_to_node_interface.bind_address ==
68+
ccf::NodeInfoNetwork::NetAddress(v1.nodehost + ":" + v1.nodeport));
69+
REQUIRE(
70+
new_converted.node_to_node_interface.published_address ==
71+
ccf::NodeInfoNetwork::NetAddress(v1.nodehost + ":" + v1.nodeport));
72+
73+
REQUIRE(new_converted.rpc_interfaces.size() == 1);
74+
const auto& primary_rpc_it =
75+
new_converted.rpc_interfaces.find(ccf::PRIMARY_RPC_INTERFACE);
76+
const auto& primary_rpc = primary_rpc_it->second;
77+
REQUIRE(
78+
primary_rpc.bind_address ==
79+
ccf::NodeInfoNetwork::NetAddress(v1.rpchost + ":" + v1.rpcport));
80+
REQUIRE(
81+
primary_rpc.published_address ==
82+
ccf::NodeInfoNetwork::NetAddress(v1.pubhost + ":" + v1.pubport));
5883
}
5984

85+
// Test that slightly malformed v2 JSON does not get misparsed as v1
86+
// and triggers an exception instead, for example when an unknown
87+
// operator feature is present
6088
{
61-
INFO(
62-
"Current format loses some information when round-tripping through old");
89+
INFO("Malformed new format does not get misparsed as old format");
6390
nlohmann::json j = current;
64-
const auto intermediate = j.get<ccf::NodeInfoNetwork_v1>();
65-
nlohmann::json j2 = intermediate;
66-
const auto converted = j2.get<ccf::NodeInfoNetwork>();
67-
REQUIRE(!(current == converted));
68-
69-
// The node information has been kept
70-
REQUIRE(current.node_to_node_interface == converted.node_to_node_interface);
71-
72-
// Only the _first_ RPC interface has kept its addresses, though lost its
73-
// sessions caps
74-
REQUIRE(converted.rpc_interfaces.size() > 0);
75-
76-
const auto& current_interface = current.rpc_interfaces.begin()->second;
77-
const auto& converted_interface =
78-
converted.rpc_interfaces.at(ccf::PRIMARY_RPC_INTERFACE);
79-
80-
REQUIRE(current_interface.bind_address == converted_interface.bind_address);
81-
REQUIRE(
82-
current_interface.published_address ==
83-
converted_interface.published_address);
84-
REQUIRE(
85-
current_interface.max_open_sessions_hard !=
86-
converted_interface.max_open_sessions_hard);
87-
REQUIRE(
88-
current_interface.max_open_sessions_soft !=
89-
converted_interface.max_open_sessions_soft);
91+
// Inject an unknown operator feature to make the JSON invalid for v2
92+
j["node_to_node_interface"]["enabled_operator_features"].push_back(
93+
"UnknownFeature");
9094

91-
// The second RPC interface has been lost
92-
REQUIRE(converted.rpc_interfaces.size() == 1);
93-
REQUIRE(converted.rpc_interfaces.size() < current.rpc_interfaces.size());
95+
REQUIRE_THROWS_AS(j.get<ccf::NodeInfoNetwork>(), ccf::JsonParseError);
9496
}
9597

9698
{

0 commit comments

Comments
 (0)