剖析Cosmos SDK(三):交互周期

煤油灯科技2022-07-13 10:40

创造

交易创建

主要的应用程序界面之一是命令行界面。交易Tx可以通过用户从命令行输入以下格式的命令来创建,在[command]中提供交易类型,在[args]中提供参数,在[flags]中提供诸如气体价格等配置:

[appname] tx [command] [args] [flags]

该命令将自动创建交易,使用账户的私钥进行签名,并将其广播给指定的对等节点。

创建交易有几个必要和可选的标志。--from标志指定了交易来自哪个账户。例如,如果交易是发送硬币,资金将从指定地址提取。

Gas和费用

介绍

在Cosmos SDK中,gas是一个特殊的单位,用于跟踪执行过程中的资源消耗。gas通常在对存储进行读写时被消耗,但如果需要进行昂贵的计算,也会被消耗掉。它有两个主要目的:

  • 确保区块不会消耗太多的资源,并且会被最终确定。这在Cosmos SDK中默认是通过区块气体表实现的。
  • 防止来自终端用户的垃圾邮件和滥用。为此,在消息执行过程中消耗的气体通常会被定价,从而产生费用(费用=气体*气体价格)。费用一般要由消息的发送者支付。请注意,Cosmos SDK默认不执行gas定价,因为可能有其他方法来防止垃圾邮件(例如带宽方案)。尽管如此,大多数应用程序将实施收费机制以防止垃圾邮件。这是通过AnteHandler完成的。

Gas Meter

在Cosmos SDK中,gas是uint64的一个简单别名,并由一个叫做gas meter的对象管理。Gas meter实现了GasMeter接口。

// GasMeter interface to track gas consumption
type GasMeter interface {
	GasConsumed() Gas
	GasConsumedToLimit() Gas
	GasRemaining() Gas
	Limit() Gas
	ConsumeGas(amount Gas, descriptor string)
	RefundGas(amount Gas, descriptor string)
	IsPastLimit() bool
	IsOutOfGas() bool
	String() string
}

其中:

  • GasConsumed()返回燃气表实例所消耗的燃气量。
  • GasConsumedToLimit() 返回燃气表实例所消耗的燃气量,如果达到极限,则返回极限值。
  • GasRemaining() 返回燃气表的剩余燃气量。
  • Limit()返回燃气表实例的极限。如果燃气表是无限的,则为0。
  • ConsumeGas(amount Gas, descriptor string) 消耗所提供的气体量。如果气体溢出,它就会用描述符的信息恐慌。如果燃气表不是无限的,如果消耗的燃气超过了限制,它就会惊慌。
  • RefundGas()从消耗的气体中扣除给定的金额。该功能可以将气体退还给交易或区块气体池,这样兼容EVM的链就可以完全支持go-ethereum StateDB接口。
  • IsPastLimit()如果燃气表实例消耗的燃气量严格超过限制,则返回真,否则返回假。
  • IsOutOfGas()如果燃气表实例消耗的燃气量高于或等于限制,则返回真,否则为假。
  • 燃气表一般被保存在ctx中,消耗燃气的模式如下:
ctx.GasMeter().ConsumeGas(amount, "description")

默认情况下,Cosmos SDK使用两种不同的燃气表,主燃气表和块燃气表。

主燃气表

ctx.GasMeter()是应用程序的主燃气表。主燃气表在BeginBlock中通过setDeliverState初始化,然后在导致状态转换的执行序列中跟踪燃气消耗,即那些最初由BeginBlock、DeliverTx和EndBlock触发的。在每个DeliverTx开始时,AnteHandler中的主gas Meter必须被设置为0,这样它就可以跟踪每个交易的气体消耗。

气体消耗可以手动完成,一般由模块开发者在BeginBlocker、EndBlocker或Msg服务中完成,但大多数情况下,只要有对商店的读或写,就会自动完成。这种自动消耗气的逻辑是在一个特殊的名叫GasKv商店里实现的。

封锁燃气表

ctx.BlockGasMeter()是用于跟踪每个区块的气体消耗量并确保其不超过一定限度的气体表。每次调用BeginBlock时都会创建一个新的BlockGasMeter实例。BlockGasMeter是有限的,每个区块的气体限制是在应用程序的共识参数中定义的。默认情况下,Cosmos SDK应用程序使用由Tendermint提供的默认共识参数:

