Skip to content

Latest commit

 

History

History
156 lines (102 loc) · 14.5 KB

File metadata and controls

156 lines (102 loc) · 14.5 KB

区块链核心数据结构

在区块链中,交易被用来驱动链上状态的变更,在比特币中通过交易驱动UTXO模型中比特币的转移,在以太坊中通过交易驱动账户模型中合约状态的变更,无论这个过程是否成功,我们都需要能查询到这个过程的发起方,中间变更的状态以及最后变更得到的结果。这就好比一笔银行转账,需要明确转移的金额,支付和收款的账户,接着需要明确双方账户中的余额是否变更成功,最后会得到张回执单,可以通过回执内容查到这笔交易的信息。转账完成后,这些信息并不会消失,而是会持久保存,可能是为了方便客户查询,可能是为了接受监管,但无论怎样这些信息都需要持久的保存下来。

状态变更的发起者就是交易,甚至可以这样说区块链就是交易驱动的状态机,由此可见交易的重要性,为了高效的驱动状态机的变更就需要对交易结构进行精心的设计。

对于银行而言,交易产生的数据都是由中心化的数据中心来存储,数据中心需要高昂的费用和专业的工程师来维持运作,设置了非常高的门槛,但是对于区块链来说,在保存这些数据的同时既要足够的去中心化(每个节点都要保存一份副本),还要降低运行的硬件成本,让更多的人能参与其中来保证系统的可靠性。为了实现这些目标,就需要为区块链平台设计一种高效的数据结构来满足这个需求。

要设计一个高效的数据结构来保存这些数据,首先需要先了解这些数据的特点,然后再根据这些特点来设计具体的存储结构。

  • 交易数据,一般而言一笔交易只有几k的大小,部署合约的交易会大一些会有十几k到几十k不等,但是部署合约的交易量很少。
  • 合约状态,合约中storage标识的数据需要持久化,这些数据只有几b大,但是每个合约都可能有很多,数量大但是体积非常小。
  • 回执数据,回执数据与交易数据像对应,一笔交易产生一个回执,体积有几k大小。

归纳一下需要存储的数据就会发现一些共性,数量非常多,体积非常小,既要保存也要可以查询。

将查询的场景在拓展一下,考虑这样一种情况,A收到来自B的一个通知,B声称他已经从某某账户中汇款一定数额的钱给了A。去中心方式下,没有任何人能证明B的可靠。接到这一通知,A如何能判断B所说的是真的呢?

如果A想本人亲自验证这笔交易,首先,A要遍历区块链账本,定位到B的账户上,这样才能查看B所给的账户支票上是否曾经有足够的金额;接下来,A要遍历后续的所有账本,看B是否已经支出了这个账户支票上的钱给别人(是否存在双花欺骗);然后还要验证脚本来判断B是否拥有该账户的支配权。这一过程要求A必须得到完整的区块链才行。

以太坊中的完整区块大小以及几十G,并且这个大小还在不断的增长,要求完整区块数据显然提高了区块链的使用门槛,为了解决这个问题又需要加入一些高效的数据结构。

本章将会从交易开始,逐步介绍几高效的数据结构,来说明主流区块链的核心数据结构。

交易结构

交易(Transaction)是指由一个外部账户转移一定资产给某个账户, 或者发出一个消息指令到某个智能合约。

在以太坊网络中,交易执行属于一个事务。具有原子性、一致性、隔离性、持久性特点。

  • 原子性: 是不可分割的最小执行单位,要么做,要么不做。
  • 一致性: 同一笔交易执行,必然是将以太坊账本从一个一致性状态变到另一个一致性状态。
  • 隔离性: 交易执行途中不会受其他交易干扰。
  • 持久性: 一旦交易提交,则对以太坊账本的改变是永久性的。后续的操作不会对其有任何影响。

因为是事务型,因此我们需确保在执行事务前让交易符合一些设计要求。

  • 交易必须唯一,能区分不同交易且同一笔交易不能重复提交到账本中。
  • 交易内容不得变化,每个节点收到的交易都必须一致,交易执行时账本状态变化也是一致的。
  • 交易必须被合法签名,只有已正确签名的交易才能被执行。
  • 交易不能占用过多系统资源,影响其他交易执行。

对交易的设计要求,涉及软件系统的方方面面,但最基础部分还是交易数据本身。

看一下交易的基础数据结构。

