0%

StudyRecord-Bsc详解-bsc_relayer同步区块头

引言:

20220922173707
目的:

  • 本地搭建起BSC和BC两条链

  • 启动bsc_relayer,能成功与两条链建立连接

  • bsc_relayer要能同步数据成功:拉取BC的数据,同步到BSC上,能成功修改bsc对应合约里的数据

bsc-relayer中继器:

简介:

bsc-relayer 是一个独立进程,主要有两个功能:

  • 拉取 BC 的块头,并同步给 BSC

  • 拉取 BC 的跨链数据包,并同步跨链数据包给 BSC

跨链同步主要涉及两个系统合约,分别为

  • TendermintLightClient.sol: 同步块头的操作时会调用到

  • CrossChain.sol: 同步跨链数据包的操作会调用到

与两条链的连接方式:

bsc-relayer 与 BC、BSC 均通过 RPC 进行通信。

  1. bsc-relayer <-----> BC:
    bsc-relayer 通过发送不同的请求信息获取 BC 上的数据,例如 abci_infoblock等。

  2. bsc-relayer <-----> BSC:
    bsc-relayer 直接使用了 Etheruem 提供的 RPC 模块,可直接调用发送数据或者请求,其中最常用的是eth_sendRawTransaction进行交易的发送。

20220922175541
RPC 的配置信息记录在 config/config.json 文件里:
20220922175444

涉及到的系统合约:

可在bsc-relayer里的const.go中看到:

executor/const.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var (
prefixForCrossChainPackageKey = []byte{0x00}
prefixForSequenceKey = []byte{0xf0}

PureHeaderSyncChannelID relayercommon.CrossChainChannelID = -1
// 只涉及以下4个合约
// TendermintLightClient.sol负责执行 bsc-relayer 发送过来的“同步块头”的交易,和 tmHeaderValidate 预编译合约一同工作:
tendermintLightClientContractAddr = common.HexToAddress("0x0000000000000000000000000000000000001003")
// 中继者奖励合约
relayerIncentivizeContractAddr = common.HexToAddress("0x0000000000000000000000000000000000001005")
// RelayerHub 管理 bsc-relayer 的权限。想要运行 bsc-relayer 的人必须调用合约来存入一些 BNB 以获得授权。
relayerHubContractAddr = common.HexToAddress("0x0000000000000000000000000000000000001006")
// CrossChain 跨链包预处理,通过 emit 事件发送跨链包到 BC。包预处理包括序列验证和默克尔证明验证
crossChainContractAddr = common.HexToAddress("0x0000000000000000000000000000000000002000")
)

只涉及到4个系统合约:

sequence ContractName ContractNameCN Role
1 TendermintLightClient Contract Tendermint轻客户端合约 负责执行 bsc-relayer中继器发送过来的“同步块头”的交易
2 RelayerIncentivize Contract 中继器奖励合约 用于 bsc-relayer claim reward。而 bsc-validator 的 reward 为打包出块的tx的 gas
3 RelayerHub Contract 中继器管理合约 用于记录 bsc-relayer 的注册信息, RelayerHub 管理bsc-relayer的权限.想要运行bsc-relayer的人必须调用合约来存入一些 BNB 以获得授权。
4 CrossChain Contract 跨链包处理合约 负责执行bsc-relayer发送过来的“同步跨链数据包”的交易,进行跨链包预处理,通过emit事件发送跨链包到BC.包预处理包括序列验证和默克尔证明验证

现在先看TendermintLightClient.sol轻客户端的源码及其实现

轻客户端的实现:

官方文档里有介绍。BSC用智能合约的形式,实现了一个轻客户端。

跨链互操作性的目的是使一个区块链能够充当另一个区块链的轻客户端。

由于信标链使用经典的拜占庭容错共识算法,轻客户端验证既便宜又容易:我们所要做的就是检查最新区块上的验证者签名,并验证状态的默克尔证明

