Skip to content

Commit bd11afb

Browse files
gaurav137achamayou
andauthored
Adding notion of a recovery owner for network recovery (#6705)
Co-authored-by: Amaury Chamayou <[email protected]> Co-authored-by: Amaury Chamayou <[email protected]>
1 parent c384f5a commit bd11afb

File tree

23 files changed

+1147
-215
lines changed

23 files changed

+1147
-215
lines changed

doc/host_config_schema/cchost_config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@
325325
"data_json_file": {
326326
"type": ["string", "null"],
327327
"description": "Path to member data file (JSON)"
328+
},
329+
"recovery_role": {
330+
"type": "string",
331+
"enum": ["NonParticipant", "Participant", "Owner"],
332+
"description": "Whether the member acts as a recovery participant and gets assigned a share that can contribute towards a recovery threshold or as an owner and gets assigned a full recovery key"
328333
}
329334
},
330335
"required": ["certificate_file"],

doc/schemas/gov_openapi.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@
344344
"member_data": {
345345
"$ref": "#/components/schemas/json"
346346
},
347+
"recovery_role": {
348+
"$ref": "#/components/schemas/MemberRecoveryRole"
349+
},
347350
"status": {
348351
"$ref": "#/components/schemas/MemberStatus"
349352
}
@@ -412,6 +415,14 @@
412415
},
413416
"type": "object"
414417
},
418+
"MemberRecoveryRole": {
419+
"enum": [
420+
"NonParticipant",
421+
"Participant",
422+
"Owner"
423+
],
424+
"type": "string"
425+
},
415426
"MemberStatus": {
416427
"enum": [
417428
"Accepted",
@@ -1337,7 +1348,7 @@
13371348
"info": {
13381349
"description": "This API is used to submit and query proposals which affect CCF's public governance tables.",
13391350
"title": "CCF Governance API",
1340-
"version": "4.5.1"
1351+
"version": "4.6.0"
13411352
},
13421353
"openapi": "3.0.0",
13431354
"paths": {

include/ccf/service/tables/members.h

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ namespace ccf
2121
DECLARE_JSON_ENUM(
2222
MemberStatus,
2323
{{MemberStatus::ACCEPTED, "Accepted"}, {MemberStatus::ACTIVE, "Active"}});
24+
25+
enum class MemberRecoveryRole
26+
{
27+
NonParticipant = 0,
28+
Participant,
29+
30+
/** If set then the member is to receive a key allowing it
31+
to single-handedly recover the network without requiring
32+
any other recovery member to submit their shares. */
33+
Owner
34+
};
35+
DECLARE_JSON_ENUM(
36+
MemberRecoveryRole,
37+
{{MemberRecoveryRole::NonParticipant, "NonParticipant"},
38+
{MemberRecoveryRole::Participant, "Participant"},
39+
{MemberRecoveryRole::Owner, "Owner"}});
2440
}
2541

2642
namespace ccf
@@ -33,26 +49,31 @@ namespace ccf
3349
std::optional<ccf::crypto::Pem> encryption_pub_key = std::nullopt;
3450
nlohmann::json member_data = nullptr;
3551

52+
std::optional<MemberRecoveryRole> recovery_role = std::nullopt;
53+
3654
NewMember() {}
3755

3856
NewMember(
3957
const ccf::crypto::Pem& cert_,
4058
const std::optional<ccf::crypto::Pem>& encryption_pub_key_ = std::nullopt,
41-
const nlohmann::json& member_data_ = nullptr) :
59+
const nlohmann::json& member_data_ = nullptr,
60+
const std::optional<MemberRecoveryRole>& recovery_role_ = std::nullopt) :
4261
cert(cert_),
4362
encryption_pub_key(encryption_pub_key_),
44-
member_data(member_data_)
63+
member_data(member_data_),
64+
recovery_role(recovery_role_)
4565
{}
4666

4767
bool operator==(const NewMember& rhs) const
4868
{
4969
return cert == rhs.cert && encryption_pub_key == rhs.encryption_pub_key &&
50-
member_data == rhs.member_data;
70+
member_data == rhs.member_data && recovery_role == rhs.recovery_role;
5171
}
5272
};
5373
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(NewMember)
5474
DECLARE_JSON_REQUIRED_FIELDS(NewMember, cert)
55-
DECLARE_JSON_OPTIONAL_FIELDS(NewMember, encryption_pub_key, member_data)
75+
DECLARE_JSON_OPTIONAL_FIELDS(
76+
NewMember, encryption_pub_key, member_data, recovery_role)
5677