// DefaultConsensusParams returns a default ConsensusParams.
func DefaultConsensusParams() *ConsensusParams {
	return &ConsensusParams{
		Block:     DefaultBlockParams(),
		Evidence:  DefaultEvidenceParams(),
		Validator: DefaultValidatorParams(),
		Version:   DefaultVersionParams(),
	}
}

// DefaultBlockParams returns a default BlockParams.
func DefaultBlockParams() BlockParams {
	return BlockParams{
		MaxBytes: 22020096, // 21MB
		MaxGas:   -1,
	}
}

// DefaultEvidenceParams returns a default EvidenceParams.
func DefaultEvidenceParams() EvidenceParams {
	return EvidenceParams{
		MaxAgeNumBlocks: 100000, // 27.8 hrs at 1block/s
		MaxAgeDuration:  48 * time.Hour,
		MaxBytes:        1048576, // 1MB
	}
}

// DefaultValidatorParams returns a default ValidatorParams, which allows
// only ed25519 pubkeys.
func DefaultValidatorParams() ValidatorParams {
	return ValidatorParams{
		PubKeyTypes: []string{ABCIPubKeyTypeEd25519},
	}
}

func DefaultVersionParams() VersionParams {
	return VersionParams{
		AppVersion: 0,
	}
}

当通过DeliverTx处理一个新的交易时,BlockGasMeter的当前值会被检查,看它是否超过限制。如果是,DeliverTx立即返回。这甚至可以发生在一个区块的第一笔交易中,因为BeginBlock本身可以消耗气体。如果没有,交易将被正常处理。在DeliverTx结束时,ctx.BlockGasMeter()跟踪的气体增加了处理该交易所消耗的数量:

ctx.BlockGasMeter().ConsumeGas(
	ctx.GasMeter().GasConsumedToLimit(),
	"block gas meter",
)

AnteHandler

AnteHandler在CheckTx和DeliverTx期间为每个事务运行,在事务中每个sdk.Msg的Protobuf Msg服务方法之前运行:

anteHandler没有在核心的Cosmos SDK中实现,而是在一个模块中实现。也就是说,现在大多数应用程序都使用auth模块中定义的默认实现。以下是anteHandler在普通Cosmos SDK应用程序中的用途。

  • 验证交易的类型是否正确。事务类型是在实现anteHandler的模块中定义的,它们遵循事务接口:
// Tx defines the interface a transaction must fulfill.
	Tx interface {
		// Gets the all the transaction's messages.
		GetMsgs() []Msg

		// ValidateBasic does a simple and lightweight validation check that doesn't
		// require access to any other information.
		ValidateBasic() error
	}

这使开发人员可以使用各种类型的应用程序进行事务处理。 在默认的认证模块中,默认的交易类型是Tx: Copy:

// Tx is the standard type used for broadcasting transactions.
message Tx {
  // body is the processable content of the transaction
  TxBody body = 1;

  // auth_info is the authorization related content of the transaction,
  // specifically signers, signer modes and fee
  AuthInfo auth_info = 2;

  // signatures is a list of signatures that matches the length and order of
  // AuthInfo's signer_infos to allow connecting signature meta information like
  // public key and signing mode by position.
  repeated bytes signatures = 3;
}
  • 核实交易中包含的每条消息的签名。每条信息都应该由一个或多个发件人签名,这些签名必须在anteHandler中进行验证。
  • 在CheckTx过程中,验证交易提供的气体价格是否大于本地的min-gas-prices(作为提醒,gas-prices可以从以下公式中扣除:费用=gas*gas-prices)。min-gas-prices是每个全节点的本地参数,在CheckTx过程中用于丢弃没有提供最低数额费用的交易。这确保了mempool不会被垃圾交易所困扰。
  • 验证交易的发送者是否有足够的资金来支付费用。当终端用户生成交易时,他们必须指出以下3个参数中的2个(第三个是隐含的):费用、气体和气体价格。这表明他们愿意为执行交易的节点支付多少钱。提供的气体值被存储在一个叫做GasWanted的参数中,供以后使用。
  • 设置newCtx.GasMeter为0,GasWanted为限。这一步很关键,因为它不仅确保交易不能消耗无限的气体,而且ctx.GasMeter在每次DeliverTx之间被重置(ctx在anteHandler运行后被设置为newCtx,而anteHandler在每次DeliverTx被调用时被运行)。