type txdata struct {
    AccountNonce uint64          `json:"nonce"    gencodec:"required"`
    Price        *big.Int        `json:"gasPrice" gencodec:"required"`
    GasLimit     uint64          `json:"gas"      gencodec:"required"`
    Recipient    *common.Address `json:"to"       rlp:"nil"` // nil means contract creation
    Amount       *big.Int        `json:"value"    gencodec:"required"`
    Payload      []byte          `json:"input"    gencodec:"required"`

    // Signature values
    V *big.Int `json:"v" gencodec:"required"`
    R *big.Int `json:"r" gencodec:"required"`
    S *big.Int `json:"s" gencodec:"required"`

    // This is only used when marshaling to JSON.
    Hash *common.Hash `json:"hash" rlp:"-"`
}
  • AccountNonce,交易发起者内部唯一标识交易的字段,避免交易双重支付。
  • Price,此交易的gas price。
  • GasLimit,此交易允许的最大gas量。
  • Recipient,交易接收者,如果为nil说明是个合同创建交易。
  • Amount,交易转移的ETH数量,单位是wei。
  • Payload,交易数据。
  • V,R,S, 交易签名,通过交易签名可以计算出交易发送者地址。

AccountNonce

AccountNonce是一个自增的uint64大整数,含义是发送者帐户的已确认交易的数量,一个地址每发送一笔交易并且被确认后,其会自增1。AccountNonce是在以太坊代码中的名字,对于用户来说更为熟知的是转换为Json以后的名字nonce

比如一个以太坊账户有100个以太币余额,同时向A和B发送了一笔转账交易,分别转了7个以太币,我们期望第一笔转账是成功的,第二笔转账是失败的,但是在以太坊这样一个分布式系统中并不能保证安装我们期望的顺序执行。这个时候就发挥nonce的作用了,假如发送第一笔交易时nonce是5,第二笔交易是6,当节点先收到第二笔交易时,查询这个发送者地址的nonce发现是4,显然下一笔交易的nonce应该是5而不是6,这个时候节点就会暂存nonce为6的这笔交易,等待nonce为5的交易到来并且执行,这样就可以按序执行交易了,同时也防止了双花攻击。

当我们在发送交易时如果不知道发送者地址的nonce也可以向区块链查询,然后在发送交易即可,还需要注意的一点是这个nonce只在对应的账户地址有意义。

Price

Price表示这笔交易愿意为每个gas付出的价格。gas是以太坊的燃料。gas不是ether,它是独立的虚拟货币,有相对于ether的汇率。以太坊使用gas来控制交易可以花费的资源量,因为它将在全球数千台计算机上处理。开放式(图灵完备的)计算模型需要某种形式的计量,以避免拒绝服务攻击或无意中的资源吞噬交易。

发起者可以调整交易中Price,以更快地确认交易。Price 越高,交易越容易被节点验证打包。相反,较低优先级的交易可能会降低他们愿意为gas支付的价格,导致确认速度减慢。可以设置的最低+gasPrice+为零,这意味着免费的交易。在区块空间需求低的时期,这些交易将被开采。

Recipient

Recipient同样也是在以太坊代码中的字段,转换为Json时被重命名为to。交易的接收者在to字段中指定。这包含一个20字节的以太坊地址。地址可以是EOA或合约地址。

以太坊没有进一步验证这个字段。任何20字节的值都被认为是有效的。如果20字节的值对应于没有相应私钥的地址,或没有相应的合约,则该交易仍然有效。如果是一笔转账交易,以太币会被发送到指定地址,但是因为指定地址的私钥无法获得,相当于失去了这笔钱的控制权,好比丢失了以太币。

Amount

Amount表示交易转移的ETH数量,单位是wei。在以太坊中一个以太币等于 10的18次方 Amount,当要表示 100 亿以太币时,Amount 等于10的27次方。已远远超过Uint64所能表示的范围(0-18446744073709551615)。因此 geth 一律采用Go标准包提供的大数 big.Int 进行货币运算和定义货币。这里的Price和Amount均是 big.Int 指针类型。

Payload

to为空时,Payload字段表示部署合约的内容,如果to不为空则表示调用合约的代码,其中有要调用的函数签名和函数参数。

V R S

V R S被用来验证签名,V是签名前缀,比如值为1的时候表示是以太坊主网,1337表示是私有的测试网络,不同的网络所对应的验证签名的算法可能不同,对应的R,S也会得到不同的结果。R,S用来恢复公钥,验证签名。

Sig = (R, S)