在 Tendermint 中,验证者在处理区块之前就区块达成一致。
这意味着该块的签名和状态根直到下一个块才包含在内。因此,每个块都包含一个名为 LastCommit 的字段,其中包含负责提交前一个块的投票,以及块头中的一个名为 AppHash 的字段,它指的是应用程序处理前一个块的交易后的 Merkle 根哈希
因此,如果我们想从高度 H验证AppHash,我们需要来自 LastCommit高度 H+1的签名。(请记住,此 AppHash 仅包含直到并包括块 H-1 的所有交易的结果

与工作量证明不同,轻客户端协议不需要下载和检查区块链中的所有标头 - 客户端始终可以直接跳转到可用的最新标头,只要验证器集没有太大变化。如果验证者集发生变化,客户端需要跟踪这些变化,这需要为每个发生重大变化的块下载标头。在这里,我们将假设验证器集是恒定的,并推迟处理验证器集更改。

以太坊平台支持 golang 实现的无状态预编译合约solidity实现的普通合约
与普通合约相比,预编译合约效率更高,gas 成本更低,但它们是无状态的。但是,链上轻客户端必须是有状态的。所以这里我们将尝试一种混合方式:

  • 预编译实现合约(无状态计算,如签名验证)

  • 普通合约(存储验证器集和可信appHash)

20220922180103

基于solidity的智能合约,我们比较熟悉,所以接下来我们先了解一下预编译合约。

预编译合约:

介绍:

预编译合约是 EVM 中用于提供更复杂库函数(通常用于加密、散列等复杂操作)的一种折衷方法,这些函数不适合编写操作码。
它们适用于简单但经常调用的合约,或逻辑上固定但计算量很大的合约。
预编译合约是在使用节点客户端(如geth)代码实现的,因为它们不需要 EVM,所以运行速度很快。 与使用直接在 EVM 中运行的函数相比,它对开发人员来说成本也更低。

简言之,预编译合约特点如下:

  • 常用于加密、散列、验证、数据计算等操作

  • 不运行在evm中,在geth客户端里实现

  • 无状态、运行速度快、成本低

BSC 中的预编译合约定义如下:

core/vm/contracts.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// PrecompiledContractsIstanbul contains the default set of pre-compiled Ethereum
// contracts used in the Istanbul release.
var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
common.BytesToAddress([]byte{4}): &dataCopy{},
common.BytesToAddress([]byte{5}): &bigModExp{},
common.BytesToAddress([]byte{6}): &bn256AddIstanbul{},
common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{},
common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{},
common.BytesToAddress([]byte{9}): &blake2F{},
// Bsc新增的
common.BytesToAddress([]byte{100}): &tmHeaderValidate{},
common.BytesToAddress([]byte{101}): &iavlMerkleProofValidate{},
}

前面的都是在Etheruem中已经实现的预编译合约了,BSC 新增了两个:

  • tmHeaderValidate预编译合约: 主要用于验证 BC 块头是否合法,被 TendermintLightClient 系统合约调用

  • iavlMerkleProofValidate预编译合约: 主要用于验证 BC 跨链数据包是否合法,被 CrossChainContract 系统合约调用

接下来主要介绍预编译合约tmHeaderValidate和系统合约TendermintLightClient

系统合约TendermintLightClient:

数据结构:

TendermintLightClient.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
contract TendermintLightClient is ILightClient, System, IParamSubscriber {

struct ConsensusState {
uint64 preValidatorSetChangeHeight; // 上一个验证者集合变更的高度
bytes32 appHash; // BC的Merkle根哈希
// 对应bc的header.Block.Header.AppHash
bytes32 curValidatorSetHash; // 对应bc的header.Block.Header.ValidatorsHash
bytes nextValidatorSet;
}

mapping(uint64 => ConsensusState) public lightClientConsensusStates; // 区块高度height=>共识状态数据
mapping(uint64 => address payable) public submitters; // 区块高度height=>提交者地址
uint64 public initialHeight; // 初始化高度
uint64 public latestHeight;
bytes32 public chainID; // 链标识符
// 初始化共识状态
bytes constant public INIT_CONSENSUS_STATE_BYTES = hex"42696e616e63652d436861696e2d4e696c650000000000000000000000000000000000000000000229eca254b3859bffefaf85f4c95da9fbd26527766b784272789c30ec56b380b6eb96442aaab207bc59978ba3dd477690f5c5872334fc39e627723daa97e441e88ba4515150ec3182bc82593df36f8abb25a619187fcfab7e552b94e64ed2deed000000e8d4a51000";
uint256 constant public INIT_REWARD_FOR_VALIDATOR_SER_CHANGE = 1e16; // 应该是0.01BNB
uint256 public rewardForValidatorSetChange;

event initConsensusState(uint64 initHeight, bytes32 appHash);
event syncConsensusState(uint64 height, uint64 preValidatorSetChangeHeight, bytes32 appHash, bool validatorChanged);
event paramChange(string key, bytes value);

/* solium-disable-next-line */
constructor() public {}

....
}

