目录 |
---|
调试方法
用 vscode 启动需要调试的 starcoin 进程,假设这个进程为 A,进程链接 dev 网络,启动另一个不需要调试的进程 starcoin B。
假设 A 是有区块的进程,B 需要同步 A 的区块(需要调试 B 则只要 vscode 启动 B 进程即可,也可以用 attach 的方法 同时调试 A 和 B)
vs code 调试设置见:/wiki/spaces/jack/pages/157286401
p2p 节点的连接
B 进程需要先和 A 联系上,在 A 节点中使用命令:
代码块 |
---|
node info |
此时输出 A 的节点信息,其中 self_address 就是 A 节点的地址,例如:
代码块 |
---|
"self_address": "/ip4/127.0.0.1/tcp/9840/p2p/12D3KooWSkzuSMR5RRXyQVYK1amvXqQPMdZ2wXfKUr8jajgWYrmL" |
复制地址 /ip4/127.0.0.1/tcp/9840/p2p/12D3KooWSkzuSMR5RRXyQVYK1amvXqQPMdZ2wXfKUr8jajgWYrmL ,切到 B,执行命令:
代码块 |
---|
node network add_peer /ip4/127.0.0.1/tcp/9840/p2p/12D3KooWSkzuSMR5RRXyQVYK1amvXqQPMdZ2wXfKUr8jajgWYrmL |
若成功会返回 OK。
执行:
代码块 |
---|
node network state |
也能发现 A 节点在 connectedPeers 这个列表中。
设置断点
在感兴趣的代码上设置好断点,区块同步的关键代码详见下面。
生成区块并同步
我们在 A 中生成一个区块即可触发 B 同步来同步 A 的区块。使用:
代码块 |
---|
dev gen-block |
即可,若提示账户不存在或者未解锁则使用 dev get-coin 命令去给 A 节点的账户发 token 或者 account unlock 一下即可。
如果我们事先设置好断点,过一会会在 A 的同步代码中暂停,此时就可以一探区块同步究竟了。
反过来若对 B 感兴趣可以做类似的断点跟踪。
观察区块状态
可以使用 chain 命令观察节点区块的状态,如:
代码块 |
---|
chain info |
可以查看是否已经同步。
调试建议
最好用 dev 网络调试,这样区块比较少,观察一个比较干净的网络比较容易看出问题。
同步区块前置概念
p2p 网络
区块链的网络连接均是基于 p2p网络,可以先了解一下 p2p 网络。
accumulator
starcoin 区块和交易在会使用 accumulator 作为 merkle proof 和 区块快速索引。其主要特点有:
1、sparce tries + merkle 两者结合的结构体,前者提供了快速索引能力,后者提供了 merkle proof;
2、基于 1,因此,叶子结点才是数据节点;
3、区块 accumulator 是指叶子结点实际包含了两个字段,一个是 id(哈希值),一个是 number,即区块高度;
4、基于3,由于叶子结点有 number 字段,因此,可以保证 accumulator 的增长方向是从左到右增长的(叶子结点是有序的,基于 number)。
更详细的文档可见:
https://cookbook.starcoin.org/zh/docs/concepts/smt
https://cookbook.starcoin.org/zh/docs/concepts/accumulator
Rust 和 starcoin 异步编程模型
区块同步大量采用的异步编程模型,先读懂:starcoin 异步模型分析 。
同步区块的逻辑简述
区块同步主要同步两个数据:
1、accumulator(区块和交易两个);
2、区块本身信息(包括交易和resource)。
同步 accumulator 一方面可以用于后续校验区块和交易,也可以帮助同步任务对齐两个节点的区块信息,断点续传。因此,同步区块的顺序是:
1、节点对齐各方的区块 number;
2、对齐后,落后节点(number 小的那一方)number + 1 为开始,批量拉取缺少的 accumulator 叶子结点;
3、accumulator 同步完成后,开始同步 block,同样以 number + 1为开始,同步 block,同步后验证 block 和交易,执行交易,存储 block等。
三个主要的同步任务与代码
FindAncestorTask
代码位于 sync/src/task/find_ancestor_task.rs。
从自身区块 current number (从 storage 的 startup info取出,即当前节点的区块高度)为参数,请求对方节点对应的区块信息(若对方节点没有对应的区块信息,则说明对方数据没自己的多),对方节点返回的区块信息包括区块 id 和 number,这时候,对方节点区块 id 应该是等于本节点区块的 id 的。
实际上这一步相当两个节点在确认信息,确认双方都有的最大的 number 区块,为后续任务确认 number 后以这个 number + 1 区块高度为始,开始同步。这个区块在代码中叫 ancestor block,任务叫 FindAncestorTask。
InnerSyncTask
代码在 sync/src/task/inner_sync_task.rs
这个相当于一个辅助 task,主要刷新了一下 peer,并拉起后续两个主要的 task(BlockAccumulatorSyncTask and then BlockSyncTask)。
BlockAccumulatorSyncTask
代码在 sync/src/task/accumulator_sync_task.rs。
第一步确认好 number 后,这一步开始以 number + 1 同步 accumulator 的叶子节点。在 AccumulatorCollector 中的 collect 函数,发现双方的 accumulator 的叶子结点数量一样后,同步动作结束,此时认为同步 accumulator 完成。
这一步调用的同步函数和上一步一样,都是调用节点的 fetch_block_ids 函数通过高度(number值)来批量拉取区块的 id。
BlockSyncTask
代码在 sync/src/task/accumulator_sync_task.rs。
有了 accumulator 后,以 number + 1 的区块 id 为开始,批量请求对方区块信息,并在同步后验证、验证和保存区块。调用的是节点的 fetch_blocks 函数来批量拉取区块的。
其它细节
同步区块时函数调用以及核心数据存放
同步的时候,sync 这边是用 VerifiedRpcClient(sync/src/verified_rpc_client.rs)这个类来进行 p2p 通信的,这个类实现对区块的读操作其实都在 network-rpc 的 NetworkRpc trait 有对应的实现,而这个 trait 的实现是 NetworkRpcImpl,都在 network-rpc 中。NetworkRpcImpl 又调用了 chain 的服务:ChainReaderService。ChainReaderService 封装 BlockChain 这个核心的数据,同步的数据源头就在这里面。
这个 BlockChain 包括之前提到的区块 accumulator,交易的 accumulutor,storage,叔块等。同步进程同样也会把同步到的数据存进这个数据结构中(即两边对等存放)。
按 crate 来划分,整条调用链路是:sync 到 network-rpc 到 chain。
peer 节点之间调用对应表,此处只列出核心的两个调用,其它依次类推:
动作 | 同步进程 | p2p 网络(NetworkRpc trait) | 被同步进程 | 发送消息 | 处理 |
---|---|---|---|---|---|
同步ancestor,找到双方都有的最大 number 值 | VerifiedRpcClient.get_block_ids | NetworkRpc.get_block_ids | NetworkRpcImpl.get_block_ids | ChainRequest::GetBlockIds | chain 处理:ChainReaderService.get_block_ids |
批量同步 accumulator 叶子结点(即区块的 id 和number) | VerifiedRpcClient.get_block_ids | NetworkRpc.get_block_ids | NetworkRpcImpl.get_block_ids | ChainRequest::GetBlockIds | chain 处理:ChainReaderService.get_block_ids |
批量同步区块数据 | VerifiedRpcClient.get_block | NetworkRpc.get_block | NetworkRpcImpl.get_block | ChainRequest::GetBlocks | chain 处理:ChainReaderService.get_block_ids |
主要的类调用:
peer 节点的选取
选取 peer 结点有四个策略,存放在 PeerStrategy,但看代码逻辑,PeerStrategy 的选取策略是最后一步,在进入这些选取策略之前,先挑出 best target(和 better target),再从中用 PeerStrategy 策略选出一个作为 VerifiedRpcClient(即代码中的 fetcher 字段)。
影响 best target 的选择主要有这几个纬度:
1、score;
2、reputation;
3、difficulty;
4、peer id;
5、是否支持 rpc。
开始 FindAncestorTask 前 会调用 PeerSelector 的 retain_rpc_peers 函数去掉不支持 rpc 调用的客户端;
然后调用 SyncFetch.get_best_target 函数获取最合适的 target 节点作为同步结点。
开始 BlockAccumulatorSyncTask 和 BlockSyncTask 前还会调用 SyncFetch.get_better_target 获取其它 peer 节点。
best target 和 better target
SyncFetch.get_best_target 策略
1、按 peer difficulty 从易到难排序(此步骤是否可以优化?代码做法是从难到易排序然后翻转变成从易到难)
2、从这些peer 中找到不同的status,作为一个集合
3、若集合元素数量大于1,则按peer id号排序
4、取集合元素最后一个元素作为候选best target
5、若候选的best target diffcult小于最小难度,则返回空
6、若候选best target 存在,则返回,否则返回none
SyncFetch.get_better_target 策略
除去上面的 best target 外,还可以找 better target 来同步,这个调用在同步 accumulator 之前做。
1、若 best target 的difficult 已经是最难,则直接用 best target。
2、从除去 best target外的 peers 中按 score 从小到大排序作为候选 better target,若这些 better target 也包含 best target 的区块,则也作为peer 节点集考虑进去。