如上所述,anteHandler返回交易在执行过程中可以消耗的最大限度的气体,称为GasWanted。最后实际消耗的数量被称为GasUsed,因此我们必须让GasUsed =< GasWanted。当DeliverTx返回时,GasWanted和GasUsed都会被转发给底层共识引擎。

此外,用户可以使用几个标志用来表明他们愿意支付多少费用:

  • --gas指的是Tx消耗多少gas(代表计算资源)。Gas取决于交易,在执行前不会精确计算,但可以通过提供auto作为--gas的值来估计。
  • --gas-adjustment(可选)可以用来扩大gas的规模,以避免被低估。例如,用户可以指定其gas调整为1.5,以使用估计gas的1.5倍。
  • --gas-prices指定用户愿意为每单位的gas支付多少钱,可以是一个或多种面额的代币。例如, --gas-prices=0.025uatom, 0.025upho意味着用户愿意为每单位的gas支付0.025uatom和0.025upho。
  • --fees指定了用户愿意支付的总费用。
  • --timeout-height指定了区块超时的高度,以防止tx被提交超过某个高度。

所付费用的最终价值等于气体乘以气体价格。换句话说,费用=ceil(gas * gasPrices)。因此,由于费用可以用气体价格计算,反之亦然,用户只需指定两者之一。

之后,验证者通过比较给定或计算的gas-prices和当地min-gas-prices价格进行比较,决定是否将交易纳入他们的区块。如果气体价格不够高,Tx就会被拒绝,所以用户会被激励支付更多的费用。

CLI实例

应用程序的用户可以在他们的CLI中输入以下命令生成一个交易,从发送者地址向接收者地址发送1000uatom。它指定了他们愿意支付的gas数量:自动估算按比例放大1.5倍,gas价格为每单位0.025uatom。

appd tx send <recipientAddress> 1000uatom --from <senderAddress> --gas auto --gas-adjustment 1.5 --gas-prices 0.025uatom

其他交易创建方法

命令行是与应用程序互动的简单方法,但Tx也可以使用gRPC或REST接口或由应用程序开发人员定义的其他入口点来创建。从用户的角度来看,交互方式取决于他们使用的Web界面或钱包(例如,使用Lunie.io创建Tx,并用Ledger Nano S对其进行签名)。

添加到Mempool

每个收到Tx的全节点(运行Tendermint)都会向应用层发送一个ABCI消息,CheckTx以检查其有效性,并接收到一个abci.ResponseCheckTx。如果Tx通过了检查,它就会被保存在节点的Mempool中,这是一个每个节点独有的交易内存池,等待被纳入一个区块中——如果发现Tx无效,诚实的节点会丢弃它。在达成共识之前,节点不断地检查传入的交易,并将其传播给对等方。

检查的类型

全节点在CheckTx期间对Tx进行无状态检查和有状态检查,目的是尽可能早地识别和拒绝无效交易,以避免浪费计算。

无状态检查不需要节点访问状态——轻型客户端或离线节点可以做这些检查,因此计算成本较低。无状态检查包括确保地址不是空的,强制执行非负数以及定义中指定的其他逻辑。

状态检查是基于承诺的状态来验证事务和消息。例子包括检查相关的值是否存在并可以进行交易,地址是否有足够的资金,以及发送者是否被授权或有正确的所有权来进行交易。在任何特定的时刻,全节点通常有多个版本的应用程序的内部状态,用于不同的目的。例如,节点在验证交易的过程中会执行状态变化,但仍然需要最后提交的状态的副本才能回答查询——它们不应该使用未提交的变化的状态来响应。

为了验证一个Tx,全节点调用CheckTx,它包括无状态和有状态的检查。进一步的验证发生在DeliverTx阶段的后期。CheckTx经历了几个步骤,首先是对Tx进行解码。

解码

当应用程序从底层共识引擎(如Tendermint)收到Tx时,它仍然是其编码的[]字节形式,需要解密才能被处理。然后,runTx函数被调用,以runTxModeCheck模式运行,这意味着该函数将运行所有检查,但在执行消息和写入状态变化之前退出。

ValidateBasic

消息(sdk.Msg)从交易(Tx)中提取。由模块开发者实现的sdk.Msg接口的ValidateBasic方法为每个交易运行。为了丢弃明显无效的消息,BaseApp类型在CheckTx和DeliverTx事务中处理消息的早期就调用ValidateBasic方法。ValidateBasic可以只包括无状态检查(不需要访问状态的检查)。