需要关注的是 INIT_CONSENSUS_STATE_BYTES 初始化共识状态变量,该变量可在创始系统合约仓库bsc-genesis-contract中的generate-tendermintlightclient.js中设置,这个值要设置得对,后面的区块头才能顺利通过验证并同步。

INIT_CONSENSUS_STATE_BYTES这变量上消耗了很多时间

初始化函数:

TendermintLightClient.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 初始化
function init() external onlyNotInit {
uint256 pointer;
uint256 length;
(pointer, length) = Memory.fromBytes(INIT_CONSENSUS_STATE_BYTES);

/* solium-disable-next-line */
// sstore: writes a (u)int256 to storage
// mload:reads a (u)int256 from memory
// 从pointer位置内存中读取数据,再写入到合约的storage存储中
assembly {
sstore(chainID_slot, mload(pointer))
}

ConsensusState memory cs;
uint64 height;
(cs, height) = decodeConsensusState(pointer, length, false);
cs.preValidatorSetChangeHeight = 0;
lightClientConsensusStates[height] = cs;

initialHeight = height;
latestHeight = height;
alreadyInit = true;
rewardForValidatorSetChange = INIT_REWARD_FOR_VALIDATOR_SER_CHANGE;

emit initConsensusState(initialHeight, cs.appHash);
}

初始化函数中主要是将初始化共识状态变量INIT_CONSENSUS_STATE_BYTES中的值通过内联汇编的方式写入storage中,该函数以及后面的函数会大量操作指针变量uint256 pointer和长度变量uint256 length来实现存储/读取数据的操作。

同步区块头函数:

