目录
1.前言
前4章笔者学习分析了hotstuff底层源码及相关逻辑,但是还是会觉得不同的模块逻辑很孤立,这一章笔者尝试从源码提供的demo实验入手,逐步去理解hotstuff的执行,笔者会尽量还原逻辑。
2.源码分析
环境准备
# ensure openssl and libevent are installed on your machine, more
# specifically, you need:
#
# CMake >= 3.9 (cmake)
# C++14 (g++)
# libuv >= 1.10.0 (libuv1-dev)//加密和通信安全库
# openssl >= 1.1.0 (libssl-dev)//异步I/O和事件驱动库
#
# on Ubuntu: sudo apt-get install libssl-dev libuv1-dev cmake make
启动文件
# start 4 demo replicas with scripts/run_demo.sh
# then, start the demo client with scripts/run_demo_client.sh
run_demo.sh
#!/bin/bash
# 定义一个包含 0 到 3 的数组,表示默认的副本编号
rep=({0..3})
# 如果脚本的命令行参数个数大于 0,则使用传入的参数作为副本编号
if [[ $# -gt 0 ]]; then
rep=($@)
fi
# 设定无限制的栈大小,避免在简单演示中因引导副本产生的长承诺解析链导致栈溢出
ulimit -s unlimited
# 循环启动指定编号的副本
for i in "${rep[@]}"; do
echo "starting replica $i" # 输出正在启动的副本编号
# 使用 valgrind 进行内存泄漏检查并启动 hotstuff 应用,结果保存到 log 文件中
#valgrind --leak-check=full ./examples/hotstuff-app --conf hotstuff-sec${i}.conf > log${i} 2>&1 &
# 使用 gdb 进行调试并启动 hotstuff 应用,获取回溯信息,结果保存到 log 文件中
#gdb -ex r -ex bt -ex q --args ./examples/hotstuff-app --conf hotstuff-sec${i}.conf > log${i} 2>&1 &
# 正常启动 hotstuff 应用,并将输出保存到对应的 log 文件中
./examples/hotstuff-app --conf ./hotstuff-sec${i}.conf > log${i} 2>&1 &
done
# 等待所有后台进程结束
wait
- 该脚本首先定义了一个包含默认副本编号(0 到 3)的数组。
- 如果脚本执行时传入了命令行参数,这些参数将覆盖默认的副本编号。
ulimit -s unlimited
命令用来设置栈大小为无限制,以防止栈溢出。- 脚本使用
for
循环遍历每个副本编号,依次启动对应的 HotStuff 应用程序,并将其输出保存到日志文件中。 - 注释掉的
valgrind
和gdb
命令行用于调试和内存泄漏检查。 - 脚本最后使用
wait
命令等待所有后台进程结束,确保所有副本都正常启动和运行。
run_demo_client.sh
#!/bin/bash
# 尝试像在 run_demo.sh 中那样首先运行副本,然后运行 run_demo_client.sh。
# 使用 Ctrl-C 终止提议的副本(例如副本 0)。这将触发领导者轮换。
# 尝试杀死进程并再次运行 run_demo_client.sh,一旦新的领导者变得稳定,新命令应该仍然能够通过(被复制)。
# 运行 hotstuff 客户端,设置索引为 0,并无限次迭代 (--iter -1),
# 允许最多 4 个异步请求同时进行 (--max-async 4)。
./examples/hotstuff-client --idx 0 --iter -1 --max-async 4
- 脚本旨在演示 HotStuff 共识协议中的领导者轮换机制。
- 在运行副本后,运行此客户端脚本,启动 HotStuff 客户端与指定的副本进行通信。
- 脚本默认连接的副本索引为 0,并且设置为无限次迭代请求(
--iter -1
),允许最多 4 个异步请求同时进行(--max-async 4
)。 - 通过手动终止提议的副本(例如使用
Ctrl-C
),可以触发领导者轮换过程。 - 在领导者轮换后,重新运行客户端,新的命令应该仍然可以成功复制到其他副本上,确保系统的容错性。
hotstuff_app
run_demo.sh文件中会启动hotstuff-app(执行文件)并传递了参数 --conf ./hotstuff-sec${i}.conf。我们先来看这个conf文件,里面包括了私钥,tls私钥、tls证书和idx,当传参给执行文件时会把这个文件的内容传进去。
privkey = 445fa01dbbb9d0510ab6d6f630c94ab43ba21ad91f31d47da92f7b679ba2f582
tls-privkey = 308204a10201000282010100c5a358cee61ca7592dbe62cd28225426dc3fc65b33432043efb5e8fcfaf334720b2000d10c422172e6f94f82746928a6d51193342a1555684eedb141321170cb02cc7b189f5506cc30cb5c26d9b84123328ed6df61fc1a6582b046def7a662bb15d84e29ffa279f1d853c25ca79fbe3fab0200f8c85566a5eedb98df4a958e8512a647258e55a7a56c1ca8b05607b38b01b6a2669af9a70efa59141f6079341c6044f615687be756d4830ea359139ea35e23f34ce226215a1a2fb8271cc60a037841e44c460e149a71d3e2fa907a48677ad18af2e7348343932f5f29ac5241cd20ce64e6798ada684164cd1109c3a969662d9aaf9897fedc5a4e256eb92bf7bd02011102820100516160cdaa0bcc7003c6dd6388ff139787de0661c9d0589471c35fefb2a060e3aa3a5ab06e75954d6e2a6c088a496b1784e91e7ee426e6eeb7169448058eb5f93d6341bed83211db9b9f07d3c30fa259c9861c3ddd0d7447ea84d1e356ea28a7635911205a33d7dc0dc822dadb9c2129466a3ca2acd7def9080011c55af249bd98e3f84a5475a43cc0369cf7fa5e4ecdd1c09ac0b0a41bd19d8af70cad8f1b6aec66a22179e5dbf9c446c645e9daa9080b455fca64365d8e02a686fbd707198302db43cd37629033574561c025d89d29c393746ce12fa01bbe60feb3fab292928ead3f8f6def6b9a7dffcd2139f6e8d1f79844d216b28dfae29dc6ba8c11063102818100f770f6dea12236dd39cb46ba96c171f3cfa92c961a12bfb62108e82e0bac280554a9ad4b99c4faee9287467573eaf6cfcba360753668c40ab1b015fbbb95f6a2d3c2402966971fa54540befdc39b58962f7d0cfaa96ff268df02efab32238544ed75fd6586bd93e38934c9d59b037f3cd7f727b38c854e1061de1f68bf43a26d02818100cc7963002015bd593af15276f185cb5a34a76bb36633c85bc86c6640419612a5a8eb2cc1a6ad43e51c2b545f88a7e8c0096a6110b85de7d1f458eca33267f9fde6b4489e346304787d33c8e1033e96b502a53ea6ad2c55299cae973719587d24468f164930f58a56aac2c7eabd68870a1056f06b8bcec5b3d2156c8a1375d89102818048c6df326ba0a6b9897805be68933fa20fe676868023a1cc27d57176f45fcf8918e69c61879449cdb2a041e64f451b6a4af3d1136a5b0c7b9dac42b3736857994d57400c2d3b81c7327c7468c10f9286867012e04ff3bfc47dd3afe70ebf273263f586c381fb85d982b52c4de24c52996cb21abc56818f6e3ae6fa2ddde6b74d028180180e47e1e5a83464d9c209b3a3f19f740631d06f756f80fbbd39ede97120b6e6501baae99b2371663f8ca083b5b966ad2e48c02015b0b1dc77198540604877c3848dae30bade78ff1dc9db65c4257b245aaa075ee732645f3f9c11ca3f37964080c58a26ba773d739b9e71df6193d3a6d4beef1bb618537e912fb26a98e0b01102818100950d7d5ec366f8bca918011359590c6e7d6fe4d3e8922d68468c3e24277f27621db17cb41d0dcddf1f77e289e49dc92101d8544e84281ec7732fbfd4e9194924a2adac4e22ef79c2ad61428f1fa03e6affa3b0de07f857de87a2e758953122b0d693cf49e54ede4eba44dcdc2c5cbd6751af5117fd107fc0e4ffb18f8774d79e
tls-cert = 308202e4308201cc020101300d06092a864886f70d01010505003039310b300906035504061302555331163014060355040a0c0d6c696273616c746963696461653112301006035504030c096c6f63616c686f7374301e170d3139303730323036353535365a170d3139303730323036353535365a3039310b300906035504061302555331163014060355040a0c0d6c696273616c746963696461653112301006035504030c096c6f63616c686f737430820120300d06092a864886f70d01010105000382010d00308201080282010100c5a358cee61ca7592dbe62cd28225426dc3fc65b33432043efb5e8fcfaf334720b2000d10c422172e6f94f82746928a6d51193342a1555684eedb141321170cb02cc7b189f5506cc30cb5c26d9b84123328ed6df61fc1a6582b046def7a662bb15d84e29ffa279f1d853c25ca79fbe3fab0200f8c85566a5eedb98df4a958e8512a647258e55a7a56c1ca8b05607b38b01b6a2669af9a70efa59141f6079341c6044f615687be756d4830ea359139ea35e23f34ce226215a1a2fb8271cc60a037841e44c460e149a71d3e2fa907a48677ad18af2e7348343932f5f29ac5241cd20ce64e6798ada684164cd1109c3a969662d9aaf9897fedc5a4e256eb92bf7bd020111300d06092a864886f70d0101050500038201010009fc34f2835e4c14de1e03202c0f0884acb51e7568b286a9b3a374e8b69f304600a2787303cd382e52923db352cf91587ea7a33da5116c886c67775b5f96357971dddb568948fbb12987523a8be0e2e537284f8ee591af17c6d58410e41d32fc634f99289089915fb4273feaaca45d3442820155f5014fbd5d19fef7954729e4439218cad6a83b9dcd21834aaec379a276f163599cdac6b9513d0c07310852bb4104a58141a158d302908c23761899a4c68c3bf81115f302d41f42ff38150c0b7dc798fd7319abec6bd9365e00bf0a0998c3b97fe4834f0688e95d1eea4df4031274c781c6661cab085d629cb924c5c65b865ae98aa2349875fafe62b7eacbc5
idx = 0
main函数
int main(int argc, char **argv) {
// 创建一个 Config 对象,指定配置文件名为 "hotstuff.conf"
Config config("hotstuff.conf");
// 创建一个 ElapsedTime 对象,并开始计时
ElapsedTime elapsed;
elapsed.start();
// 定义并初始化所有的配置选项
auto opt_blk_size = Config::OptValInt::create(1); // 块大小,默认值为 1
auto opt_parent_limit = Config::OptValInt::create(-1); // 父节点限制,默认值为 -1(无限制)
auto opt_stat_period = Config::OptValDouble::create(10); // 统计周期,默认值为 10 秒
auto opt_replicas = Config::OptValStrVec::create(); // 复制节点列表
auto opt_idx = Config::OptValInt::create(0); // 当前节点索引,默认值为 0
auto opt_client_port = Config::OptValInt::create(-1); // 客户端端口,默认值为 -1(未指定)
auto opt_privkey = Config::OptValStr::create(); // 私钥文件路径
auto opt_tls_privkey = Config::OptValStr::create(); // TLS 私钥文件路径
auto opt_tls_cert = Config::OptValStr::create(); // TLS 证书文件路径
auto opt_help = Config::OptValFlag::create(false); // 帮助标志,默认值为 false
auto opt_pace_maker = Config::OptValStr::create("dummy"); // 节奏控制器类型,默认值为 "dummy"
auto opt_fixed_proposer = Config::OptValInt::create(1); // 固定提议者的 ID,默认值为 1
auto opt_base_timeout = Config::OptValDouble::create(1); // 基本超时时间,默认值为 1 秒
auto opt_prop_delay = Config::OptValDouble::create(1); // 提议延迟时间,默认值为 1 秒
auto opt_imp_timeout = Config::OptValDouble::create(11); // 弹劾超时时间,默认值为 11 秒
auto opt_nworker = Config::OptValInt::create(1); // 验证线程数量,默认值为 1
auto opt_repnworker = Config::OptValInt::create(1); // 复制节点网络线程数量,默认值为 1
auto opt_repburst = Config::OptValInt::create(100); // 复制节点网络的突发大小,默认值为 100
auto opt_clinworker = Config::OptValInt::create(8); // 客户端网络线程数量,默认值为 8
auto opt_cliburst = Config::OptValInt::create(1000); // 客户端网络的突发大小,默认值为 1000
auto opt_notls = Config::OptValFlag::create(false); // 是否禁用 TLS,默认值为 false
auto opt_max_rep_msg = Config::OptValInt::create(4 << 20); // 最大复制节点消息大小,默认值为 4MB
auto opt_max_cli_msg = Config::OptValInt::create(65536); // 最大客户端消息大小,默认值为 64KB
// 添加配置选项到 Config 对象中
config.add_opt("block-size", opt_blk_size, Config::SET_VAL); // 设置块大小
config.add_opt("parent-limit", opt_parent_limit, Config::SET_VAL); // 设置父节点限制
config.add_opt("stat-period", opt_stat_period, Config::SET_VAL); // 设置统计周期
config.add_opt("replica", opt_replicas, Config::APPEND, 'a', "add an replica to the list"); // 添加复制节点
config.add_opt("idx", opt_idx, Config::SET_VAL, 'i', "specify the index in the replica list"); // 指定复制节点索引
config.add_opt("cport", opt_client_port, Config::SET_VAL, 'c', "specify the port listening for clients"); // 指定客户端端口
config.add_opt("privkey", opt_privkey, Config::SET_VAL); // 设置私钥文件路径
config.add_opt("tls-privkey", opt_tls_privkey, Config::SET_VAL); // 设置 TLS 私钥文件路径
config.add_opt("tls-cert", opt_tls_cert, Config::SET_VAL); // 设置 TLS 证书文件路径
config.add_opt("pace-maker", opt_pace_maker, Config::SET_VAL, 'p', "specify pace maker (dummy, rr)"); // 指定节奏控制器类型
config.add_opt("proposer", opt_fixed_proposer, Config::SET_VAL, 'l', "set the fixed proposer (for dummy)"); // 设置固定提议者 ID
config.add_opt("base-timeout", opt_base_timeout, Config::SET_VAL, 't', "set the initial timeout for the Round-Robin Pacemaker"); // 设置基本超时时间
config.add_opt("prop-delay", opt_prop_delay, Config::SET_VAL, 't', "set the delay that follows the timeout for the Round-Robin Pacemaker"); // 设置提议延迟时间
config.add_opt("imp-timeout", opt_imp_timeout, Config::SET_VAL, 'u', "set impeachment timeout (for sticky)"); // 设置弹劾超时时间
config.add_opt("nworker", opt_nworker, Config::SET_VAL, 'n', "the number of threads for verification"); // 设置验证线程数量
config.add_opt("repnworker", opt_repnworker, Config::SET_VAL, 'm', "the number of threads for replica network"); // 设置复制节点网络线程数量
config.add_opt("repburst", opt_repburst, Config::SET_VAL, 'b', ""); // 设置复制节点网络的突发大小
config.add_opt("clinworker", opt_clinworker, Config::SET_VAL, 'M', "the number of threads for client network"); // 设置客户端网络线程数量
config.add_opt("cliburst", opt_cliburst, Config::SET_VAL, 'B', ""); // 设置客户端网络的突发大小
config.add_opt("notls", opt_notls, Config::SWITCH_ON, 's', "disable TLS"); // 禁用 TLS
config.add_opt("max-rep-msg", opt_max_rep_msg, Config::SET_VAL, 'S', "the maximum replica message size"); // 设置最大复制节点消息大小
config.add_opt("max-cli-msg", opt_max_cli_msg, Config::SET_VAL, 'S', "the maximum client message size"); // 设置最大客户端消息大小
config.add_opt("help", opt_help, Config::SWITCH_ON, 'h', "show this help info"); // 显示帮助信息
// 创建一个 EventContext 对象,用于事件处理
EventContext ec;
// 解析命令行参数
config.parse(argc, argv);
// 如果帮助标志被设置,则打印帮助信息并退出
if (opt_help->get()) {
config.print_help();
exit(0);
}
// 获取命令行参数中指定的复制节点索引和客户端端口
auto idx = opt_idx->get();
auto client_port = opt_client_port->get();
// 解析复制节点列表
std::vector<std::tuple<std::string, std::string, std::string>> replicas;
for (const auto &s: opt_replicas->get()) {
auto res = trim_all(split(s, ","));
if (res.size() != 3)
throw HotStuffError("invalid replica info"); // 如果格式不正确,抛出异常
replicas.push_back(std::make_tuple(res[0], res[1], res[2])); // 将解析结果存储到 replicas 向量中
}
// 验证复制节点索引是否有效
if (!(0 <= idx && (size_t)idx < replicas.size()))
throw HotStuffError("replica idx out of range");
// 获取指定复制节点的绑定地址
std::string binding_addr = std::get<0>(replicas[idx]);
// 如果客户端端口未指定,则从绑定地址中提取客户端端口
if (client_port == -1) {
auto p = split_ip_port_cport(binding_addr);
size_t idx;
try {
client_port = stoi(p.second, &idx); // 尝试将端口号从字符串转换为整数
} catch (std::invalid_argument &) {
throw HotStuffError("client port not specified"); // 如果转换失败,抛出异常
}
}
// 创建 NetAddr 对象,用于监听来自复制节点的连接
NetAddr plisten_addr{split_ip_port_cport(binding_addr).first};
// 获取父节点限制配置项的值
auto parent_limit = opt_parent_limit->get();
// 根据节奏控制器类型创建相应的节奏控制器对象
hotstuff::pacemaker_bt pmaker;
if (opt_pace_maker->get() == "dummy")
pmaker = new hotstuff::PaceMakerDummyFixed(opt_fixed_proposer->get(), parent_limit); // Dummy 类型节奏控制器
else
pmaker = new hotstuff::PaceMakerRR(ec, parent_limit, opt_base_timeout->get(), opt_prop_delay->get()); // Round-Robin 类型节奏控制器
// 创建网络配置对象
HotStuffApp::Net::Config repnet_config;
ClientNetwork<opcode_t>::Config clinet_config;
// 设置最大消息大小
repnet_config.max_msg_size(opt_max_rep_msg->get());
clinet_config.max_msg_size(opt_max_cli_msg->get());
// 如果 TLS 私钥和证书路径不为空,并且 TLS 未禁用,则启用 TLS
if (!opt_tls_privkey->get().empty() && !opt_notls->get()) {
auto tls_priv_key = new salticidae::PKey(
salticidae::PKey::create_privkey_from_der(
hotstuff::from_hex(opt_tls_privkey->get())));
auto tls_cert = new salticidae::X509(
salticidae::X509::create_from_der(
hotstuff::from_hex(opt_tls_cert->get())));
repnet_config
.enable_tls(true)
.tls_key(tls_priv_key)
.tls_cert(tls_cert);
}
// 设置复制节点网络和客户端网络的线程数和突发数
repnet_config
.burst_size(opt_repburst->get())
.nworker(opt_repnworker->get());
clinet_config
.burst_size(opt_cliburst->get())
.nworker(opt_clinworker->get());
// 创建 HotStuff 应用对象
papp = new HotStuffApp(opt_blk_size->get(),
opt_stat_period->get(),
opt_imp_timeout->get(),
idx,
hotstuff::from_hex(opt_privkey->get()),
plisten_addr,
NetAddr("0.0.0.0", client_port),
std::move(pmaker),
ec,
opt_nworker->get(),
repnet_config,
clinet_config);
// 解析复制节点列表并创建复制节点的网络地址、私钥和证书
std::vector<std::tuple<NetAddr, bytearray_t, bytearray_t>> reps;
for (auto &r: replicas) {
auto p = split_ip_port_cport(std::get<0>(r));
reps.push_back(std::make_tuple(
NetAddr(p.first),
hotstuff::from_hex(std::get<1>(r)),
hotstuff::from_hex(std::get<2>(r))));
}
// 定义一个关闭回调函数,当收到 SIGINT 或 SIGTERM 信号时停止应用程序
auto shutdown = [&](int) { papp->stop(); };
salticidae::SigEvent ev_sigint(ec, shutdown);
salticidae::SigEvent ev_sigterm(ec, shutdown);
ev_sigint.add(SIGINT);
ev_sigterm.add(SIGTERM);
// 启动 HotStuff 应用程序
papp->start(reps);
// 停止计时并打印消耗时间
elapsed.stop(true);
return 0; // 程序正常结束
}
配置文件初始化:
Config config("hotstuff.conf");
程序开始时,创建了一个 Config
对象,用于读取和解析配置文件 "hotstuff.conf"
。 hotstuff.conf文件如下,包括区块大小,pacemaker的类别(rr),以及不同副本的端口号等
block-size = 1
pace-maker = rr
replica = 127.0.0.1:10000;20000, 039f89215177475ac408d079b45acef4591fc477dd690f2467df052cf0c7baba23, 542865a568784c4e77c172b82e99cb8a1a53b7bee5f86843b04960ea4157f420
replica = 127.0.0.1:10001;20001, 0278740a5bec75e333b3c93965b1609163b15d2e3c2fdef141d4859ec70c238e7a, c261250345ebcd676a0edeea173526608604f626b2e8bc4fd2142d3bde1d44d5
replica = 127.0.0.1:10002;20002, 0269eb606576a315a630c2483deed35cc4bd845abae1c693f97c440c89503fa92e, 065b010aed5629edfb5289e8b22fc6cc6b33c4013bfdd128caba80c3c02d6d78
replica = 127.0.0.1:10003;20003, 03e6911bf17e632eecdfa0dc9fc6efc9ddca60c0e3100db469a3d3d62008044a53, 6540a0fea67efcb08f53ec3a952df4c3f0e2e07c2778fd92320807717e29a651
计时器启动:
创建并启动了一个计时器,用于记录程序的运行时间。
定义配置选项并将配置选项添加到 Config
对象中:
定义并初始化了多个配置选项,比如区块大小、父节点限制、统计周期等。这些选项用于在命令行或配置文件中配置应用程序的行为。然后将配置选项添加到 Config
对象中,这样可以通过命令行参数或配置文件来设置这些选项的值。
auto opt_blk_size = Config::OptValInt::create(1);
auto opt_parent_limit = Config::OptValInt::create(-1);
auto opt_stat_period = Config::OptValDouble::create(10);
...
config.add_opt("block-size", opt_blk_size, Config::SET_VAL);
config.add_opt("parent-limit", opt_parent_limit, Config::SET_VAL);
...
解析命令行参数:
解析传递给程序的命令行参数,将其值与之前定义的配置选项关联。
config.parse(argc, argv);
显示帮助信息并退出:
如果用户请求帮助信息(通过 --help
参数),程序会打印帮助信息并退出。
if (opt_help->get()) {
config.print_help();
exit(0);
}
解析复制节点列表:
从配置中读取复制节点列表(定义在hotstuff.conf中),并将每个节点的信息解析并存储在 replicas
向量中。res[i]如下:
res[0]:127.0.0.1:10000;20000
res[1]:039f89215177475ac408d079b45acef4591fc477dd690f2467df052cf0c7baba23
res[2]:542865a568784c4e77c172b82e99cb8a1a53b7bee5f86843b04960ea4157f420
std::vector<std::tuple<std::string, std::string, std::string>> replicas;
for (const auto &s: opt_replicas->get()) {
auto res = trim_all(split(s, ","));
if (res.size() != 3)
throw HotStuffError("invalid replica info"); // 如果格式不正确,抛出异常
replicas.push_back(std::make_tuple(res[0], res[1], res[2])); // 将解析结果存储到 replicas 向量中
}
验证复制节点索引有效性:
检查启动协议的当前节点的索引是否在有效范围内。
auto idx = opt_idx->get();
if (!(0 <= idx && (size_t)idx < replicas.size()))
throw HotStuffError("replica idx out of range");
创建客户端端口和副本监听地址:
- 获取binding_addr(127.0.0.1:10000;20000)
- 尝试获取客户端端口(因为是demo,所以配置环境中没有指定)➡从副本地址(binding_addr)中获取➡client_port=20000
- 从绑定地址中提取 IP 地址,并创建用于监听来自复制节点连接的
NetAddr
对象plisten_addr(127.0.0.1)。
// 获取指定复制节点的绑定地址
std::string binding_addr = std::get<0>(replicas[idx]);
auto client_port = opt_client_port->get();
// 如果客户端端口未指定,则从绑定地址中提取客户端端口
if (client_port == -1) {
auto p = split_ip_port_cport(binding_addr);
size_t idx;
try {
client_port = stoi(p.second, &idx); // 尝试将端口号从字符串转换为整数
} catch (std::invalid_argument &) {
throw HotStuffError("client port not specified"); // 如果转换失败,抛出异常
}
}
NetAddr plisten_addr{split_ip_port_cport(binding_addr).first};
- 127.0.0.1:10000;20000➡split_ip_port_cport函数➡(127.0.0.1:10000,20000)
// 分割IP和端口号的函数
std::pair<std::string, std::string> split_ip_port_cport(const std::string &s) {
auto ret = trim_all(split(s, ";")); // 通过分号分割并去除空格
if (ret.size() != 2)
throw std::invalid_argument("invalid cport format"); // 格式不对时抛出异常
return std::make_pair(ret[0], ret[1]); // 返回IP和端口号
}
创建节奏控制器:
根据配置中的节奏控制器类型创建相应的节奏控制器对象。可以是 PaceMakerDummyFixed
或 PaceMakerRR。(在hotstuff.conf中配置的是rr,因此是启动
既具备父块选择的逻辑,也具备轮流提议者的机制的pacemaker)(节奏器在第四章)
// 获取父节点限制配置项的值
auto parent_limit = opt_parent_limit->get();
hotstuff::pacemaker_bt pmaker;
if (opt_pace_maker->get() == "dummy")
pmaker = new hotstuff::PaceMakerDummyFixed(opt_fixed_proposer->get(), parent_limit);
else
pmaker = new hotstuff::PaceMakerRR(ec, parent_limit, opt_base_timeout->get(), opt_prop_delay->get());
网络配置(peer网络和client网络)
初始化复制节点网络和客户端网络的配置,包括最大消息大小、线程数等设置。并且启动TLS
// 创建网络配置对象
HotStuffApp::Net::Config repnet_config;//using Net = PeerNetwork<opcode_t>;
ClientNetwork<opcode_t>::Config clinet_config;//using salticidae::ClientNetwork;
// 设置最大消息大小
repnet_config.max_msg_size(opt_max_rep_msg->get());//auto opt_max_rep_msg = Config::OptValInt::create(4 << 20); 最大复制节点消息大小,默认值为 4MB
clinet_config.max_msg_size(opt_max_cli_msg->get());//auto opt_max_cli_msg = Config::OptValInt::create(65536);最大客户端消息大小,默认值为 64KB
// 如果 TLS 私钥和证书路径不为空,并且 TLS 未禁用,则启用 TLS
if (!opt_tls_privkey->get().empty() && !opt_notls->get()) {
auto tls_priv_key = new salticidae::PKey(
salticidae::PKey::create_privkey_from_der(
hotstuff::from_hex(opt_tls_privkey->get())));
auto tls_cert = new salticidae::X509(
salticidae::X509::create_from_der(
hotstuff::from_hex(opt_tls_cert->get())));//using salticidae::from_hex;
repnet_config
.enable_tls(true)
.tls_key(tls_priv_key)
.tls_cert(tls_cert);
}
// 设置复制节点网络和客户端网络的线程数和突发数
repnet_config
.burst_size(opt_repburst->get())//auto opt_repburst = Config::OptValInt::create(100); // 复制节点网络的突发大小,默认值为 100
.nworker(opt_repnworker->get());//auto opt_repnworker = Config::OptValInt::create(1); // 复制节点网络线程数量,默认值为 1
clinet_config
.burst_size(opt_cliburst->get())//auto opt_cliburst = Config::OptValInt::create(1000); // 客户端网络的突发大小,默认值为 1000
.nworker(opt_clinworker->get());//auto opt_clinworker = Config::OptValInt::create(8); // 客户端网络线程数量,默认值为 8
创建 HotStuff 应用程序实例:
构建协议app并且启动协议
salticidae::BoxObj<HotStuffApp> papp = nullptr; // HotStuffApp的全局对象
// 创建 HotStuff 应用对象
papp = new HotStuffApp(opt_blk_size->get(),
opt_stat_period->get(),
opt_imp_timeout->get(),
idx,
hotstuff::from_hex(opt_privkey->get()),
plisten_addr,
NetAddr("0.0.0.0", client_port),
std::move(pmaker),
ec,
opt_nworker->get(),
repnet_config,
clinet_config);
// 解析复制节点列表并创建复制节点的网络地址、私钥和证书
std::vector<std::tuple<NetAddr, bytearray_t, bytearray_t>> reps;
for (auto &r: replicas) {
auto p = split_ip_port_cport(std::get<0>(r));
reps.push_back(std::make_tuple(
NetAddr(p.first),
hotstuff::from_hex(std::get<1>(r)),
hotstuff::from_hex(std::get<2>(r))));
}
// 定义一个关闭回调函数,当收到 SIGINT 或 SIGTERM 信号时停止应用程序
auto shutdown = [&](int) { papp->stop(); };
salticidae::SigEvent ev_sigint(ec, shutdown);
salticidae::SigEvent ev_sigterm(ec, shutdown);
ev_sigint.add(SIGINT);
ev_sigterm.add(SIGTERM);
// 启动 HotStuff 应用程序
papp->start(reps);
// 停止计时并打印消耗时间
elapsed.stop(true);
HostuffApp
HotStuffApp
类实现了 HotStuff 共识协议的核心功能。它管理与客户端的通信、处理请求和响应、维护定时器以及进行系统统计信息打印。
class HotStuffApp: public HotStuff {
double stat_period; // 统计周期
double impeach_timeout; // 弹劾超时时间
EventContext ec; // 事件上下文
EventContext req_ec; // 请求事件上下文
EventContext resp_ec; // 响应事件上下文
/** 副本和客户端之间的网络消息传递。*/
ClientNetwork<opcode_t> cn;
/** 定时器对象,用于调度周期性打印系统统计信息 */
TimerEvent ev_stat_timer;
/** 定时器对象,用于监控简单弹劾的进展 */
TimerEvent impeach_timer;
/** 客户端RPC的监听地址 */
NetAddr clisten_addr;
// 未确认的命令映射,使用命令的哈希值作为键
std::unordered_map<const uint256_t, promise_t> unconfirmed;
using conn_t = ClientNetwork<opcode_t>::conn_t;
using resp_queue_t = salticidae::MPSCQueueEventDriven<std::pair<Finality, NetAddr>>;//多生产者单消费者队列
/* 用于处理发送给客户端响应的专用线程 */
std::thread req_thread;
std::thread resp_thread;
resp_queue_t resp_queue; // 响应队列
salticidae::BoxObj<salticidae::ThreadCall> resp_tcall; // 响应线程调用对象
salticidae::BoxObj<salticidae::ThreadCall> req_tcall; // 请求线程调用对象
// 客户端请求命令处理器
void client_request_cmd_handler(MsgReqCmd &&, const conn_t &);
// 解析命令的静态方法
static command_t parse_cmd(DataStream &s) {
auto cmd = new CommandDummy();
s >> *cmd;
return cmd;
}
// 重置弹劾定时器
void reset_imp_timer() {
impeach_timer.del(); // 删除当前定时器
impeach_timer.add(impeach_timeout); // 添加新的定时器,设置为弹劾超时
}
// 状态机执行逻辑的重写函数
void state_machine_execute(const Finality &fin) override {
reset_imp_timer(); // 每次执行时重置弹劾定时器
#ifndef HOTSTUFF_ENABLE_BENCHMARK
HOTSTUFF_LOG_INFO("replicated %s", std::string(fin).c_str()); // 打印日志信息
#endif
}
#ifdef HOTSTUFF_MSG_STAT
std::unordered_set<conn_t> client_conns; // 客户端连接集合
void print_stat() const; // 打印统计信息
#endif
public:
// 构造函数
HotStuffApp(
uint32_t blk_size, // 区块大小,表示每个区块中包含的命令数量
double stat_period, // 统计周期,指定多长时间进行一次系统统计信息打印
double impeach_timeout, // 弹劾超时时间,指定在没有达成共识时,触发弹劾的等待时间
ReplicaID idx, // 当前副本的唯一标识(ID)
const bytearray_t &raw_privkey, // 副本的私钥,用于签名和验证
NetAddr plisten_addr, // 副本之间通信的监听地址,用于与其他副本建立连接
NetAddr clisten_addr, // 客户端通信的监听地址,用于接受客户端请求
hotstuff::pacemaker_bt pmaker, // 节奏器(Pacemaker),用于驱动共识协议的进展
const EventContext &ec, // 事件上下文,管理事件的循环和调度
size_t nworker, // 工作线程的数量,用于并行处理任务
const Net::Config &repnet_config, // 副本网络的配置参数
const ClientNetwork<opcode_t>::Config &clinet_config // 客户端网络的配置参数
);
/ 启动函数
// reps 参数是一个包含多个副本信息的向量,每个元素都是一个三元组
// 三元组中的元素分别表示副本的网络地址、公开密钥(公钥)和验证密钥
void start(const std::vector<std::tuple<NetAddr, bytearray_t, bytearray_t>> &reps) {
// 启动 HotStuffApp 实例并开始接受客户端请求和处理共识
// 这个函数可能会初始化网络连接、启动工作线程以及设置事件循环
}
// 停止函数
// 停止 HotStuffApp 实例的运行
void stop() {
// 停止所有运行中的线程、断开网络连接、清理资源
// 这个函数会确保应用程序干净地关闭,不会留下悬挂的操作
}
};
HotstuffApp构造函数
构造与客户端之间的通信网络并引用基类(Hotstuff)的构造函数,回看前几章,Hotstuff构造会引用HotstuffBase构造(负责peer网络构造以及调用核心类HotstuffCore构造函数),HotstuffCore函数负责构造核心逻辑构造(创世区块、尾块等等)
HotStuffApp::HotStuffApp(uint32_t blk_size,
double stat_period,
double impeach_timeout,
ReplicaID idx,
const bytearray_t &raw_privkey,
NetAddr plisten_addr,
NetAddr clisten_addr,
hotstuff::pacemaker_bt pmaker,
const EventContext &ec,
size_t nworker,
const Net::Config &repnet_config,
const ClientNetwork<opcode_t>::Config &clinet_config)
: HotStuff(blk_size, idx, raw_privkey,
plisten_addr, std::move(pmaker), ec, nworker, repnet_config),
stat_period(stat_period),
impeach_timeout(impeach_timeout),
ec(ec),
cn(req_ec, clinet_config),
clisten_addr(clisten_addr) {
/* 准备用于发送确认的线程 */
resp_tcall = new salticidae::ThreadCall(resp_ec); // 创建一个新的线程调用对象,用于响应处理线程
req_tcall = new salticidae::ThreadCall(req_ec); // 创建另一个线程调用对象,用于请求处理线程
/* 注册响应队列处理程序 */
resp_queue.reg_handler(resp_ec, [this](resp_queue_t &q) {
std::pair<Finality, NetAddr> p; // 定义队列对,包含最终性和网络地址
while (q.try_dequeue(p)) { // 尝试从队列中出队元素
try {
cn.send_msg(MsgRespCmd(std::move(p.first)), p.second); // 发送响应消息给客户端
} catch (std::exception &err) {
HOTSTUFF_LOG_WARN("unable to send to the client: %s", err.what()); // 发送失败时记录警告
}
}
return false; // 返回 false 表示处理程序没有继续等待新的消息
});
/* 注册客户端消息处理程序 */
cn.reg_handler(salticidae::generic_bind(&HotStuffApp::client_request_cmd_handler, this, _1, _2));
cn.start(); // 启动客户端网络
cn.listen(clisten_addr); // 开始监听客户端连接
}
HotstuffApp启动函数
HotStuffApp::start
函数的主要目的是初始化和启动 HotStuffApp
实例所需的各个组件,包括统计定时器、弹劾定时器、请求和响应处理线程,以及客户端连接管理。它设置了定时器以处理周期性任务,并开始了事件主循环以处理实时事件。这使得 HotStuffApp
实例能够全面启动并稳定运行。
void HotStuffApp::start(const std::vector<std::tuple<NetAddr, bytearray_t, bytearray_t>> &reps) {
// 初始化系统统计定时器,并设置回调函数
ev_stat_timer = TimerEvent(ec, [this](TimerEvent &) {
HotStuff::print_stat(); // 打印基础统计信息
HotStuffApp::print_stat(); // 打印应用级别的统计信息
// HotStuffCore::prune(100); // 可选:修剪核心数据,保留最新的100个条目
ev_stat_timer.add(stat_period); // 重新设置定时器,继续下一个统计周期
});
ev_stat_timer.add(stat_period); // 添加并启动统计定时器
// 初始化弹劾定时器,并设置回调函数
impeach_timer = TimerEvent(ec, [this](TimerEvent &) {
if (get_decision_waiting().size()) // 如果有未决的决策(共识尚未达成)
get_pace_maker()->impeach(); // 启动节奏器的弹劾过程,尝试更换领导者
reset_imp_timer(); // 重置弹劾定时器
});
impeach_timer.add(impeach_timeout); // 添加并启动弹劾定时器
// 打印启动信息和参数
HOTSTUFF_LOG_INFO("** starting the system with parameters **");
HOTSTUFF_LOG_INFO("blk_size = %lu", blk_size); // 打印区块大小
HOTSTUFF_LOG_INFO("conns = %lu", HotStuff::size()); // 打印当前连接数
HOTSTUFF_LOG_INFO("** starting the event loop...");
// 调用基类的 start 函数,启动共识协议并初始化副本信息
HotStuff::start(reps);
// 注册客户端连接处理器
cn.reg_conn_handler([this](const salticidae::ConnPool::conn_t &_conn, bool connected) {
auto conn = salticidae::static_pointer_cast<conn_t::type>(_conn);
if (connected)
client_conns.insert(conn); // 如果连接建立,添加到客户端连接集合
else
client_conns.erase(conn); // 如果连接断开,从集合中移除
return true; // 返回 true 表示处理成功
});
// 启动请求处理线程
req_thread = std::thread([this]() { req_ec.dispatch(); });
// 启动响应处理线程
resp_thread = std::thread([this]() { resp_ec.dispatch(); });
// 进入事件主循环,开始处理事件
ec.dispatch();
}
HotstuffApp停止函数
void HotStuffApp::stop() {
// 异步调用请求处理线程的停止方法
papp->req_tcall->async_call([this](salticidae::ThreadCall::Handle &) {
req_ec.stop(); // 停止请求事件循环
});
// 异步调用响应处理线程的停止方法
papp->resp_tcall->async_call([this](salticidae::ThreadCall::Handle &) {
resp_ec.stop(); // 停止响应事件循环
});
// 等待请求和响应处理线程结束
req_thread.join();
resp_thread.join();
// 停止主事件循环
ec.stop();
}
请求获取
void HotStuffApp::client_request_cmd_handler(MsgReqCmd &&msg, const conn_t &conn) {
const NetAddr addr = conn->get_addr(); // 获取客户端的网络地址
auto cmd = parse_cmd(msg.serialized); // 解析客户端消息中的命令
const auto &cmd_hash = cmd->get_hash(); // 获取命令的哈希值
HOTSTUFF_LOG_DEBUG("processing %s", std::string(*cmd).c_str()); // 记录调试信息,显示正在处理的命令
// 执行命令并将结果放入响应队列
exec_command(cmd_hash, [this, addr](Finality fin) {
resp_queue.enqueue(std::make_pair(fin, addr)); // 将最终性和客户端地址加入响应队列
});
}
hotstuff_client
基础定义
定义了一些全局变量、请求结构体等
// 全局变量定义
EventContext ec; // 事件上下文,用于处理事件循环
ReplicaID proposer; // 提议者的 ID
size_t max_async_num; // 最大并发发送命令数
int max_iter_num; // 最大迭代次数
uint32_t cid; // 客户端 ID
uint32_t cnt = 0; // 命令计数器
uint32_t nfaulty; // 允许的故障副本数量
// 请求结构体,用于存储命令、确认数和计时器
struct Request {
command_t cmd; // 命令
size_t confirmed; // 已确认的数量
salticidae::ElapsedTime et; // 计时器
Request(const command_t &cmd): cmd(cmd), confirmed(0) { et.start(); } // 初始化请求
};
// 网络类型定义
using Net = salticidae::MsgNetwork<opcode_t>;
// 存储副本连接和等待中的请求
std::unordered_map<ReplicaID, Net::conn_t> conns; // 副本ID到连接对象的映射
std::unordered_map<const uint256_t, Request> waiting; // 命令哈希到请求对象的映射
std::vector<NetAddr> replicas; // 存储副本地址的向量
std::vector<std::pair<struct timeval, double>> elapsed; // 用于基准测试的时间数据
std::unique_ptr<Net> mn; // 网络对象的智能指针
main函数
相对于app来说,client只需要关注客户端与节点的交互,笔者还是先从main函数入手
int main(int argc, char **argv) {
// 创建配置对象,读取配置文件 "hotstuff.conf"
Config config("hotstuff.conf");
// 配置选项初始化
auto opt_idx = Config::OptValInt::create(0); // 副本索引,默认为 0
auto opt_replicas = Config::OptValStrVec::create(); // 副本地址列表
auto opt_max_iter_num = Config::OptValInt::create(100); // 最大迭代次数,默认为 100
auto opt_max_async_num = Config::OptValInt::create(10); // 最大并发发送命令数,默认为 10
auto opt_cid = Config::OptValInt::create(-1); // 客户端 ID,默认为 -1,表示使用默认值
auto opt_max_cli_msg = Config::OptValInt::create(65536); // 最大客户端消息大小,默认为 64K
// 信号处理,优雅停止事件循环
// 定义信号处理函数,停止事件循环
auto shutdown = [&](int) { ec.stop(); };
salticidae::SigEvent ev_sigint(ec, shutdown); // 处理 SIGINT 信号
salticidae::SigEvent ev_sigterm(ec, shutdown); // 处理 SIGTERM 信号
ev_sigint.add(SIGINT); // 将 SIGINT 信号与 shutdown 处理程序关联
ev_sigterm.add(SIGTERM); // 将 SIGTERM 信号与 shutdown 处理程序关联
// 初始化网络对象
mn = std::make_unique<Net>(ec, Net::Config().max_msg_size(opt_max_cli_msg->get())); // 创建网络对象,并设置最大消息大小
mn->reg_handler(client_resp_cmd_handler); // 注册客户端响应命令的处理回调函数
mn->start(); // 启动网络对象,使其开始接收和处理消息
// 解析配置选项
config.add_opt("idx", opt_idx, Config::SET_VAL); // 添加副本索引选项
config.add_opt("cid", opt_cid, Config::SET_VAL); // 添加客户端 ID 选项
config.add_opt("replica", opt_replicas, Config::APPEND); // 添加副本地址列表选项
config.add_opt("iter", opt_max_iter_num, Config::SET_VAL); // 添加最大迭代次数选项
config.add_opt("max-async", opt_max_async_num, Config::SET_VAL); // 添加最大并发发送命令数选项
config.add_opt("max-cli-msg", opt_max_cli_msg, Config::SET_VAL, 'S', "the maximum client message size"); // 添加最大客户端消息大小选项
config.parse(argc, argv); // 解析命令行参数并更新配置选项
// 获取配置值
auto idx = opt_idx->get(); // 获取副本索引
max_iter_num = opt_max_iter_num->get(); // 获取最大迭代次数
max_async_num = opt_max_async_num->get(); // 获取最大并发发送命令数
std::vector<std::string> raw; // 存储副本地址的原始字符串列表
// 解析副本地址列表
for (const auto &s: opt_replicas->get()) {
auto res = salticidae::trim_all(salticidae::split(s, ",")); // 去除多余空格并拆分地址
if (res.size() < 1)
throw HotStuffError("format error"); // 如果格式错误,则抛出异常
raw.push_back(res[0]); // 提取 IP 和端口,存储在 raw 向量中
}
// 检查副本索引是否在有效范围内
if (!(0 <= idx && (size_t)idx < raw.size() && raw.size() > 0))
throw std::invalid_argument("out of range"); // 如果副本索引超出范围,则抛出异常
// 设置客户端 ID,如果未指定,则使用副本索引作为默认值
cid = opt_cid->get() != -1 ? opt_cid->get() : idx;
// 解析副本地址,并存储在 replicas 向量中
for (const auto &p: raw) {
auto _p = split_ip_port_cport(p); // 拆分 IP 和端口
size_t _;
replicas.push_back(NetAddr(NetAddr(_p.first).ip, htons(stoi(_p.second, &_)))); // 存储副本地址
}
// 计算允许的故障副本数
nfaulty = (replicas.size() - 1) / 3;
HOTSTUFF_LOG_INFO("nfaulty = %zu", nfaulty); // 输出允许的故障副本数
// 连接到所有副本
connect_all();
// 尝试发送命令(可能会进入循环)
while (try_send());
// 启动事件循环,处理事件
ec.dispatch();
#ifdef HOTSTUFF_ENABLE_BENCHMARK
// 如果启用了基准测试,输出基准测试结果
for (const auto &e: elapsed) {
char fmt[64];
struct tm *tmp = localtime(&e.first.tv_sec); // 获取当前时间
// 格式化时间并输出基准测试结果
strftime(fmt, sizeof fmt, "%Y-%m-%d %H:%M:%S.%%06u [hotstuff info] %%.6f\n", tmp);
fprintf(stderr, fmt, e.first.tv_usec, e.second);
}
#endif
return 0; // 程序正常退出
}
除了配置、验证、网络启动,就是connect_all连接所有副本并try_send发送命令,以及client_resp_cmd_handler响应副本,我们主要关注这三个函数。
connect_all
连接所有副本并存储在conns中
void connect_all() {
// 遍历副本地址,建立与每个副本的连接
for (size_t i = 0; i < replicas.size(); i++)
// 使用 mn->connect_sync() 同步连接到副本,并将连接存储在 conns 中
conns.insert(std::make_pair(i, mn->connect_sync(replicas[i])));
}
try_send
当发送给每个副本后,协议会根据pacemaker的提议者id来决定谁是提议者。
// 尝试发送命令
bool try_send(bool check = true) {
// 检查是否满足发送条件(可选的检查和异步命令数量限制)
if ((!check || waiting.size() < max_async_num) && max_iter_num) {
// 创建一个新的命令,并构造请求消息
auto cmd = new CommandDummy(cid, cnt++); // 创建新命令
MsgReqCmd msg(*cmd); // 包装命令为请求消息
// 遍历所有副本连接,将消息发送到每个副本
for (auto &p: conns) mn->send_msg(msg, p.second); // 发送消息到所有副本
#ifndef HOTSTUFF_ENABLE_BENCHMARK
// 如果未启用基准测试,则记录发送的命令的日志
HOTSTUFF_LOG_INFO("send new cmd %.10s", get_hex(cmd->get_hash()).c_str());
#endif
// 将命令添加到等待中的请求中
waiting.insert(std::make_pair(cmd->get_hash(), Request(cmd)));
// 减少最大迭代次数(如果设置了限制)
if (max_iter_num > 0)
max_iter_num--;
return true; // 成功发送命令
}
return false; // 未满足发送条件
}
client_resp_cmd_handler
获取副本决策信息并移除waiting中的请求,然后重新发送命令
// 处理客户端响应命令的回调函数
void client_resp_cmd_handler(MsgRespCmd &&msg, const Net::conn_t &) {
// 处理客户端响应消息
auto &fin = msg.fin; // 获取响应消息中的命令完成信息
HOTSTUFF_LOG_DEBUG("got %s", std::string(msg.fin).c_str());
const uint256_t &cmd_hash = fin.cmd_hash; // 获取命令的哈希值
auto it = waiting.find(cmd_hash); // 查找等待中的请求
if (it == waiting.end()) return; // 如果请求不在等待中,则返回
auto &et = it->second.et; // 获取该请求的计时器
et.stop(); // 停止计时器以记录响应时间
if (++it->second.confirmed <= nfaulty) return; // 如果确认数未达到阈值,则返回
#ifndef HOTSTUFF_ENABLE_BENCHMARK
// 如果未启用基准测试,则记录响应的日志
HOTSTUFF_LOG_INFO("got %s, wall: %.3f, cpu: %.3f",
std::string(fin).c_str(),
et.elapsed_sec, et.cpu_elapsed_sec);
#else
// 如果启用了基准测试,则记录时间数据
struct timeval tv;
gettimeofday(&tv, nullptr); // 获取当前时间
elapsed.push_back(std::make_pair(tv, et.elapsed_sec)); // 记录基准测试数据
#endif
// 移除已处理的请求
waiting.erase(it);
// 尝试发送更多命令(如果有待发送的命令)
while (try_send());
}