Provenance Blockchain团队在Cosmos SDK升级后发现测试网存在奇怪行为,节点从旧数据快照或创世区块初始化时无法同步,出现应用哈希不匹配错误。经调查发现是authz.MsgGrant
中的time.Now()
调用导致验证时的非确定性问题,可能导致验证器网络暂停。该团队已与Cosmos SDK团队沟通,并修复了此问题。
区块链的核心原则之一是能够从创世区块开始,以无需信任的方式验证状态机,从而获得与网络本身相同的结果。阅读一下当我们的团队在公共测试网上发现奇怪行为时发生了什么。
区块链技术的核心原则之一是能够从创世区块开始,以无需信任的方式验证状态机,从而获得与网络本身相同的结果。为此,交易必须是确定性的。任何阻止状态干净重放的东西都会打破这一规则,从而破坏对数据和网络正确性的信任。区块链的设计考虑到了这一点,如果网络上对某个区块的状态存在过多的分歧,则会发布停止指令。通过罚没、惩罚和重新加入所需的费用,网络会监督验证者以确保正确性。
最近,在 Cosmos SDK 升级后,Provenance Blockchain 团队开始在我们的公共测试网上注意到奇怪的行为。任何使用当前数据快照上线的节点,以及验证器网络,都在按预期向前推进。但是,从较旧的数据快照启动或从创世区块初始化的节点都无法同步,并出现 app hash 不匹配错误。当当抢跑的节点为当前区块和交易计算出的哈希值与验证器网络中达成一致的哈希值不同时,Tendermint 状态机中就会发生 App hash 不匹配。更多时候,当我们遇到用户或组织在错误的高度运行错误版本的 Provenance Blockchain 节点二进制文件时,我们会遇到此错误。但是,在这种情况下,节点运行的版本与测试网验证器完全相同。应该没有理由出现 app hash 不匹配错误,但多次尝试从不同的数据备份重新启动节点后,问题仍然存在。在我的本地测试网节点上,这种情况一直发生在区块 3639413。因此,挖掘开始了。
第一步是从测试网获取良好的区块数据。使用我们值得信赖的 Provenance Blockchain Explorer,我能够提取网络在该高度成功提交的数据。在该区块中,只有一种消息类型,即 MarkerTransferAuthorization
的 authz 授权:
// 为了简洁起见,删除了标准签名和头部数据。
// -- snip --
{
"authorization": {
"@type": "/provenance.marker.v1.MarkerTransferAuthorization",
"transferLimit": [\
{\
"denom": "figure-v2.psa.stock",\
"amount": "333"\
}\
]
},
"expiration": "2021-10-05T20:45:47.233020Z"
}
// -- snip --
MarkerTransferAuthorization
消息位于 Provenance Blockchain Marker 模块 中。审查处理程序和 keeper 没有发现任何特别引人注目的内容,如下所示。
func (k Keeper) authzHandler(ctx sdk.Context, admin sdk.AccAddress, from sdk.AccAddress, amount sdk.Coin) error {
markerAuth := types.MarkerTransferAuthorization{}
authorization, expireTime := k.authzKeeper.GetCleanAuthorization(ctx, admin, from, markerAuth.MsgTypeURL())
if authorization == nil {
return fmt.Errorf("%s account has not been granted authority to withdraw from %s account", admin, from)
}
accept, err := authorization.Accept(ctx, &types.MsgTransferRequest{Amount: amount})
if err != nil {
return err
}
if accept.Accept {
limitLeft, _ := authorization.(*types.MarkerTransferAuthorization).DecreaseTransferLimit(amount)
if limitLeft.IsZero() {
return k.authzKeeper.DeleteGrant(ctx, admin, from, markerAuth.MsgTypeURL())
}
return k.authzKeeper.SaveGrant(ctx, admin, from, &types.MarkerTransferAuthorization{TransferLimit: limitLeft}, expireTime)
}
return fmt.Errorf("authorization was not accepted for %s", admin)
}
一切看起来都像是标准的日常 authz keeper 调用。注意到的一小件事是 GetCleanAuthorization
会在过期的授权上发出删除操作(为了主动删除授权列表),并且 marker 模块在用完限制时也会这样做;但是在区块链交易的范围内,这两种行为都是完全可以接受的。因此,我的视线转向了受影响模块(bank、authz、auth、marker)的 IAVL 存储。
IAVL 树是一个版本化的不可变 AVL 树,在 Cosmos SDK 中用于存储状态和应用程序数据,随着链的推进。在交易执行期间,每个模块都可以使用 keeper 写入他们对树的视图,以存储数据。有关 keeper 的更多信息,请参见 Cosmos SDK 文档,位置在此。
Cosmos SDK 团队的好人们提供了一个名为 iaviewer 的便捷工具,用于检查状态机数据存储和当前的树形结构。使用此工具,我们可以窥视网络上任何给定区块高度的状态,前提是我们拥有节点的数据目录。使用来自我们其中一个同步 sentinel 节点的已知良好数据目录转储,以及来自我停止的本地测试网节点的错误数据目录转储,我开始挖掘这两棵树。我编写了一个小型 bash 脚本来快速生成导出的差异,以比较好状态和坏状态。
##!/usr/bin/env bashout=./diffs[ ! -d "$out" ] && mkdir -p "$out"block=3639413for db in application state; do
for typ in shape data; do
for module in authz auth bank marker; do
echo "Processing db $db for key k:$module for $typ"
outfile="$out/$typ-$module-$db"
goodDb="chains/goodnode/data/$db.db"
badDb="chains/badnode/data/$db.db" iaviewer $typ $goodDb s/k:$module/ $block > $outfile.good
if [ "$?" != "0" ]; then
echo "Failed to run iaviewer $typ $goodDb $module"
fi iaviewer $typ $badDb s/k:$module/ $block > $outfile.bad
if [ "$?" != "0" ]; then
echo "Failed to run iaviewer $typ $badDb $module"
fi diff -u $outfile.good $outfile.bad > $outfile.diff
done
done
done
它不多,但完成了工作。从那里,我使用 find ./diffs/ -not -empty -name \*.diff -ls
验证了所有发生在良好节点上但没有发生在不良节点上的更改,以列出非空差异文件并忽略其余的。此区块仅显示了 authz 和 bank 两个模块在节点之间存在不同的数据。
521905 4 -rw-rw-r-- 1 phil phil 369 Sep 28 21:24 diffs/shape-authz-application.diff
527480 952 -rw-rw-r-- 1 phil phil 971838 Sep 28 21:24 diffs/data-bank-application.diff
525838 1264 -rw-rw-r-- 1 phil phil 1291038 Sep 28 21:24 diffs/shape-bank-application.diff
522956 4 -rw-rw-r-- 1 phil phil 646 Sep 28 21:24 diffs/data-authz-application.diff
每个包含交易的区块都应该在 bank 模块中有所变动,因为每个交易都需要支付 gas,从而奖励给验证者。但是,由于重点更多地放在 auth 和 authz 模块问题上,因此审查了 authz 模块的差异。
由于 IAVL 树在其核心是重新平衡的二叉树,因此每次添加新节点时,形状都应该发生变化。我使用良好节点和不良节点在此区块上的形状之间的差异验证了这一点。
$ cat diffs/shape-authz-application.diff
--- ./diffs/shape-authz-application.good 2021-09-28 21:24:46.733497072 +0000
+++ ./diffs/shape-authz-application.bad 2021-09-28 21:24:47.129527545 +0000
@@ -1,2 +1,2 @@
Got version: 3639413
-*0 011464F28E2E365D68C47A693E010D2AB28F7B75C1E7146A3C0C5392B47E08F0B5E50DB7C20C86BA018D462F70726F76656E616E63652E6D61726B65722E76312E4D73675472616E7366657252657175657374
+<nil>
此差异证明良好节点已成功将 authz 授权写入状态存储,而不良节点则没有。如果对十六进制值进行解码,则可以看到已写入节点 0 中树的上述 MarkerTransferAuthorization
的 protobuf 字节数据。
▷▷ echo "011464F28E2E365D68C47A693E010D2AB28F7B75C1E7146A3C0C5392B47E08F0B5E50DB7C20C86BA018D462F70726F76656E616E63652E6D61726B65722E76312E4D73675472616E7366657252657175657374" | unhex
*{uj<>
S
F/provenance.marker.v1.MsgTransferRequest%
良好节点的数据获得了 marker 转移授权(如从 Provenance blockchain Explorer 的交易中所见),而不良节点的数据没有此授权。同样,我们也可以从 authz 模块状态存储中的数据中验证差异。
$ cat diffs/data-authz-application.diff
--- ./diffs/data-authz-application.good 2021-09-28 21:24:51.789886168 +0000
+++ ./diffs/data-authz-application.bad 2021-09-28 21:24:52.185916644 +0000
@@ -1,6 +1,4 @@
Got version: 3639413
Printing all keys with hashed values (to detect diff)
- 011464F28E2E365D68C47A693E010D2AB28F7B75C1E7146A3C0C5392B47E08F0B5E50DB7C20C86BA018D462F70726F76656E616E63652E6D61726B65722E76312E4D73675472616E7366657252657175657374
- 61848FAB9772C1FEDCAB2E38B8E962553D427EA5B1CC960457D55E7F06AE2989
-Hash: 46D6028D29055581097F24BAB92F456B69B5765A3E4214247DF2A97192EA73D4
-Size: 1
+Hash: E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
+Size: 0
你可以在数据差异中看到与形状差异中相同的 MarkerTransferAuthorization
二进制 protobuf 数据,但是也可以在数据差异中看到这两个节点计算的总大小和哈希值。
我现在有证据表明 authz 授权不知何故存在于最新的节点中,但没有到达快速同步节点。一位同事,Danny Wedul,提出了我们尚未研究 checkTx
,而是专注于数据和我们自己的模块更改和更新。经过一番来回讨论后,我们都进入了 Cosmos SDK 消息验证的兔子洞。
在 Tendermint ABCI 世界中,传入区块的流程会通过 checkTx() 进行,以便在快速同步期间进行每次重放。在 Cosmos SDK 中,checkTx
然后迭代每个区块交易中的每条消息,并调用 Message.ValidateBasic()
。此验证期望所有消息都通过确定性方法和计算进行验证。authz.MsgGrant
如何打破这一点?
在 authz.MsgGrant 结构中,有一个 authz.Grant
字段。
// ValidateBasic implements Msg
func (msg MsgGrant) ValidateBasic() error {
granter, err := sdk.AccAddressFromBech32(msg.Granter)
if err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "invalid granter address")
}
grantee, err := sdk.AccAddressFromBech32(msg.Grantee)
if err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "invalid granter address")
} if granter.Equals(grantee) {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "granter and grantee cannot be same")
}
return msg.Grant.ValidateBasic()
}
在消息验证结束时,它会委派到 Grant
字段的验证。
func (g Grant) ValidateBasic() error {
if g.Expiration.Unix() < time.Now().Unix() {
return sdkerrors.Wrap(ErrInvalidExpirationTime, "Time can't be in the past")
} av := g.Authorization.GetCachedValue()
a, ok := av.(Authorization)
if !ok {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "expected %T, got %T", (Authorization)(nil), av)
}
return a.ValidateBasic()
}
在 Grant
字段的验证中,有一个对 time.Now() 的调用。像当前时钟时间这样不断变化的向量本质上是不确定的。
特定验证是 if g.Expiration.Unix() < time.Now().Unix() { ... }
检查。此断言对于最新的节点通过,因为 time.Now()
在整个网络进行区块铸造和验证时都在授权过期窗口内。但是,在将来重放状态数据时,此条件失败,因为时间已转移到将来,并且授权的过期时间现在存在于 Now()
之前。我们找到了不确定性的点。
那么,为什么这毕竟是不好的呢?我们不能继续简单地使用来自同步网络的数据目录转储吗?
不。如果攻击的时机正确,例如在验证者离线并在不同时间开始铸造和重放的升级期间,时间偏差和恶意制作的有效负载可能会导致验证者网络停止。另一个缺点是任何尝试从创世区块同步的节点都会失败,从而破坏区块链技术的一些主要原则(无需信任、可验证、独立)。
发现问题后,我们的团队谨慎地与 Cosmos SDK 团队联系,以沟通调查结果,等待他们的修复,并留出时间让其他网络升级以在公开披露之前缓解问题。
PHIL STORY
Phil 是一位来自蒙大拿州的软件工程师,致力于 Provenance Blockchain 生态系统。他目前领导 R&D 团队,在协议之上开发下一代概念和技术。在工作之余,他喜欢户外活动、游戏和与家人共度时光。
- 原文链接: medium.com/provenanceblo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!