Cosmos是一个互操作区块链的网络,允许开发者使用Cosmos SDK构建特定应用的区块链。文章详细介绍了Cosmos的架构、模块化设计、安全措施以及与其他区块链的通信方式,强调了其在应用开发和安全性方面的优势。
Cosmos,所谓的区块链互联网,究竟是什么?简单来说,它是一个互操作的区块链网络。Cosmos SDK 为开发者提供了一个框架,使他们能够轻松创建自己的应用专用链,并通过跨链通信(IBC)协议与其他链进行通信。
那么,我们为什么要创建自己的应用专用区块链呢?一个优势是性能。定制的区块链意味着我们不需要与其他应用竞争资源。基础区块链用于单一目的,而不是与其他一百万个应用共享。此外,交易不需要经过虚拟机的瓶颈进行解释。
另一个重要的优势是主权。与部署在通用区块链上的应用不同,我们的应用的治理不受基础区块链治理的限制。也就是说,只有一层治理。
应用专用区块链可以分为三个部分:网络层、共识层和应用层。
Tendermint Core 为我们处理前两层,传播交易并允许节点就应添加到区块链的交易达成一致。作为开发者,我们只需要实现应用层,其负责处理交易和更新状态。我们可以使用 Cosmos SDK,一个 Golang 框架,来构建这个层。
使用 Cosmos SDK 构建的应用由单个模块组成。模块可以被视为构成完整区块链应用的构建块。SDK 中的每个模块负责区块链应用的特定方面,例如管理区块链的状态或与外部系统交互。通过使用模块,开发者可以轻松重用现有功能,并专注于构建应用的独特特性。
Cosmos SDK 包括许多内置模块,如质押模块、治理模块和 IBC 模块,并且它还允许开发者创建自己的自定义模块。愿景是创建一个庞大的开源模块生态系统,使开发者能够快速轻松地创建复杂的应用。
然而,在没有访问控制机制的情况下,恶意模块的潜在风险会造成巨大的安全隐患。例如,开源模块可能会因为供应链攻击而变得恶意。为了减轻这一风险,Cosmos SDK 使用对象能力模型来强制模块之间的边界。模块只能获取引用该能力对象的能力。
每个模块可以被视为一个独立的状态机。一个模块可以使用一个或多个 KVStores
(键值存储)来维护其状态。存储密钥提供对存储的无限制读写访问。定义了一个 Keeper 对象来持有该密钥,并提供以受限方式与存储交互的方法。Keeper 被命名为守门人,因为它是存储的守卫。因此,storeKey
不应向应用中的其他模块暴露。
模块可以定义接口并扩展它们的 Keeper,以与外部模块交互。接口应仅公开模块操作所必需的方法。以下是质押模块的 Keeper 的示例,它扩展了银行和认证模块的 Keepers。
// x/staking 存储的 Keeper
type Keeper struct {
storeKey storetypes.StoreKey
cdc codec.BinaryCodec
authKeeper types.AccountKeeper
bankKeeper types.BankKeeper
hooks types.StakingHooks
authority string
}
银行模块定义了以下接口。
type Keeper interface {
SendKeeper
WithMintCoinsRestriction(MintingRestrictionFn) BaseKeeper
InitGenesis(sdk.Context, *types.GenesisState)
ExportGenesis(sdk.Context) *types.GenesisState
...
MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
DelegateCoins(ctx sdk.Context, delegatorAddr, moduleAccAddr sdk.AccAddress, amt sdk.Coins) error
UndelegateCoins(ctx sdk.Context, moduleAccAddr, delegatorAddr sdk.AccAddress, amt sdk.Coins) error
types.QueryServer
}
注意,BankKeeper
接口仅包含这些方法的一个子集,它们被暴露给质押模块。
type BankKeeper interface {
GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
LockedCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
GetSupply(ctx sdk.Context, denom string) sdk.Coin
SendCoinsFromModuleToModule(ctx sdk.Context, senderPool, recipientPool string, amt sdk.Coins) error
UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
DelegateCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
}
外部 keepers 随后在质押模块的构造函数中初始化为新的接口类型。
// NewKeeper 创建一个新的质押 Keeper 实例
func NewKeeper(
cdc codec.BinaryCodec,
key storetypes.StoreKey,
ak types.AccountKeeper,
bk types.BankKeeper,
authority string,
) *Keeper {
...
return &Keeper{
storeKey: key,
cdc: cdc,
authKeeper: ak,
bankKeeper: bk,
hooks: nil,
authority: authority,
}
}
BaseApp
类型实现了 Cosmos SDK 大多数核心功能。它主要由应用程序区块链接口(ABCI)实现组成,以与 Tendermint 进行交互、一个多存储以持久化状态以及一个消息服务路由器,用于将交易路由到适当的模块。开发人员通常将扩展 BaseApp
来在 Cosmos SDK 上创建应用。
type BaseApp struct {
// 初始化时创建
logger log.Logger
name string // 应用程序名称来自 abci.Info
db dbm.DB // 通用数据库后端
cms storetypes.CommitMultiStore // 主要(未缓存)状态
...
mempool mempool.Mempool // 应用侧内存池
anteHandler sdk.AnteHandler // 手续费和身份验证的前处理器
postHandler sdk.PostHandler // 后处理器,可选,例如用于小费
initChainer sdk.InitChainer // 初始化状态与验证者和状态块
beginBlocker sdk.BeginBlocker // 在处理任何交易前运行的逻辑
processProposal sdk.ProcessProposalHandler // 在 ABCI ProcessProposal 上运行的处理器
prepareProposal sdk.PrepareProposalHandler // 在 ABCI PrepareProposal 上运行的处理器
endBlocker sdk.EndBlocker // 在所有交易后运行的逻辑,和确定验证者集合的变化
...
// checkState 在 InitChain 时设置,并在 Commit 时重置
// deliverState 在 InitChain 和 BeginBlock 时设置,并在 Commit 时设置为 nil
checkState *state // 供 CheckTx 使用
deliverState *state // 供 DeliverTx 使用
processProposalState *state // 供 ProcessProposal 使用
prepareProposalState *state // 供 PrepareProposal 使用
...
}
一些重要组件包括:
CommitMultiStore
是 BaseApp
中主要的未缓存状态。它由应用中每个模块的 KVStores
组成。状态在每个区块结束时进行提交,一旦经过大多数验证者签名的预提交。anteHandler
在 CheckTx
和 DeliverTx
阶段都运行,负责身份验证、费用支付和其他执行前检查。CheckTx
阶段的有效交易。这些交易随后被 ProcessProposalHandler
用于提议一个区块。checkState
和 deliverState
是从 CommitMultiStore
派生的缓存状态。分别在 CheckTx
和 DeliverTx
阶段使用。CheckTx
一旦完整节点收到来自 Tendermint 的交易,它会通过 ABCI 向应用层发送 CheckTx
消息。这个阶段的目标是尽早消除无效交易,并保护完整节点的内存池不受垃圾交易的影响。注意,此阶段不收取Gas费用;因此,建议保持检查轻量化。
CheckTx 阶段执行以下步骤:
sdk.Msg
。sdk.Msg
上 运行 validateBasic
,执行基本的合理性检查。checkState
,本身是缓存的,在 anteHandler
被调用之前被 分支。这是为了确保如果 anteHandler
失败,状态的写入不会被提交。anteHandler
对交易进行身份验证和费用检查。请注意,未执行精确的Gas检查,因为处理程序不处理 sdk.Msg
。Gas费用是根据交易大小和特定于节点的 minGasPrices 计算的。BeginBlock
当 Tendermint 引擎收到区块提案时,它使用 ABCI 向应用层发送 BeginBlock
消息。这允许开发者在运行消息之前执行代码。它还重置主Gas计量器,分支 CommitMultiStore
来初始化 deliverState
,并调用所有已加载模块的 BeginBlocker()
。重要的是,不要在 BeginBlock
中有计算量大的逻辑,因为它与用户交易没有直接关联,无法收取Gas费用。
DeliverTx
DeliverTx
阶段在执行交易之前执行与 CheckTx 阶段完全相同的检查(除了 anteHandler
中的Gas费用检查,节点之间有所不同)。此外,交易会从应用侧的内存池 移除。交易中的每个 Msg 随后被 路由 到适当模块的 protobuf 服务。如果所有 Msg 成功执行,且 PostHandler 成功,分支的多存储会被 写入 到 DeliverState
的 CacheMultiStore
。
这是应用逻辑的核心位置,并被执行。
值得注意的是,auth 模块 提供了一个特殊的 AnteHandler
,执行特殊的检查,包括签名验证、费用扣除、Gas计算及账户序列递增,以避免重放攻击。
EndBlock
一旦所有交易执行完毕,Tendermint 将 EndBlock
消息发送到应用。如同 BeginBlock
,它可以被开发者用来在 Msg 被运行后执行代码。它还会调用所有已加载模块的 EndBlocker()
。
Commit
一旦达成共识(即基础 Tendermint 引擎已收到 2/3 验证者的预提交),它会将 Commit ABCI 消息发送到应用。Cosmos SDK 随后 将 从 DeliverState
中分支出的多存储写入 app.cms
,并持久化。DeliverState
存储来自 BeginBlock、DeliverTx 和 EndBlock 阶段的所有状态转换。它将 DeliverState
设置为 nil
,并将 app.cms
的提交哈希返回给 Tendermint。
最常见的错误之一是应用程序中存在非确定性行为,导致共识失败并使区块链停滞。例如,安全建议 Jackfruit 报告了一个由于非确定性引发的问题。authz 模块使用了主观的本地时钟时间而非块头的时间戳。
func (g Grant) ValidateBasic() error {
if g.Expiration.Unix() < time.Now().Unix() {
return sdkerrors.Wrap(ErrInvalidExpirationTime, "时间不能在过去")
}
}
另一个常见的非确定性来源是 遍历映射。因为映射的迭代顺序是不可确定的。建议仅在清除映射或从映射中检索键时进行迭代,以便于排序。
m := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
// 不好:非确定性
for k, v := range m {
fmt.Println(k, v)
}
// 好:确定性
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)
for _, k := range ks {
fmt.Println(k, m[k])
}
其他非确定性来源包括(但不限于)使用某些系统包(例如 unsafe、runtime、reflect、math/rand)、协程执行顺序以及任何外部来源的调用(例如磁盘、网络)。
Golang 中存在整数溢出的经典问题。建议使用 math 库。从无符号整数转换为有符号整数时也应小心。
var x uint8 = 255
// 整数溢出:打印 0
fmt.Println(x + 1)
浮点数由于精度有限而不是结合的。如果你使用浮点数,请确保精度损失不会导致行为不完善。
var f, f2, f3 float64 = 0.1, 0.2, 0.3
// 错误
fmt.Println((f+f2)+f3 == f+(f2+f3))
应格外小心,以避免在 BeginBlock
和 EndBlock
中发生恐慌和过度计算。这些函数中的错误可能导致拒绝服务。请注意,BeginBlock
和 EndBlock
与用户发送交易无关,因此你无法直接在这些函数中收取Gas。如果需要,应该在之前收取Gas。
既然我们拥有了应用专用区块链,我们希望能够与其他链通信。这是通过 IBC 协议完成的,IBC 代表跨区块链通信。那么,IBC 是如何工作的?
IBC 的一个组成部分是轻客户端。轻客户端是链上节点,存储区块链状态的一个子集,并跟踪另一条链的共识状态。唯一的客户端 ID 用于识别每个轻客户端,也包含其他链的证明规范,使其能够验证与共识状态的承诺证明。这使资源受限的设备能够参与网络。
轻客户端被封装在 connectionEnd
对象中。在不同区块链上的一对 connectionEnd
对象组成一个连接。通过四次握手,连接建立对方链的身份,并验证轻客户端是否正确对应于连接的链。
为了传递数据(例如执行连接握手),区块链首先提交状态到特定路径。中继者监控这一路径并将数据及证明传递到对方链。然后,证明被传递给轻客户端并验证。通过这种架构,无需信任中继者。如果中继者传递故障数据,证明会被拒绝,只影响网络的生存性。理论上,IBC 的安全性依赖于所连接链的安全性。
一旦建立连接,通道便被创建以便在跨链模块之间转发数据包。首先,一个模块可以绑定到一个端口。进行此操作将返回一个动态对象能力。然后,可以通过另一个四次握手在两个端口之间建立通道。这也返回另一个动态能力。在端口或通道上进行任何操作都将需要正确的能力,意味着恶意模块无法使用它们不拥有的端口或通道。模块现在可以通过通道发送数据包将其数据传递给另一个模块。这个构造被称为 IBC/TAO 层——TAO 代表传输、认证和排序。
在 Tendermint 中,需要 2/3 的验证者达成共识。这意味着如果你控制 1/3 的验证者,你可以攻击链的生存性。如果你控制 2/3 的验证者,则可以攻击链的正确性。
跨链安全允许共享来自提供链(例如 ATOM)到消费链的验证者。验证者可以使用他们在提供链上质押的代币在消费链上产生区块。验证者被从提供链选择以在消费链和提供链上运行验证者节点。作为回报,他们会从两个链中获得奖励和费用。
当然,如果验证者行为不端或不可靠,他们在提供链上质押的代币可能会被削减。这是通过名为 CCV(跨链验证)的机制实现的。消费链可以通过 CCV 模块向提供链提交不当行为的证据,验证者面临在提供链上失去质押代币的风险。两个链都实现了 CCV 模块并通过 IBC 进行通信。
IBC 的设计确实考虑到了安全性。不需要信任第三方验证跨链通信。如果发生不当行为,有机制来限制造成的损害。例如,中继者可以向轻客户端提交验证者不当行为的证明。或者使用动态能力来限制恶意模块的影响。
然而,像跨链桥一样,IBC 仍然是攻击者的一个有吸引力的目标,其安全性仅与其最薄弱的环节密切相关。Dragonfruit 安全建议(导致 BNB 智能链利用的错误)和 Dragonberry 安全建议 显示了 IAVL RangeProof 和 ics23 中的实现问题。幸运的是,ics23 错误在攻击者之前被 Cosmos 和 Osmosis 的核心团队发现。
此外,IBC 的存在,如同桥梁,如果链被黑客攻击,则会使缓解困难,因为代币可以转移到其他链,提供攻击者更容易的退出策略。
对于很多应用来说,维护自己的链可能显得过于复杂。你可以使用 CosmWasm,这是 Cosmos SDK 的一个模块,实现了链上的智能合约虚拟机。它为开发者提供了一个易于使用的接口,以在多种语言中编写智能合约,这些合同可以编译为 WebAssembly(WASM)字节码,并在 Cosmos 网络上执行。这也可以被视为一个 L1 链,用户可以在其上部署自己的智能合约,以供任何人交互。
要部署合同,我们首先将 wasm 代码上传到区块链。如果成功上传,响应中将包含代码 ID。然后,我们可以用某个 InstantiateMsg 实例化我们上传的合约。
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
我们可以按需多次实例化代码。每次实例化时,我们将获得一个唯一的合约地址(这意味着合约只能实例化一次)。上传代码和实例化之间的这一分离允许重用仅在初始化时有所不同的共同代码。
可以选择在实例化时提供一个所有者。所有者有权使用 MsgMigrateContract 事务迁移合约,将合约指向新代码。无需复杂的代理模式。
执行事务的入口点只有一个。执行函数负责将 Msg 派发到正确的处理程序。这防止了开发者意外暴露不必要的入口点。
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::CreatePot {
target_addr,
threshold,
} => execute_create_pot(deps, info, target_addr, threshold),
ExecuteMsg::Receive(msg) => execute_receive(deps, info, msg),
}
}
对于合约的可组合性,CosmWasm 使用了演员模型。核心思想是,演员一次只能处理一条消息。如果合约想要调用另一个合约,它必须保存其状态并调度一条消息。例如,托管合约 会完成执行并返回一个调用 BankMsg::Send
的新消息。请注意,所有消息都是捆绑在一条事务中,因此如果任何消息失败,整个事务将被回滚。
fn send_tokens(to_address: Addr, amount: Vec<Coin>, action: &str) -> Response {
Response::new()
.add_message(BankMsg::Send {
to_address: to_address.clone().into(),
Amount,
})
.add_attribute("action", action)
.add_attribute("to", to_address)
}
如果需要从消息中获取结果,则可以使用 子消息。子消息将通过调用者的响应函数提供一个回复。
CosmWasm 的设计借鉴了 Solidity 并防止常见的陷阱。
然而,拒绝服务仍然是一个有效的关注点。
总体而言,Cosmos 生态系统是对去中心化应用未来的独特愿景。许多安全机制到位,以帮助防止开发者导致代价高昂的错误。结合 Cosmos SDK 和 CosmWasm 中可用的强大测试框架,我们对 Cosmos 的未来持乐观态度。
Zellic 专注于保护新兴技术。我们的安全研究人员已经发现了在从财富 500 强到 DeFi 巨头的最有价值目标中的漏洞。
开发者、创始人和投资者信任我们的安全评估,以快速、自信地出货,并且没有关键的漏洞。凭借我们在实际攻击安全研究方面的背景,我们发现了其他人遗漏的东西。
联系我们↗ 进行一次更好的审计。真正的审计,而不是走过场。
- 原文链接: zellic.io/blog/exploring...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!