TendermintLightClient.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 同步区块头
// bsc-relayer会调用该方法
function syncTendermintHeader(bytes calldata header, uint64 height) external onlyRelayer returns (bool) {
require(submitters[height] == address(0x0), "can't sync duplicated header"); // 不能同步重复的区块头
require(height > initialHeight, "can't sync header before initialHeight"); // 不能同步初始高度之前的区块头,当前同步的区块高度必须大于初始高度

uint64 preValidatorSetChangeHeight = latestHeight; // 上一个验证人集合变更的高度 = 当前最新高度
ConsensusState memory cs = lightClientConsensusStates[preValidatorSetChangeHeight];
for (; preValidatorSetChangeHeight >= height && preValidatorSetChangeHeight >= initialHeight;) {
preValidatorSetChangeHeight = cs.preValidatorSetChangeHeight;
cs = lightClientConsensusStates[preValidatorSetChangeHeight]; // cs存储的是上一次的共识状态数据
}
if (cs.nextValidatorSet.length == 0) {
preValidatorSetChangeHeight = cs.preValidatorSetChangeHeight;
cs.nextValidatorSet = lightClientConsensusStates[preValidatorSetChangeHeight].nextValidatorSet;
require(cs.nextValidatorSet.length != 0, "failed to load validator set data");
}

//32 + 32 + 8 + 32 + 32 + cs.nextValidatorSet.length;
uint256 length = 136 + cs.nextValidatorSet.length;
bytes memory input = new bytes(length + header.length);
uint256 ptr = Memory.dataPtr(input); // Returns a memory pointer to the data portion of the provided bytes array.
require(encodeConsensusState(cs, preValidatorSetChangeHeight, ptr, length), "failed to serialize consensus state");

// write header to input
uint256 src;
ptr = ptr + length;
(src, length) = Memory.fromBytes(header);
Memory.copy(src, ptr, length); // Copy 'len' bytes from memory address 'src', to address 'dest'.

length = input.length + 32;
// Maximum validator quantity is 99
bytes32[128] memory result;
/* solium-disable-next-line */
assembly {
// call validateTendermintHeader precompile contract
// 调用预编译合约中的方法
// Contract address: 0x64
// if iszero(call(gasLimit, contractAddress, value, input, inputLength, output, outputLength)) {
if iszero(staticcall(not(0), 0x64, input, length, result, 4096)) {
revert(0, 0)
}
}

// Judge if the validator set is changed
/* solium-disable-next-line */
assembly {
length := mload(add(result, 0))
}
// 验证者集合是否发生变化的标志
bool validatorChanged = false;
if ((length & (0x01 << 248)) != 0x00) {
validatorChanged = true;
// 系统奖励合约的提取奖励函数
ISystemReward(SYSTEM_REWARD_ADDR).claimRewards(msg.sender, rewardForValidatorSetChange);
}
length = length & 0xffffffffffffffff;

/* solium-disable-next-line */
assembly {
ptr := add(result, 32)
}

uint64 actualHeaderHeight;
// 解码共识状态
// ptr 指针位置、length长度、validatorChanged是否验证者集合发生变化
(cs, actualHeaderHeight) = decodeConsensusState(ptr, length, !validatorChanged);
require(actualHeaderHeight == height, "header height doesn't equal to the specified height"); // 区块头高度必须等于指定的高度

submitters[height] = msg.sender;
cs.preValidatorSetChangeHeight = preValidatorSetChangeHeight;
lightClientConsensusStates[height] = cs;
if (height > latestHeight) {
latestHeight = height;
}

emit syncConsensusState(height, preValidatorSetChangeHeight, cs.appHash, validatorChanged);

return true;
}

该函数需要关注的是 调用了预编译合约tmHeaderValidate进行区块头合法性的验证

1
2
3
4
5
6
7
8
9
assembly {
// call validateTendermintHeader precompile contract
// 调用预编译合约中的方法
// Contract address: 0x64
// if iszero(call(gasLimit, contractAddress, value, input, inputLength, output, outputLength)) {
if iszero(staticcall(not(0), 0x64, input, length, result, 4096)) {
revert(0, 0)
}
}

调用关系如下:
20220923112409

一开始就卡在调用预编译合约这一步,验证一直没法通过。

预编译合约tmHeaderValidate:

先在geth中找到预编译合约的代码文件: core/vm/contracts_lightclient.go ,通过测试文件core/vm/contracts_lightclient_test.go可以帮助我们测试、快速了解。

测试代码:

core/vm/contracts_lightclient_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func TestTmHeaderValidateAndMerkleProofValidate(t *testing.T) {
// 初始共识状态
consensusStateBytes, err := hex.DecodeString("...")
require.NoError(t, err)
// 解码
cs, err := lightclient.DecodeConsensusState(consensusStateBytes)
require.NoError(t, err)
// 新区块头的数据
headerBytes, err := hex.DecodeString("....")
require.NoError(t, err)

parameterInput := make([]byte, 32+len(consensusStateBytes)+len(headerBytes))
binary.BigEndian.PutUint64(parameterInput[24:32], uint64(len(consensusStateBytes)))
copy(parameterInput[32:32+len(consensusStateBytes)], consensusStateBytes)
copy(parameterInput[32+len(consensusStateBytes):], headerBytes)

totalLengthPrefix := make([]byte, 32)
binary.BigEndian.PutUint64(totalLengthPrefix[0:8], 0)
binary.BigEndian.PutUint64(totalLengthPrefix[8:16], 0)
binary.BigEndian.PutUint64(totalLengthPrefix[16:24], 0)
binary.BigEndian.PutUint64(totalLengthPrefix[24:], uint64(len(parameterInput)))
input := append(totalLengthPrefix, parameterInput...)

var tmHeaderValidateContract tmHeaderValidate
// 区块头验证
// 区块头里的共识状态
syncedConsensusStateBytes, err := tmHeaderValidateContract.Run(input) // 验证
require.NoError(t, err)
syncedConsensusState, err := lightclient.DecodeConsensusState(syncedConsensusStateBytes[32:])
require.NoError(t, err)
// 连续区块的情况
require.Equal(t, testHeight+1, syncedConsensusState.Height)
require.Equal(t, cs.ChainID, syncedConsensusState.ChainID) // chainId相等
....
}