准则

执行ValidateBasic时不收取gas,所以我们建议只执行所有必要的无状态检查以实现中间件的操作(例如,解析所需的签名者账户以验证中间件的签名)和不影响CheckTx阶段性能的无状态检查。在模块Msg服务器中处理消息时必须执行其他验证操作。

例如,如果消息是将硬币从一个地址发送到另一个地址,ValidateBasic可能会检查非空的地址和非负的硬币数量,但不需要了解状态,如地址的账户余额。

参见Msg Service Validation。

AnteHandler

在ValidateBasic检查之后,AnteHandlers被运行。从技术上讲,它们是可选的,但在实践中,它们经常出现以执行签名验证、gas计算、费用扣除和其他与区块链交易相关的核心操作。

缓存上下文的副本被提供给AnteHandler,它执行为交易类型指定的有限检查。使用副本允许AnteHandler为Tx做有状态的检查,而不修改最后提交的状态,如果执行失败,则恢复到原始状态。

例如,Auth模块AnteHandler检查和增加序列号,检查签名和账号,并从交易的第一个签名者那里扣除费用——所有的状态变化都是使用checkState进行的。

Gas

Context初始化了一个GasMeter,将跟踪在执行Tx过程中使用了多少气体。用户为Tx提供的气体量被称为GasWanted。如果GasConsumed,即执行过程中消耗的气体量超过GasWanted,执行将停止,对状态的缓存副本所做的改变将不会被提交。否则,CheckTx将GasUsed设置为GasConsumed并在结果中返回。在计算气体和费用值后,验证器节点检查用户指定的gas-prices是否大于其本地定义的min-gas-prices。

弃置或添加到内存池

如果在CheckTx过程中的任何一点,Tx失败了,它就会被丢弃并且交易的生命周期就会结束。否则,如果它成功地通过了CheckTx,默认的协议是将其转发给对等节点,并将其加入Mempool,这样Tx就会成为下一个区块中的候选者。

内存池的作用是跟踪所有全节点所看到的交易。全节点保留他们最后一个看到的mempool.cache_size交易的内存池缓存,作为防止重放攻击的第一道防线。理想情况下,mempool.cache_size要足够大足以涵盖整个mempool中的所有事务。如果mempool缓存太小,无法跟踪所有的交易,CheckTx就负责识别和拒绝重放交易。

目前,现有的预防措施包括费用和一个序列(nonce)计数器,以区分重放交易和相同但有效的交易。如果攻击者试图用许多Tx的副本来扰乱节点,保持内存池缓存的全节点将拒绝相同的副本,而不是对所有的副本运行CheckTx。即使这些副本有递增的序列号,攻击者也会因为需要支付费用而收到抑制。

验证器节点保留一个内存池以防止重放攻击,就像全节点所做的那样,但也将其作为一个未确认的交易池,为区块包含做准备。请注意,即使一个Tx在这个阶段通过了所有的检查,它仍然有可能在以后被发现是无效的,因为CheckTx没有完全验证交易(即它没有实际执行消息)。

包含在一个区块中

共识是验证器节点就接受哪些交易达成协议的过程,分几轮进行。每一轮都是由提议者创建一个最新的交易区块开始,最后由验证者——拥有投票权的特殊全节点负责达成共识,同意接受该区块或用一个无区块代替。验证者节点执行共识算法,如Tendermint BFT,使用ABCI请求向应用程序确认交易,以便达成这一协议。

共识的第一步是区块提案。共识算法在验证者中选择一个提议者来创建和提议一个区块——为了使一个Tx被包括在内,它必须在这个提议者的内存池中。

状态变化

共识的下一步是执行交易以完全验证它们。所有从正确的提议者那里收到区块提议的全节点通过调用ABCI函数BeginBlock、每个交易的DeliverTx和EndBlock来执行交易。虽然每个全节点都在本地运行一切,但这个过程会产生一个单一的、明确的结果,因为消息的状态转换是确定的,而且交易在区块提议中是明确排序的。

		-----------------------
		|Receive Block Proposal|
		-----------------------
		          |
			  v
		-----------------------
		| BeginBlock	      |
		-----------------------
		          |
			  v
		-----------------------
		| DeliverTx(tx0)      |
		| DeliverTx(tx1)      |
		| DeliverTx(tx2)      |
		| DeliverTx(tx3)      |
		|	.	      |
		|	.	      |
		|	.	      |
		-----------------------
		          |
			  v
		-----------------------
		| EndBlock	      |
		-----------------------
		          |
			  v
		-----------------------
		| Consensus	      |
		-----------------------
		          |
			  v
		-----------------------
		| Commit	      |
		-----------------------

