以太坊中的合约创建流程

以太坊 Yellow Paper 学习笔记(三)

Posted by Nicodechal on 2020-10-22

之前已经提到过,交易有两种类型,其中一种为合约创建,使用 Λ\Lambda 表示,合约创建其实就是创建账户的过程,账户是包含合约代码的账户。

(σ′,g′,A,z,o)≡Λ(σ,s,o,g,p,v,i,e,ζ,w)(\mathbf{\sigma'},g',A,z,\mathbf{o})\equiv\Lambda(\mathbf{\sigma},s,o,g,p,v,\mathbf{i},e,\zeta,w)

上面是完整的创建函数定义,输入参数定义如下:

  1. σ\mathbf{\sigma},执行前的状态,此状态是已经完成交易前操作的状态,即从发送方账户扣掉 TgTpT_gT_p,同时 nonce 加一了。
  2. ss,发送方账户。
  3. oo,原始的交易发起者。当合约创建和消息调用过程不是直接通过交易发起的而是由代码触发时会和发起者不同。
  4. gg,可用的 gas。
  5. pp,gasprice。
  6. vv,一部分随着交易执行发送的钱。
  7. i\mathbf{i},初始化合约的代码。
  8. ee,此时的栈深度。
  9. ζ\zeta,创建新账户时用到的盐值。
  10. ww,代表对状态修改的权限。

输出之前介绍过前 4 个,第五个 o\mathbf{o} 为创建的合约的代码。

创建账户

创建的合约地址 ( 就是一个账户 ) 是一个通过发送方地址和其 nonce 计算得到的,如果新账户记为 aa,则有:

a≡A(s,σ[s]n−1,ζ,i)A(s,n,ζ,i)≡B96..255(KEC(B(s,n,ζ,i)))B(s,n,ζ,i)≡{RLP((s,n))if ζ=∅(255)⋅s⋅ζ⋅KEC(i)otherwise\begin{aligned} a &\equiv A(s,\mathbf{\sigma}[s]_n-1,\zeta,\mathbf{i}) \\\\ A(s,n,\zeta,\mathbf{i}) &\equiv\mathcal{B}_{96..255}(\mathtt{KEC}(B(s,n,\zeta,\mathbf{i}))) \\\\ B(s,n,\zeta,\mathbf{i}) &\equiv \left\{ \begin{array}{l} &\mathtt{RLP}((s, n)) & if\ \zeta= \emptyset\\ (255)\cdot s\cdot \zeta \cdot \mathtt{KEC}(\mathbf{i}) & \mathrm{otherwise} \end{array} \right. \end{aligned}

有两种情况,如果有盐值,使用 (255)⋅s⋅ζ⋅KEC(i)(255)\cdot s\cdot \zeta \cdot \mathtt{KEC}(\mathbf{i}) 将发送方的地址、盐值和初始化代码的哈希值连接得到用来产生新账户的字节数组。如果没有盐值,则直接对发送方地址和其 nonce 组成的序列取 RLP 编码得到需要的字节数组,这个数组可以记为 BB。

然后对于上述字节数组取哈希得到 256 为的结果,然后取结果最右边的 160 位作为新的账户地址。

这里注意传入的 nonce 减一是因为执行前已经将账户的 nonce 加一了,减一取到当前需要使用的 nonce。

新创建的账户四个数据域分别初始化如下:

  1. nonce,初始化为 1。
  2. balance,初始化为传入的金额。
  3. storageRoot,此时为空,所以初始化为空树的根哈希。
  4. codeHash,代码为空所以初始化为空串的哈希值。

假设账户创建完的状态为 σ⋆\mathbf{\sigma}^\star,则有:

σ⋆≡σ except:σ⋆[a]=(1,v+v′,TRIE(∅),KEC(()))σ⋆[s]={∅if σ[s]=∅ ∧v=0a⋆otherwisea⋆≡(σ[s]n,σ[s]b−v,σ[s]s,σ[s]c)v′={0if σ[a]=∅σ[a]botherwise\begin{aligned} \mathbf{\sigma}^\star &\equiv \mathbf{\sigma}\ \mathrm{except}:\\\\ \mathbf{\sigma}^\star[a] &=(1,v+v',\mathtt{TRIE}(\emptyset),\mathtt{KEC}(())) \\\\ \mathbf{\sigma}^\star[s] &= \left\{ \begin{array}{l} \emptyset & if\ \mathbf{\sigma}[s]=\emptyset\ \wedge v=0 \\ \mathbf{a}^\star & \mathrm{otherwise} \end{array} \right. \\\\ \mathbf{a}^\star &\equiv (\mathbf{\sigma}[s]_n,\mathbf{\sigma}[s]_b - v, \mathbf{\sigma}[s]_\mathbf{s},\mathbf{\sigma}[s]_c) \\\\ v'&= \left\{ \begin{array}{l} 0 & if\ \mathbf{\sigma}[a]=\emptyset \\ \mathbf{\sigma}[a]_b & \mathrm{otherwise} \end{array} \right. \end{aligned}

这里除了初始化新账户,发送方账户也要减去响应发出的金额。另外如果创建的账户的状态已经存在,则也会将其中包含的余额用来初始化新账户。

执行初始化代码

执行代码的函数定义为 Ξ\Xi:

(σ⋆⋆,g⋆⋆,A,o)≡Ξ(σ⋆,g,I,{s,a})(\mathbf{\sigma}^{\star\star},g^{\star\star},A,\mathbf{o})\equiv\Xi(\mathbf{\sigma}^\star,g,I,\{s, a\})

其中 II 是一些执行相关的变量,包括执行的代码等,这里暂不展开。

执行代码的过程中会消耗 gas,如果 gas 不够会抛出 OOG (out of gas) 异常,此时不会影响以太坊状态,同时执行后的状态被设为 ∅\emptyset。

如果初始化的代码成功执行完了,需要支付一个合约成功创建的费用,定义为 cc,根据最后创建的合约的字节数进行收费:

c≡Gcodedeposit×∥o∥c\equiv G_{codedeposit}\times\parallel\mathbf{o}\parallel

收费标准为 200gas/byte。此时如果 gas 不够,也会抛出 OOG 异常。

当出现异常时,剩余可用的 gas 会变成 0。也就是说出现异常时,为创建合约支付的金额不会受影响,但是向创建的账户的转账 ( 即执行中传递的 value 值 ) 会被终止。定义异常判断条件 FF:

F≡((σ⋆⋆=∅∧o=∅) ∨g⋆⋆<c ∨ ∥o∥>24576)F\equiv((\mathbf{\sigma}^{\star\star}=\emptyset \wedge \mathbf{o}=\emptyset)\ \vee g^{\star\star}<c\ \vee\ \parallel\mathbf{o}\parallel>24576)

最后一个表示合约长度超出限制。

如果没有异常,剩余的金额会进行正常的返还流程,此时对状态的修改也会持久化。

最终的状态 (σ′,g′,A,z)(\mathbf{\sigma'},g',A,z) 定义如下。

g′≡{0if  Fg⋆⋆−cotherwiseσ′≡{σif  Fσ⋆⋆except:    σ′[a]=∅if  DEAD(σ⋆⋆,a)σ⋆⋆except:    σ′[a]c=KEC(o)if  otherwisez≡{0if  σ⋆⋆=∅ ∨g⋆⋆<c1otherwiseDEAD(σ,a)≡σ[a]=∅ ∨EMPTY(σ,a)EMPTY(σ,a)≡σ[a]c=KEC(()) ∧ σ[a]n=0 ∧ σ[a]b=0\begin{aligned} g' &\equiv \left\{ \begin{array}{l} 0 & if\ \ F \\ g^{\star\star}-c & \mathrm{otherwise} \end{array} \right. \\\\ \mathbf{\sigma}' &\equiv \left\{ \begin{array}{l} \mathbf{\sigma} & if\ \ F \\ \mathbf{\sigma}^{\star\star} &\mathrm{except}: \\ \ \ \ \ \mathbf{\sigma}'[a]=\emptyset & if\ \ \mathtt{DEAD}(\mathbf{\sigma}^{\star\star},a) \\ \mathbf{\sigma}^{\star\star} &\mathrm{except}: \\ \ \ \ \ \mathbf{\sigma}'[a]_c=\mathtt{KEC}(\mathbf{o}) & if\ \ \mathrm{otherwise} \end{array} \right. \\\\ z &\equiv \left\{ \begin{array}{l} 0 & if\ \ \mathbf{\sigma}^{\star\star}=\emptyset\ \vee g^{\star\star}<c\\ 1 & \mathrm{otherwise} \end{array} \right. \\\\ \mathtt{DEAD}(\mathbf{\sigma}, a) &\equiv \mathbf{\sigma}[a]=\emptyset\ \vee \mathtt{EMPTY}(\mathbf{\sigma}, a) \\\\ \mathtt{EMPTY}(\mathbf{\sigma}, a) &\equiv \mathbf{\sigma}[a]_c=\mathtt{KEC}(())\ \wedge\ \mathbf{\sigma}[a]_n=0\ \wedge\ \mathbf{\sigma}[a]_b=0 \end{aligned}

如果创建成功 (此时账户不存在或者状态为空 (即没有代码,nonce 为 0 且余额为 0) ),会对账户的 codeHash 设置为账户代码的哈希值。如果执行成功返回 1,否则返回 0。