共识状态数据结构:

core/vm/lightclient/types.go
1
2
3
4
5
6
7
8
9
consensusState := ConsensusState{
ChainID: chainID,
Height: height,
AppHash: appHash,
CurValidatorSetHash: curValidatorSetHash,
NextValidatorSet: &tmtypes.ValidatorSet{
Validators: validatorSet,
},
}

先看测试代码前面的部分,主要步骤如下:

  • 十六进制的初始化的共识状态字节数组字符串,解码成共识状态数据结构体cs

  • 十六进制的要同步的区块头数据,解码成共识状态数据结构体newCs

  • 两个数据结构进行比较、验证

轻客户端本身保存着当前的活跃验证者集合,当收到新的区块后,

  1. 首先对新区块头做基本的检查: 例如chainId是否相等

  2. 判断新区块头是否获得了3分之2的投票
    区块头中的验证者集合与轻客户端本身存储的验证者集合做比较,若相同,则通过+2/3的投票,通过验证,若不同,更新轻客户端的活跃验证者集合,再次验证;

  3. 当轻客户端的安全性不足时(如高度不连续、bft超过1/3等等),与全节点进行交互验证。

上面部分摘自《区块链架构与实现-cosmos详解-9.1.1轻客户端原理概述》

共识状态数据结构:

先把解码后的数据打印出来观察一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
=== RUN   TestTmHeaderValidateAndMerkleProofValidateMy
[*****] tmHeaderValidate Run Func
[*****] Step 1
[*****] Step 2
[*****] cs: &{715 2 [41 236 162 84 179 133 155 255 239 175 133 244 201 93 169 251 210 101 39 118 107 120 66 114 120 156 48 236 86 179 128 182] [235 150 68 42 170 178 7 188 89 151 139 163 221 71 118 144 245 197 135 35 52 252 57 230 39 114 61 170 151 228 65 232] ValidatorSet{
Proposer: Validator{35B42F545A2E3575E48A95B47C9E9540074756BD PubKeyEd25519{8BA4515150EC3182BC82593DF36F8ABB25A619187FCFAB7E552B94E64ED2DEED} VP:1000000000000 A:0}
Validators:
Validator{35B42F545A2E3575E48A95B47C9E9540074756BD PubKeyEd25519{8BA4515150EC3182BC82593DF36F8ABB25A619187FCFAB7E552B94E64ED2DEED} VP:1000000000000 A:0}
}}
[*****] header: SignedHeader{
Header{
Version: {10 0}
ChainID: 715
Height: 5335
Time: 2022-09-20 10:00:41.504366656 +0000 UTC
NumTxs: 0
TotalTxs: 0
LastBlockID: 0EE58A3014F929D14E9A82AB04CDBF2097B1915A0CF8C23EB8143B900777357F:1:A84AF4EA95C5
LastCommit: B6D621C0189F2DFD4767E85C57B61813804DECF7A32F08E73666F53616E5FDDF
Data:
Validators: C506C171480200F8EDE028065D07D02CA8DA35C94463326A6E31FBD6CF82388C
NextValidators: C506C171480200F8EDE028065D07D02CA8DA35C94463326A6E31FBD6CF82388C
App: 0E067C5259944D79F6110455B387C9EF941C9D10A8552B5ACDD3DEF69868DE25
Consensus: 294D8FBD0B94B767A7EBA9840F299A3586DA7FE6B5DEAD3B7EECBA193C400F93
Results:
Evidence:
Proposer: 931B028E3A2B6C045D0E4579470E9E50847029A2
}#20404785119D52196F6BA954C57AF0F0FF19E680921A7F7E8A4A8CD485195972
Commit{
BlockID: 20404785119D52196F6BA954C57AF0F0FF19E680921A7F7E8A4A8CD485195972:1:6B3354315C9D
Precommits:
Vote{0:931B028E3A2B 5335/00/2(Precommit) 20404785119D 654C58D09A92 @ 2022-09-20T10:00:42.512185556Z}
}
}