要验证签名,必须有签名(R和S),序列化交易和公钥(与用于创建签名的私钥对应)。实质上,对签名的验证意味着“只有生成此公钥的私钥的所有者才能在此交易上产生此签名。

交易回执

在以太坊中矿工会把交易打包成区块传播到其它节点,当其它节点验证区块的有效性后就会逐笔执行交易,部分交易是作为消息在以太坊的EVM中执行的,在EVM执行交易的时候就会出现多种可能,比如执行出现错误,执行因为gas不够被回滚等,同时在执行过程中也会有日志产生。交易的发起者是无法及时感知到这些在执行过程中的状态和结果的,首先是不知道交易何时被打包执行,其次是不知道EVM执行具体需要多少时间。

为了解决交易的执行过程对交易发起者不透明的问题,以太坊引入了交易回执的概念,当一笔交易发送成功的时候发送者会获得一个交易哈希,当需要知道这笔交易的执行过程中产生的状态和结果时就需要用这个哈希来查询回执。由于得到回执的时间不确定,这里就需要轮训来获得。回执非常像银行的交易电子回单。

同样,在以太坊中一份交易回执记录了关于此笔交易的处理结果信息:

回执信息分为三部分:共识信息、交易信息、区块信息。下面分别介绍各类信息。

执行信息

这部分的内容记录了交易经过执行以后的信息。

  • Status: 成功与否,1表示成功,0表示失败。注意在高度1035301前,并非1或0,而是 StateRoot,表示此交易执行完毕后的以太坊状态。
  • CumulativeGasUsed: 区块中已执行的交易累计消耗的Gas,包含当前交易。
  • Logs: 当前交易执行所产生的智能合约事件列表。
  • Bloom:是从 Logs 中提取的事件布隆过滤器,用于快速检测某主题的事件是否存在于Logs中。

交易信息

这部分信息记录的是关于回执所对应的交易信息。

  • TxHash : 交易回执所对应的交易哈希。
  • ContractAddress: 当这笔交易是部署新合约时,记录新合约的地址。
  • GasUsed: 这笔交易执行所消耗的Gas燃料。

回执信息

这部分信息完全是为了方便外部读取交易回执,不但知道交易执行情况,还能方便的指定该交易属于哪个区块中第几笔交易。

  • BlockHash: 交易所在区块哈希。
  • BlockNumber: 交易所在区块高度。
  • TransactionIndex: 交易在区块中的序号。

回执存储

交易回执作为交易执行中间产物,为了方便快速获取某笔交易的执行明细。以太坊中有跟随区块存储时实时存储交易回执。但为了降低存储量,只存储了必要内容。

首先,在存储时,将交易回执对象转换为精简内容。

未存储的内容要么可以通过查询交易获得,要么是动态生成的,没有存储的必要。

交易池

交易被创建后就会由源节点广播到区块链网络中的其它节点,比如在比特币中交易的传播方式采用了Gossip协议,由源节点发送到其相邻节点,相邻节点在转发到他们的相邻节点,以此类推。一笔交易可以在极端的时间内传播到整个区块链网络,从而让网络中的所有节点接收到这笔交易。

当网络中的节点接收到这笔交易并且验证通过后会将有效的交易添加到自己的内存池(memory pool)中。内存池是�区块链节点维护的一份未确认交易的临时列表。交易池中的交易是在网络中广播但是尚未打包到区块中的待确认有效交易,这些交易等待矿工将其打包成区块。

一些节点实现还维护一个单独的孤立交易池。交易在网络中传播的过程中并不总是顺序的到达目的节点,这就会导致有可能子交易先于父交易到达,这时子交易就是孤立交易暂存于孤立交易池中。当一笔新交易到达时会检查是否匹配孤立交易池中的交易,如果匹配则把与之匹配的交易从孤立交易池中移除,同时放入交易池中。

如果流入交易池交易的数量大于交易打包成区块的数量就会造成区块链网络的拥塞现象,大量的低手续费的交易迟迟不能被打包成区块,降低了整个网络中交易确认的速度。比如在2017年5月和12月,比特币内存池中等待确认的交易数量达到了历史高值,均突破了18万笔交易,造成了区块链网络的严重拥塞。对于这个问题社区也提出了一些解决方案,比如区块链扩容,通过增加打包交易的速率降低网络拥塞,再比如闪电网络,把大量的交易放到比特币之外的二层协议来解决等等,但是目前社区还没有达成最终解决方案的共识。