Skip to content

Commit 03192af

Browse files
committed
Add apollo case for tls exchange with single signature scheme
1 parent 7955657 commit 03192af

File tree

5 files changed

+122
-66
lines changed

5 files changed

+122
-66
lines changed

client/bftclient/include/bftclient/config.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ struct ClientConfig {
7070
bool use_unified_certs = false;
7171
std::optional<std::string> transaction_signing_private_key_file_path = std::nullopt;
7272
std::optional<concord::secretsmanager::SecretData> secrets_manager_config = std::nullopt;
73-
std::optional<std::string> replicas_master_key_folder_path = "./replicas_rsa_keys";
73+
std::optional<std::string> replicas_master_key_folder_path = "./replicas_main_keys";
7474
concord::crypto::SignatureAlgorithm message_sigs_algorithm = concord::crypto::SignatureAlgorithm::EdDSA;
7575
};
7676

tests/apollo/test_skvbc_reconfiguration.py

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,36 @@ async def test_tls_exchange_replicas_replicas_and_replicas_client(self, bft_netw
487487
nb_fast_path = await bft_network.get_metric(r, bft_network, "Counters", "totalFastPaths")
488488
self.assertGreater(nb_fast_path, fast_paths[r])
489489

490+
@with_trio
491+
@with_bft_network(
492+
lambda builddir, replica_id: start_replica_cmd(builddir, replica_id) + ["--key-exchange-on-start", "True"],
493+
bft_configs=[BFTConfig(n=4, f=1, c=0)],
494+
with_cre=True, publish_master_keys=True, rotate_keys=True)
495+
async def test_tls_exchange_single_signature_scheme(self, bft_network):
496+
"""
497+
Ensure that progress can be made after tls exchange when the replicas'
498+
main key and the consensus key are identical
499+
"""
500+
bft_network.start_all_replicas()
501+
bft_network.start_cre()
502+
await bft_network.check_initial_master_key_publication(bft_network.all_replicas())
503+
await self.wait_until_cre_gets_replicas_master_keys(bft_network, bft_network.all_replicas())
504+
skvbc = kvbc.SimpleKVBCProtocol(bft_network)
505+
ops_for_exchange = (CHECKPOINT_SEQUENCES * 3) + 1
506+
await bft_network.wait_for_consensus_path(
507+
path_type=ConsensusPathType.OPTIMISTIC_FAST,
508+
run_ops=lambda: skvbc.send_n_kvs_sequentially(ops_for_exchange, description='Trigger stale key removal'),
509+
threshold=ops_for_exchange)
510+
511+
await self.run_replica_tls_key_exchange_cycle(bft_network, bft_network.all_replicas(), list(bft_network.all_replicas()) + [bft_network.cre_id])
512+
bft_network.copy_certs_from_server_to_clients(src=bft_network.config.n - 1)
513+
bft_network.restart_clients(False, False)
514+
ops_after_exchange = 100
515+
await bft_network.wait_for_consensus_path(
516+
path_type=ConsensusPathType.OPTIMISTIC_FAST,
517+
run_ops=lambda: skvbc.send_n_kvs_sequentially(ops_after_exchange, description='Make progress after tls rotation'),
518+
threshold=ops_after_exchange)
519+
490520
@with_trio
491521
@with_bft_network(start_replica_cmd, selected_configs=lambda n, f, c: n == 7, with_cre=True, publish_master_keys=True)
492522
async def test_tls_exchange_replicas_replicas_and_replicas_clients_with_st(self, bft_network):
@@ -563,28 +593,31 @@ async def run_replica_tls_key_exchange_cycle(self, bft_network, replicas, affect
563593
reps_data = {}
564594
if len(affected_replicas) == 0:
565595
affected_replicas = bft_network.all_replicas()
566-
for rep in affected_replicas:
567-
reps_data[rep] = {}
568-
for r in replicas:
569-
val = []
570-
cert_path = os.path.join(bft_network.certdir + "/" + str(rep), str(r))
571-
if bft_network.use_unified_certs:
572-
cert_path = os.path.join(cert_path, "node.cert")
573-
else:
574-
cert_path = os.path.join(cert_path, "server", "server.cert")
575-
576-
with open(cert_path) as orig_key:
577-
cert_text = orig_key.readlines()
578-
val += [cert_text]
579-
cert_path = os.path.join(bft_network.certdir + "/" + str(rep), str(r))
580-
if bft_network.use_unified_certs:
581-
cert_path = os.path.join(cert_path, "node.cert")
582-
else:
583-
cert_path = os.path.join(cert_path, "client", "client.cert")
584-
with open(cert_path) as orig_key:
585-
cert_text = orig_key.readlines()
586-
val += [cert_text]
587-
reps_data[rep][r] = val
596+
597+
with log.start_action(action_type="read_tls_certificates_before_rotation",
598+
participants_to_update=affected_replicas, participants_with_new_certs=replicas):
599+
for rep in affected_replicas:
600+
reps_data[rep] = {}
601+
for r in replicas:
602+
val = []
603+
cert_path = os.path.join(bft_network.certdir + "/" + str(rep), str(r))
604+
if bft_network.use_unified_certs:
605+
cert_path = os.path.join(cert_path, "node.cert")
606+
else:
607+
cert_path = os.path.join(cert_path, "server", "server.cert")
608+
609+
with open(cert_path) as orig_key:
610+
cert_text = orig_key.readlines()
611+
val += [cert_text]
612+
cert_path = os.path.join(bft_network.certdir + "/" + str(rep), str(r))
613+
if bft_network.use_unified_certs:
614+
cert_path = os.path.join(cert_path, "node.cert")
615+
else:
616+
cert_path = os.path.join(cert_path, "client", "client.cert")
617+
with open(cert_path) as orig_key:
618+
cert_text = orig_key.readlines()
619+
val += [cert_text]
620+
reps_data[rep][r] = val
588621
client = bft_network.random_client()
589622
op = operator.Operator(bft_network.config, client, bft_network.builddir)
590623

@@ -593,7 +626,8 @@ async def run_replica_tls_key_exchange_cycle(self, bft_network, replicas, affect
593626
except trio.TooSlowError:
594627
# As we rotate the TLS keys immediately, the call may return with timeout
595628
pass
596-
with trio.fail_after(60):
629+
with log.start_action(action_type="wait_for_new_tls_certificates",
630+
participants_to_update=affected_replicas, participants_with_new_certs=replicas) as action, trio.fail_after(60):
597631
succ = False
598632
while not succ:
599633
await trio.sleep(1)
@@ -610,6 +644,7 @@ async def run_replica_tls_key_exchange_cycle(self, bft_network, replicas, affect
610644
diff = difflib.unified_diff(reps_data[rep][r][0], cert_text, fromfile="new", tofile="old", lineterm='')
611645
lines = sum(1 for l in diff)
612646
if lines > 0:
647+
action.log(message_type=f"Participant id: {rep} got a new tls certificate for participant id: {r}")
613648
continue
614649
cert_path = os.path.join(bft_network.certdir + "/" + str(rep), str(r))
615650
if bft_network.use_unified_certs:
@@ -621,6 +656,7 @@ async def run_replica_tls_key_exchange_cycle(self, bft_network, replicas, affect
621656
diff = difflib.unified_diff(reps_data[rep][r][1], cert_text, fromfile="new", tofile="old", lineterm='')
622657
lines = sum(1 for l in diff)
623658
if lines > 0:
659+
action.log(message_type=f"Participant id: {rep} got a new tls certificate for participant id: {r}")
624660
continue
625661
succ = False
626662
break
@@ -665,7 +701,7 @@ async def test_key_exchange_no_single_signature_scheme(self, bft_network):
665701
selected_configs=lambda n, f, c: n == 7, publish_master_keys=True)
666702
async def test_single_signature_scheme_to_no_single_signature_scheme(self, bft_network):
667703
"""
668-
Ensure that the network can
704+
Ensures that the network can make progress after switching signature schemes
669705
"""
670706
bft_network.start_all_replicas()
671707
skvbc = kvbc.SimpleKVBCProtocol(bft_network)
@@ -2178,7 +2214,7 @@ async def wait_until_cre_gets_replicas_master_keys(self, bft_network, replicas):
21782214
while succ is False:
21792215
succ = True
21802216
for r in replicas:
2181-
master_key_path = os.path.join(bft_network.testdir, "replicas_rsa_keys", str(r), "pub_key")
2217+
master_key_path = os.path.join(bft_network.testdir, "replicas_main_keys", str(r), "pub_key")
21822218
if os.path.isfile(master_key_path) is False:
21832219
succ = False
21842220
break

tests/apollo/util/bft.py

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
# Add the pyclient directory to $PYTHONPATH
1414
import hashlib
15+
import pathlib
1516
from logging import setLogRecordFactory
1617
import sys
1718
import signal
@@ -33,6 +34,7 @@
3334
from typing import Coroutine, Sequence, Callable, Optional, Tuple, Dict, TextIO
3435
import re
3536
import trio
37+
import json
3638

3739
from util.test_base import repeat_test, ApolloTest
3840
from util.consts import CHECKPOINT_SEQUENCES
@@ -46,7 +48,8 @@
4648
import inspect
4749
from enum import Enum
4850
from util import bft_metrics, eliot_logging as log
49-
from util.eliot_logging import log_call, logdir_timestamp, log_message
51+
from util.eliot_logging import log_call, logdir_timestamp, log_message, add_destinations, remove_destination, \
52+
format_eliot_message
5053
from util import skvbc as kvbc
5154
from util.bft_test_exceptions import AlreadyRunningError, AlreadyStoppedError, KeyExchangeError, CreError
5255
from bft_config import BFTConfig
@@ -217,8 +220,9 @@ async def wrapper(*args, **kwargs):
217220
async with trio.open_nursery() as background_nursery:
218221
@repeat_test(num_repeats, break_on_first_failure, break_on_first_success, test_name)
219222
async def test_with_bft_network():
220-
with BftTestNetwork.new(config, background_nursery, with_cre=with_cre, use_unified_certs=use_unified_certs) as bft_network:
221-
bft_network.current_test = test_name
223+
with BftTestNetwork.new(
224+
config, background_nursery, with_cre=with_cre,
225+
use_unified_certs=use_unified_certs, case_name=test_name) as bft_network:
222226
seed = test_instance.test_seed
223227
with log.start_task(action_type=f"{async_fn.__name__}",
224228
bft_config=f'{bft_config}_clients={config.num_clients}',
@@ -256,8 +260,14 @@ async def test_with_bft_network():
256260
class BftTestNetwork:
257261
"""Encapsulates a BFT network instance for testing purposes"""
258262

263+
def write_message(self, message):
264+
self._eliot_log_file.write(f"{json.dumps(message)}\n")
265+
259266
def __enter__(self):
260267
"""context manager method for 'with' statements"""
268+
if not self._eliot_log_file:
269+
self._eliot_log_file = (self.current_test_case_path / 'eliot.log').open('a+')
270+
add_destinations(self.write_message)
261271
return self
262272

263273
def __exit__(self, etype, value, tb):
@@ -293,9 +303,14 @@ def __exit__(self, etype, value, tb):
293303
self.perf_proc.wait()
294304

295305
self.check_error_logs()
306+
remove_destination(self.write_message)
307+
if self._eliot_log_file:
308+
self._eliot_log_file.close()
309+
self._eliot_log_file = None
296310

297311
def __init__(self, is_existing, origdir, config, testdir, certdir, builddir, toolsdir,
298-
procs, replicas, clients, metrics, client_factory, background_nursery, ro_replicas=[]):
312+
procs, replicas, clients, metrics, client_factory, background_nursery, ro_replicas=[],
313+
case_name=""):
299314
self.is_existing = is_existing
300315
# An existing deployment might pass some of the folders paths as empty so we skip the next assertion.
301316
if not is_existing:
@@ -322,9 +337,8 @@ def __init__(self, is_existing, origdir, config, testdir, certdir, builddir, too
322337
else:
323338
self.client_factory = partial(self._create_new_client, BFT_CLIENT_TYPE)
324339
self.open_fds = {}
325-
self.current_test = ""
340+
self.current_test = case_name
326341
self.background_nursery = background_nursery
327-
self.current_test_case_path = None
328342
self.test_start_time = None
329343
self.perf_proc = None
330344
self.ro_replicas = ro_replicas
@@ -335,13 +349,25 @@ def __init__(self, is_existing, origdir, config, testdir, certdir, builddir, too
335349
self.cre_fds = None
336350
self.cre_id = self.config.n + self.config.num_ro_replicas + self.config.num_clients + RESERVED_CLIENTS_QUOTA
337351
self._logs_dir = Path(self.builddir) / "tests" / "apollo" / "logs" / logdir_timestamp()
352+
self._eliot_log_file = None
353+
self._suite_name = os.environ.get('TEST_NAME', None)
354+
if not self._suite_name:
355+
now = datetime.now().strftime("%y-%m-%d_%H:%M:%S")
356+
self._suite_name = f"{now}_{self.current_test}"
357+
358+
if os.environ.get('BLOCKCHAIN_VERSION', default="1").lower() == "4":
359+
self._suite_name = self._suite_name + "_v4"
360+
361+
self.current_test_case_path = self._logs_dir / self._suite_name / self.current_test
362+
self.current_test_case_path.mkdir(parents=True, exist_ok=True)
338363

339364
# Setup transaction signing parameters
340365
self.setup_txn_signing()
341366
self._generate_operator_keys()
342367

343368
@classmethod
344-
def new(cls, config, background_nursery, client_factory=None, with_cre=False, use_unified_certs=False):
369+
def new(cls, config, background_nursery, client_factory=None, with_cre=False, use_unified_certs=False,
370+
case_name=None):
345371
builddir = os.path.abspath("../../build")
346372
toolsdir = os.path.join(builddir, "tools")
347373
testdir = tempfile.mkdtemp(prefix='testdir_')
@@ -367,7 +393,8 @@ def new(cls, config, background_nursery, client_factory=None, with_cre=False, us
367393
ro_replicas=[bft_config.Replica(i, "127.0.0.1",
368394
bft_config.bft_msg_port_from_node_id(i),
369395
bft_config.metrics_port_from_node_id(i))
370-
for i in range(config.n, config.n + config.num_ro_replicas)]
396+
for i in range(config.n, config.n + config.num_ro_replicas)],
397+
case_name=case_name
371398
)
372399
bft_network.with_cre = with_cre
373400
bft_network.use_unified_certs = use_unified_certs
@@ -427,13 +454,6 @@ def start_cre(self):
427454
stderr_file = None
428455

429456
if os.environ.get('KEEP_APOLLO_LOGS', "").lower() in ["true", "on"]:
430-
test_name = os.environ.get('TEST_NAME')
431-
432-
if not test_name:
433-
now = datetime.now().strftime("%y-%m-%d_%H:%M:%S")
434-
test_name = f"{now}_{self.current_test}"
435-
436-
self.current_test_case_path = self._logs_dir / test_name / self.current_test
437457
test_log = f"{self.current_test_case_path / 'stdout_cre.log' }"
438458

439459
Path(self.current_test_case_path).mkdir(parents=True, exist_ok=True)
@@ -590,7 +610,7 @@ def generate_tls_certs(self, num_to_generate, start_index=0, use_unified_certs=F
590610

591611
def copy_certs_from_server_to_clients(self, src):
592612
src_cert = self.certdir + "/" + str(src)
593-
for c in range(self.num_total_replicas() , self.num_total_replicas() + self.config.num_clients + RESERVED_CLIENTS_QUOTA):
613+
for c in range(self.num_total_replicas(), self.num_total_replicas() + self.config.num_clients + RESERVED_CLIENTS_QUOTA):
594614
comp_cert_dir = self.certdir + "/" + str(c)
595615
shutil.rmtree(comp_cert_dir, ignore_errors=True)
596616
shutil.copytree(src_cert, comp_cert_dir)
@@ -615,7 +635,7 @@ def _create_reserved_clients(self):
615635

616636
def new_reserved_client(self):
617637
if len(self.reserved_client_ids_in_use) == RESERVED_CLIENTS_QUOTA:
618-
raise NotImplemented("You must increase RESERVED_CLIENTS_QUOTA, see comment above")
638+
raise NotImplemented("You must increase RESERVED_CLIENTS_QUOTA, see comment above")
619639
start_id = self.num_total_replicas() + self.config.num_clients
620640
reserved_client_ids = [ id for id in range(start_id, start_id + RESERVED_CLIENTS_QUOTA) ]
621641
free_reserved_client_ids = set(reserved_client_ids) - set(self.reserved_client_ids_in_use)
@@ -849,13 +869,7 @@ def start_replica(self, replica_id):
849869
keep_logs = os.environ.get('KEEP_APOLLO_LOGS', "").lower() in ["true", "on"]
850870

851871
if keep_logs:
852-
test_name = os.environ.get('TEST_NAME')
853-
if os.environ.get('BLOCKCHAIN_VERSION', default="1") == "4" :
854-
test_name = test_name + "_v4"
855-
856-
test_name = test_name if test_name else f"{self.current_test}"
857-
858-
self.current_test_case_path = self._logs_dir / test_name / self.current_test
872+
suite_name = self._suite_name
859873
replica_test_log_path = self.current_test_case_path / f"stdout_{replica_id}.log"
860874

861875
Path(self.current_test_case_path).mkdir(parents=True, exist_ok=True)
@@ -876,7 +890,7 @@ def start_replica(self, replica_id):
876890
"""
877891
if os.environ.get('CODECOVERAGE', "").lower() in ["true", "on"] and \
878892
os.environ.get('GRACEFUL_SHUTDOWN',"").lower() in ["true", "on"]:
879-
self.coverage_dir = f"{self.builddir}/tests/apollo/codecoverage/{test_name}/{self.current_test}/"
893+
self.coverage_dir = f"{self.builddir}/tests/apollo/codecoverage/{suite_name}/{self.current_test}/"
880894
Path(self.coverage_dir).mkdir(parents=True, exist_ok=True)
881895
profraw_file_name = f"coverage_{replica_id}_%9m.profraw"
882896
profraw_file_path = os.path.join(

tests/apollo/util/eliot_logging.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from functools import lru_cache
2-
from eliot import start_action, start_task, to_file, add_destinations, log_call, log_message
2+
from eliot import start_action, start_task, to_file, add_destinations, log_call, log_message, remove_destination
33
from datetime import datetime
44
import os
55
import sys
@@ -60,29 +60,35 @@ def set_file_destination():
6060
to_file(open(test_log, "a+"))
6161

6262

63-
# Set logs to the console
64-
def stdout(message):
63+
def format_eliot_message(message):
6564
if message.get("action_status", "") == "succeeded":
6665
return
6766

6867
additional_fields = [(key, val) for key, val in message.items() if key not in
69-
("action_type", "action_status", "result", "task_uuid",
70-
"timestamp", "task_level", "message_type")]
68+
("action_type", "action_status", "result", "task_uuid",
69+
"timestamp", "task_level", "message_type")]
7170

7271
fields = [datetime.fromtimestamp(message["timestamp"]).strftime("%d/%m/%Y %H:%M:%S.%f"),
73-
message.get("message_type", None),
74-
message.get("action_type", None),
75-
message.get("action_status", None),
76-
message.get("result", None),
77-
str(additional_fields).replace('\\n', '\n'),
78-
message["task_uuid"],
79-
]
72+
message.get("message_type", None),
73+
message.get("action_type", None),
74+
message.get("action_status", None),
75+
message.get("result", None),
76+
str(additional_fields).replace('\\n', '\n'),
77+
message["task_uuid"],
78+
]
79+
80+
return f' - '.join([field for field in fields if field])
8081

81-
print(f' - '.join([field for field in fields if field]))
82+
83+
# Set logs to the console
84+
def eliot_stdout(message):
85+
formatted = format_eliot_message(message)
86+
if formatted:
87+
print(formatted)
8288

8389

8490
if os.getenv('APOLLO_LOG_STDOUT', False):
85-
add_destinations(stdout)
91+
add_destinations(eliot_stdout)
8692

8793
if os.environ.get('KEEP_APOLLO_LOGS', "").lower() in ["true", "on"]:
8894
# Uncomment to see logs in console

tests/simpleKVBC/TesterCRE/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ creParams setupCreParams(int argc, char** argv) {
5353
{"use-unified-certs", optional_argument, 0, 'U'},
5454
{0, 0, 0, 0}};
5555
creParams cre_param;
56-
cre_param.replicasKeysFolder = "./replicas_rsa_keys";
56+
cre_param.replicasKeysFolder = "./replicas_main_keys";
5757
ClientConfig& client_config = cre_param.bftConfig;
5858
int o = 0;
5959
int optionIndex = 0;

0 commit comments

Comments
 (0)