基本检查:

先对新区块头的数据进行基本检查,例如chainId是否相等。
一开始就是卡在这一步,generate-tendermintlightclient.js中默认的INIT_CONSENSUS_STATE_BYTES里的chainIdBinance-Chain-Nile,而我同步过来新区块头里的chainId为715

core/vm/contracts_lightclient_encode_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func TestTmHeaderValidateAndMerkleProofValidateTemp(t *testing.T) {
// 初始共识状态
// TendermintLightClient.sol默认的INIT_CONSENSUS_STATE_BYTES里的chainId为Binance-Chain-Nile
consensusStateBytesStr := "3731350000000000000000000000000000000000000000000000000000000000000000000000000229eca254b3859bffefaf85f4c95da9fbd26527766b784272789c30ec56b380b6eb96442aaab207bc59978ba3dd477690f5c5872334fc39e627723daa97e441e88ba4515150ec3182bc82593df36f8abb25a619187fcfab7e552b94e64ed2deed000000e8d4a51000" // 十六进制字符串
consensusStateBytes, err := hex.DecodeString(consensusStateBytesStr)
require.NoError(t, err)

cs, err := lightclient.DecodeConsensusState(consensusStateBytes)
require.NoError(t, err)

fmt.Printf("cs: %v \n", cs)
// TODO:生成chainId可自定义的INIT_CONSENSUS_STATE_BYTES
cs.ChainID = "715" // 修改链id
cs.Height = 2 // 修改初始高度

// TODO:修改验证者集合ValidatorSet和Validators
consensusStateBytes, err = cs.EncodeConsensusState()
// 需要转成十六进制字符串
newConsensusStateBytesStr := hex.EncodeToString(consensusStateBytes)
fmt.Println("consensusStateBytesHexStr: ", newConsensusStateBytesStr)
newConsensusStateBytes, err := hex.DecodeString(newConsensusStateBytesStr)
newcs, err := lightclient.DecodeConsensusState(newConsensusStateBytes)
require.NoError(t, err)
fmt.Printf("newcs: %v \n", newcs)
}

我尝试去修改其中的链id,再编码回去,chainId校验可以通过,但会出现新的报错:投票权重不足

1
2
Error: Received unexpected error:
Invalid commit -- insufficient old voting power: got 0, needed 666666666667

投票权重检查:

接着上面的报错,同步过来的新区块的提案者地址为

1
Proposer:  931B028E3A2B6C045D0E4579470E9E50847029A2

但初始的共识状态里的验证者集合没有该地址,所以投票权重检查不通过

1
2
3
Validators:
Validator{35B42F545A2E3575E48A95B47C9E9540074756BD PubKeyEd25519{8BA4515150EC3182BC82593DF36F8ABB25A619187FCFAB7E552B94E64ED2DEED} VP:1000000000000 A:0}
}

至此,我们有两个思路:

  1. 基于generate-tendermintlightclient.js中默认的INIT_CONSENSUS_STATE_BYTES,我们去魔改/生成一个对的初始共识状态十六进制字节数组字符串

  2. 直接获取bc上某高度个共识状态数据,再编码成十六进制字节数组字符串

思路1:

比较棘手的是PubKeyEd25519公钥数据,没法通过BC的cli、rpc api直接获取。

1
2
3
Validators:
Validator{35B42F545A2E3575E48A95B47C9E9540074756BD PubKeyEd25519{8BA4515150EC3182BC82593DF36F8ABB25A619187FCFAB7E552B94E64ED2DEED} VP:1000000000000 A:0}
}

参考文章COSMOS ED25519简析,我们可以在bnc-tendermint中调用函数,自己生成。打开BC的配置文件/config/priv_validator_key.json