5778
struct MemberDetails
5879
{
@@ -62,14 +83,17 @@ namespace ccf
6283
members for example. */
6384
nlohmann::json member_data = nullptr;
6485

86+
std::optional<MemberRecoveryRole> recovery_role = std::nullopt;
87+
6588
bool operator==(const MemberDetails& rhs) const
6689
{
67-
return status == rhs.status && member_data == rhs.member_data;
90+
return status == rhs.status && member_data == rhs.member_data &&
91+
recovery_role == rhs.recovery_role;
6892
}
6993
};
7094
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(MemberDetails)
7195
DECLARE_JSON_REQUIRED_FIELDS(MemberDetails, status)
72-
DECLARE_JSON_OPTIONAL_FIELDS(MemberDetails, member_data)
96+
DECLARE_JSON_OPTIONAL_FIELDS(MemberDetails, member_data, recovery_role)
7397

7498
using MemberInfo = ServiceMap<MemberId, MemberDetails>;
7599

samples/constitutions/default/actions.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,25 @@ const actions = new Map([
372372
function (args) {
373373
checkX509CertBundle(args.cert, "cert");
374374
checkType(args.member_data, "object?", "member_data");
375-
// Also check that public encryption key is well formed, if it exists
375+
const recovery_role = args.recovery_role;
376+
if (recovery_role !== undefined) {
377+
checkEnum(
378+
recovery_role,
379+
["NonParticipant", "Participant", "Owner"],
380+
"recovery_role",
381+
);
382+
}
376383

377-
// Check if member exists
378-
// if not, check there is no enc pub key
379-
// if it does, check it doesn't have an enc pub key in ledger
384+
if (
385+
args.encryption_pub_key == null &&
386+
args.recovery_role !== null &&
387+
args.recovery_role !== undefined
388+
) {
389+
throw new Error(
390+
"Cannot specify a recovery_role value when encryption_pub_key is not specified",
391+
);
392+
}
393+
// Also check that public encryption key is well formed, if it exists
380394
},
381395

382396
function (args) {
@@ -401,6 +415,7 @@ const actions = new Map([
401415

402416
let member_info = {};
403417
member_info.member_data = args.member_data;
418+
member_info.recovery_role = args.recovery_role;
404419
member_info.status = "Accepted";
405420
ccf.kv["public:ccf.gov.members.info"].set(
406421
rawMemberId,

src/crypto/sharing.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,20 @@ namespace ccf::crypto
5656
return k;
5757
}
5858

59-
std::vector<uint8_t> serialise() const
59+
void serialise(std::vector<uint8_t>& serialised) const
6060
{
6161
auto size = serialised_size;
62-
std::vector<uint8_t> serialised(size);
62+
if (serialised.size() != size)
63+
{
64+
throw std::invalid_argument("Invalid serialised share size");
65+
}
66+
6367
auto data = serialised.data();
6468
serialized::write(data, size, x);
6569
for (size_t i = 0; i < LIMBS; ++i)
6670
{
6771
serialized::write(data, size, y[i]);
6872
}
69-
return serialised;
7073
}
7174

7275
Share(const std::span<uint8_t const>& serialised)

src/crypto/test/secret_sharing.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ TEST_CASE("Serialisation")
172172
share.y[7] = 6;
173173
share.y[8] = 7;
174174
share.y[9] = 56;
175-
Share new_share(share.serialise());
175+
std::vector<uint8_t> serialised(share.serialised_size);
176+
share.serialise(serialised);
177+
Share new_share(serialised);
176178

177179
INFO(share.to_str());
178180
INFO(new_share.to_str());

src/host/configuration.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,18 @@ namespace host
4848
std::string certificate_file;
4949
std::optional<std::string> encryption_public_key_file = std::nullopt;
5050
std::optional<std::string> data_json_file = std::nullopt;
51+
std::optional<ccf::MemberRecoveryRole> recovery_role = std::nullopt;
5152

5253
bool operator==(const ParsedMemberInfo& other) const = default;
5354
};
5455

5556
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ParsedMemberInfo);
5657
DECLARE_JSON_REQUIRED_FIELDS(ParsedMemberInfo, certificate_file);
5758
DECLARE_JSON_OPTIONAL_FIELDS(
58-
ParsedMemberInfo, encryption_public_key_file, data_json_file);
59+
ParsedMemberInfo,
60+
encryption_public_key_file,
61+
data_json_file,
62+
recovery_role);
5963

6064
struct CCHostConfig : public ccf::CCFConfig
6165
{

src/host/main.cpp

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -221,38 +221,76 @@ int main(int argc, char** argv)
221221
"On start, ledger directory should not exist ({})",
222222
config.ledger.directory));
223223
}
224+
224225
// Count members with public encryption key as only these members will be
225226
// handed a recovery share.
226-
// Note that it is acceptable to start a network without any member having
227-
// a recovery share. The service will check that at least one recovery
228-
// member is added before the service can be opened.
229-
size_t members_with_pubk_count = 0;
227+
// Note that it is acceptable to start a network without any member
228+
// having a recovery share. The service will check that at least one
229+
// recovery member (participant or owner) is added before the
230+
// service can be opened.
231+
size_t recovery_participants_count = 0;
232+
size_t recovery_owners_count = 0;
230233
for (auto const& m : config.command.start.members)
231234
{
232235
if (m.encryption_public_key_file.has_value())
233236
{
234-
members_with_pubk_count++;
237+
auto role =
238+
m.recovery_role.value_or(ccf::MemberRecoveryRole::Participant);
239+
if (role == ccf::MemberRecoveryRole::Participant)
240+
{
241+
recovery_participants_count++;
242+
}
243+
else if (role == ccf::MemberRecoveryRole::Owner)
244+
{
245+
recovery_owners_count++;
246+
}
235247
}
236248
}
237249