DeliverTx

在BaseApp中定义的DeliverTx ABCI函数完成了大部分的状态转换:它按照共识期间承诺的顺序为区块中的每个事务运行。在底层,DeliverTx几乎与CheckTx相同,但在交付模式而不是检查模式下调用runTx函数。全节点使用deliverState,而不是使用他们的checkState:

  • 解码:由于DeliverTx是一个ABCI调用,Tx是以编码的[]byte形式被接收。节点首先解密交易,使用应用程序中定义的TxConfig,然后在runTxModeDeliver中调用runTx,这与CheckTx非常相似,但也执行并写入状态变化。
  • 检查和AnteHandler:全节点再次调用validateBasicMsgs和AnteHandler。之后会发生第二次检查,是因为他们在添加到Mempool阶段可能没有看到相同的交易,而恶意的提议者可能包括无效的交易。这里的一个区别是AnteHandler不会将gas-prices与节点的min-gas-prices进行比较,因为该值是每个节点的本地值——不同节点的不同值会产生不确定的结果。
  • MsgServiceRouter:虽然CheckTx退出时,DeliverTx继续运行runMsgs以完全执行事务中的每个Msg。由于事务中可能有来自不同模块的消息,BaseApp需要知道哪个模块可以找到合适的处理程序。这是通过BaseApp的MsgServiceRouter实现的,这样它就可以被模块的Protobuf Msg服务所处理。对于LegacyMsg路由,通过模块管理器调用Route函数来检索路由名称并在模块中找到就Handler。

Msg服务:Protobuf Msg服务负责执行Tx中的每条消息,并使状态转换持续在deliverTxState中。

  • PostHandlers:PostHandlers在消息执行后运行。如果他们失败了,runMsgs以及PostHandlers的状态变化都会被恢复。
  • Gas:当Tx被传送时,GasMeter被用来跟踪正在使用的气体量;如果执行完成,在abci.ResponseDeliverTx中设置并返回。如果执行停止是因为BlockGasMeter或GasMeter用完了,或者其他方面出了问题,那么最后的延迟函数会适当地出现错误或恐慌。

如果有任何因Tx无效或GasMeter耗尽而导致的失败状态变化,事务处理就会终止,任何状态变化都会被恢复。区块提案中的无效事务会导致验证器节点拒绝该区块,并投票给nil区块。

Commit

最后一步是让节点提交区块和状态变化。验证器节点执行状态转换的上一步以验证交易,然后签署区块以确认它。不是验证者的完整节点不参与共识——即它们不能投票——但会听取投票以了解它们是否应该提交状态变化。

当他们收到足够的验证者投票(2/3以上的预承诺按投票权加权)时,完整节点会承诺将一个新的区块添加到区块链中,并最终完成应用层中的状态转换。一个新的状态根被生成,作为状态转换的Merkle证明。应用程序使用从Baseapp继承的Commit ABCI方法;它通过将deliverState写入应用程序的内部状态来同步所有的状态转换。一旦状态变化被提交,checkState就会从最近提交的状态重新开始,deliverState重设为nil,以便保持一致并反映变化。

请注意,并不是所有的区块都有相同数量的交易,共识有可能导致一个nil区块或一个根本没有交易的区块。在公共区块链网络中,验证者也有可能是拜占庭的或者是恶意的,这可能会阻止区块链中的Tx被提交。可能的恶意行为包括提议者决定审查Tx,将其排除在区块之外,或者验证者投票反对该区块。

至此,Tx的交易生命周期已经结束:节点已经验证了它的有效性,通过执行其状态变化来交付它,并提交这些变化。Tx本身,以[]byte的形式,被存储在一个区块中并附加到区块链上。


原文链接:https://docs.cosmos.network/main/basics/tx-lifecycle.html

https://docs.cosmos.network/main/basics/gas-fees.html


---煤油灯科技victorlamp.com翻译整理---

你可能也喜欢这些文章

科技互联网,科技,资讯,动态,洞察,量子,计算,AI,人工智能,机器人,区块链,Web3,分布式,操作系统,OS,芯片,视频,深度,论文