/config/priv_validator_key.json
1
2
3
4
5
6
7
8
9
10
11
{
"address": "36231A72BBC68CBD82D0EEC690EE8540B0B049AF",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "AXtsaPfWGEp+oDrq/PKMEvTImmb4HS87juIdVykoSNg="
},
"priv_key": {
"type": "tendermint/PrivKeyEd25519",
"value": "cijqz2BN0KPc1qETWND0Dr7Zk4g1TYTV+MH6S04YuRcBe2xo99YYSn6gOur88owS9MiaZvgdLzuO4h1XKShI2A=="
}
}

修改并运行测试脚本:

privval/file_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func TestUnmarshalValidatorKeyTemp(t *testing.T) {
//assert, require := assert.New(t), require.New(t)

serialized := fmt.Sprintf(`{
"address": "931B028E3A2B6C045D0E4579470E9E50847029A2",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "+dC/2i1cfw/ZcHZvsxt8dDXxbl1EnClGeckR4o4vUUo="
},
"priv_key": {
"type": "tendermint/PrivKeyEd25519",
"value": "B8z+AJ9+wtAoeMWShtlZ1BVewYp9UtD+LRMXoE8ITGX50L/aLVx/D9lwdm+zG3x0NfFuXUScKUZ5yRHiji9RSg=="
}
}`)
fmt.Println(serialized)
val := FilePVKey{}
err := cdc.UnmarshalJSON([]byte(serialized), &val)
//require.Nil(err, "%+v", err)
fmt.Println(err)

// make sure the values match
//assert.EqualValues(addr, val.Address)
//assert.EqualValues(pubKey, val.PubKey)
//assert.EqualValues(privKey, val.PrivKey)
fmt.Println(val.Address)
fmt.Println((val.PubKey.(ed25519.PubKeyEd25519)).String())
}

输出测试结果,成功打印出PubKeyEd25519字符串

1
2
3
4
=== RUN   TestUnmarshalValidatorKeyTemp
PubKeyEd25519{F9D0BFDA2D5C7F0FD970766FB31B7C7435F16E5D449C294679C911E28E2F514A}
--- PASS: TestUnmarshalValidatorKeyTemp (0.00s)
PASS

思路1要改的内容太多,容易出错,应采取思路2的更为直接、准确。

思路2:

直接获取BC上某高度上共识状态数据,再编码成十六进制字节数组字符串

币安是先有BC链,再有BSC链,它们主网在TendermintLightClient.sol轻客户端合约里的初始高度设置成一个很大的值11018685520220923143424
可猜测:

  • BC链是先于BSC链,持续运行着

  • 选取了一个BC链上一个特定的高度里的共识状态数据作为轻客户端合约里的初始共识状态数据

  • 然后运行起BSC链、bsc-relayer

  • 中继器连接两条链,读取到BC特定高度的数据后,会同步BC后面的共识状态到轻客户端合约里

bsc-relayer中新增测试脚本:

executor/blockinfo_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 查询区块信息
func TestGetBlockInfo(t *testing.T) {
initFlags()
var height int64
height = 50
bbcNetworkType := viper.GetInt(flagBBCNetworkType)
bbcExecutor, _ := NewBBCExecutor(cfg, types.ChainNetwork(bbcNetworkType))
// 关键函数
cs, err := bbcExecutor.GetInitConsensusState(height)
if err != nil {
fmt.Println(err)
}
common.Logger.Infof("cs: %v\n", cs)
fmt.Printf("cs: %v \n", cs)
consensusStateBytes, err := cs.EncodeConsensusState()
// 需要转成十六进制字符串
newConsensusStateBytesStr := hex.EncodeToString(consensusStateBytes)
fmt.Println("consensusStateBytesHexStr: ", newConsensusStateBytesStr)
}

运行测试脚本:

1
2
cd /bsc-relayer/executor
go test -v

主要是使用了bsc-relayer中的bbcExecutor.GetInitConsensusState(height)函数,成功获取BC上高度为50的共识状态数据,并编码成十六进制字符串。