238250
recovery_threshold =
239251
config.command.start.service_configuration.recovery_threshold;
240252
if (recovery_threshold == 0)
241253
{
242-
LOG_INFO_FMT(
243-
"Recovery threshold unset. Defaulting to number of initial "
244-
"consortium members with a public encryption key ({}).",
245-
members_with_pubk_count);
246-
recovery_threshold = members_with_pubk_count;
254+
if (recovery_participants_count == 0 && recovery_owners_count != 0)
255+
{
256+
LOG_INFO_FMT(
257+
"Recovery threshold unset. Defaulting to 1 as only consortium "
258+
"members that are recovery owners ({}) are specified.",
259+
recovery_owners_count);
260+
recovery_threshold = 1;
261+
}
262+
else
263+
{
264+
LOG_INFO_FMT(
265+
"Recovery threshold unset. Defaulting to number of initial "
266+
"consortium members with a public encryption key ({}).",
267+
recovery_participants_count);
268+
recovery_threshold = recovery_participants_count;
269+
}
247270
}
248-
else if (recovery_threshold > members_with_pubk_count)
271+
else
249272
{
250-
throw std::logic_error(fmt::format(
251-
"Recovery threshold ({}) cannot be greater than total number ({})"
252-
"of initial consortium members with a public encryption "
253-
"key (specified via --member-info options)",
254-
recovery_threshold,
255-
members_with_pubk_count));
273+
if (recovery_participants_count == 0 && recovery_owners_count != 0)
274+
{
275+
if (recovery_threshold > 1)
276+
{
277+
throw std::logic_error(fmt::format(
278+
"Recovery threshold ({}) cannot be greater than 1 when all "
279+
"initial consortium members ({}) are of type recovery owner "
280+
"(specified via --member-info options)",
281+
recovery_threshold,
282+
recovery_participants_count));
283+
}
284+
}
285+
else if (recovery_threshold > recovery_participants_count)
286+
{
287+
throw std::logic_error(fmt::format(
288+
"Recovery threshold ({}) cannot be greater than total number ({})"
289+
"of initial consortium members with a public encryption "
290+
"key (specified via --member-info options)",
291+
recovery_threshold,
292+
recovery_participants_count));
293+
}
256294
}
257295
}
258296
}
@@ -612,12 +650,17 @@ int main(int argc, char** argv)
612650
for (auto const& m : config.command.start.members)
613651
{
614652
std::optional<ccf::crypto::Pem> public_encryption_key = std::nullopt;
653+
std::optional<ccf::MemberRecoveryRole> recovery_role = std::nullopt;
615654
if (
616655
m.encryption_public_key_file.has_value() &&
617656
!m.encryption_public_key_file.value().empty())
618657
{
619658
public_encryption_key = ccf::crypto::Pem(
620659
files::slurp(m.encryption_public_key_file.value()));
660+
if (m.recovery_role.has_value())
661+
{
662+
recovery_role = m.recovery_role.value();
663+
}
621664
}
622665

623666
nlohmann::json md = nullptr;
@@ -629,7 +672,8 @@ int main(int argc, char** argv)
629672
startup_config.start.members.emplace_back(
630673
ccf::crypto::Pem(files::slurp(m.certificate_file)),
631674
public_encryption_key,
632-
md);
675+
md,
676+
recovery_role);
633677
}
634678
startup_config.start.constitution = "";
635679
for (const auto& constitution_path :

src/node/gov/handlers/acks.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,12 @@ namespace ccf::gov::endpoints
266266
return;
267267
}
268268

269-
// If this is a newly-active recovery member in an open service,
270-
// allocate them a recovery share immediately
269+
// If this is a newly-active recovery participant/owner in an open
270+
// service, allocate them a recovery share immediately
271271
if (
272272
newly_active &&
273-
InternalTablesAccess::is_recovery_member(ctx.tx, member_id))
273+
InternalTablesAccess::is_recovery_participant_or_owner(
274+
ctx.tx, member_id))
274275
{
275276
auto service_status =
276277
InternalTablesAccess::get_service_status(ctx.tx);

0 commit comments

Comments
 (0)