1
2
3
consensusStateBytesHexStr:  373135000000000000000000000000000000000000000000000000000000000000000000000000323cebfa274480fa027136f6f9049aa0b6a92b4b3843ac5bce32d2d19af15d7a6a6e42927ceb8681e615b60bc77cb1ff3449b93bc9017b6c68f7d6184a7ea03aeafcf28c12f4c89a66f81d2f3b8ea51000
--- PASS: TestGetBlockInfo (0.00s)
PASS

至此,我们就获得了一个准确的初始化共识状态数据,可作为TendermintLightClient.sol轻客户端合约的初始化共识状态数据。

现在我们需要将BC、BSC、bsc-relayer三者结合起来完整验证一下,验证bsc-relayer 是否能够同步新区块头的到TendermintLightClient.sol轻客户端合约里,并成功修改到轻客户端合约里的数据。

完整验证:

运行BC链:

修改一下BC链配置文件里的同步区块间隔,原本是一天一次,现在要调整得快一些

1
2
3
[base]
# Interval blocks of breathe block, if breatheBlockInterval is 0, breathe block will be created every day.
breatheBlockInterval = 100

然后初始化并运行起BC链

获取初始化共识状态数据

使用bsc-relayer上的测试脚本获取某特定高度的初始化共识状态数据的十六进制字节数组字符串

executor/blockinfo_test.go
1
2
3
4
5
=== RUN   TestGetBlockInfo
consensusStateBytesHexStr: 373135000000000000000000000000000000000000000000000000000000000000000000000000323cebfa274480fa027136f6f9049aa0b6a92b4b3843ac5bce32d2d19af15d7a6a6e42927ceb8681e615b60bc77cb1ff3449b93bc9017b6c68f7d6184a7ea03aeafcf28c12f4c89a66f81d2f3b8ea51000
--- PASS: TestGetBlockInfo (0.00s)
PASS
ok github.com/binance-chain/bsc-relayer/executor 0.080s

重新生成BSC的genesis.json:

在创始系统合约仓库bsc-genesis-contract中的generate-tendermintlightclient.js中设置initConsensusStateBytes

generate-tendermintlightclient.js
1
2
3
program.option("--initConsensusStateBytes <initConsensusStateBytes>",
"init consensusState bytes, hex encoding, no prefix with 0x",
"373135000000000000000000000000000000000000000000000000000000000000000000000000323cebfa274480fa027136f6f7986147b96aa4adb7c421b909049aa0b6a92b4b3843ac5bce32d2d19af15d7a6a6e42927ceb8681e615b60bc77cb1ff3449b93bc9017b6c68f7d6184a7ea03aeafcf28c12f4c89a66f81d2f3b8ee21d57292848d8000000e8d4a51000");

生成genesis.json,初始化并运行起BSC链

运行bsc-relayer:

初始化并运行起bsc-relayer中继器

查看结果:

用本地搭建的blockscout区块链浏览器查看结果
20220923145003
bsc-relayer中继器发起到TendermintLightClient.sol轻客户端合约的tx交易成功
Remix IDE读取TendermintLightClient.sol轻客户端合约的变量,修改成功。
20220923145414

跨链同步主要涉及两个系统合约,分别为

  • TendermintLightClient.sol: 同步块头的操作时会调用到
  • CrossChain.sol: 同步跨链数据包的操作会调用到

至此,我们就搞定了TendermintLightClient.sol轻客户端合约中区块头的同步问题,后面继续研究同步跨链数据包的问题。

小结:

涉及到的github代码仓库较多,简单梳理一下:
20220923151740

项目 repositories github地址
BC node https://github.com/bnb-chain/node
BC bnc-cosmos-sdk https://github.com/bnb-chain/bnc-cosmos-sdk
BC bnc-tendermint https://github.com/bnb-chain/bnc-tendermint
BSC bsc https://github.com/bnb-chain/bsc
BSC bsc-genesis-contract https://github.com/bnb-chain/bsc-genesis-contract
Relayer bsc-relayer https://github.com/bnb-chain/bsc-relayer
Relayer oracle-relayer https://github.com/bnb-chain/oracle-relayer

Refs: