深入EVM:EVM工作机制的深层分析(终极版)

本文翻译自英文原文: Getting Deep Into EVM: How Ethereum Works Backstage

本文是“深入区块链”系列文章之一,意在深入介绍以太坊等区块链的内部工作机制。LinkChain博客前期曾翻译并分享了该系列文章的其中一篇“四十种智能合约支持平台(完全版)”,其它文章如下:

Getting Deep Into Geth: Why Syncing Ethereum Node Is Slow

Getting Deep Into Ethereum: How Data Is Stored In Ethereum?

本文将详细深入介绍EVM的核心机制,涉及智能合约建立机制、消息调用机制,并介绍存储、内存、CALLDATA和栈(STACK)这四类数据管理机制。

要理解本文内容,读者最好具有基础的以太坊知识。如果有必要,推荐阅读下面这篇博客:

5 resources to get started with ethereum

文中使用的所有例子和演示,均开源提供在该代码库中,读者可以克隆代码、运行npm install,进而查看运行结果。

EVM概览

在深入了解并通过代码例子查看EVM的工作机制之前,我们先阐述EVM最适用于以太坊之处,以及EVM的组成。在看到下面给出的复杂结构图时,请不要心生恐惧。一旦读完本文,你自然会理解这些它们。

下图给出了EVM是如何匹配以太坊运行的:

下图给出了EVM的基本架构:

下图给出了EVM各组成部分间的相互作用机制,由此实现了以太坊的神奇功能。

至此,我们对EVM有了一个整体上的了解。下面,我们将深入介绍各部分在以太坊运行中的重要作用。

以太坊合约(Ethereum Contracts)

基本概念

智能合约就是一种计算机程序。以太坊合约可以称为运行在EVM上的智能合约。EVM是一种“沙盒”(Sandbox)运行时,它为智能合约在以太坊中的运行提供了完全独立的环境。这也意味着,运行在EVM中的每个智能合约不能访问网络,也不能访问运行EVM主机上的任何进程。

我们知道,以太坊具有两种类型的账户,合约账户(contract account)和外部账户(external account)。每个账户由一个地址唯一标识,所有账户共享同一地址空间。EVM可处理的地址长度为160比特。

每个账户由余额(balance)、nonce、字节码(bytecode)和存储数据(即存储,storage)组成。以太坊的两类账户间存在着一些差异之处。例如,外部账户的字节码和存储为空,而合约账户中存储了字节码和整个状态树的默克尔根哈希值。此外,外部地址对应一个私钥,而合约账户则没有。对于合约账户,除了对每个以太坊交易做正常的加密认证外,所有其余动作均由其所持有的字节码控制。

创建合约

合约是以交易的形式创建的。交易中的接收者地址为空,数据域则包含了要创建合约的编译后字节码(需注意,一个合约可以创建另一个合约)。下面给出一个例子。打开练习一的目录,其中可以看到一个名为“MyContract”的合约。该合约的代码如下:

pragma solidity ^0.4.21;
contract MyContract {
  event Log(address addr);
function MyContract() public {
    emit Log(this);
  }
function add(uint256 a, uint256 b) public pure returns (uint256) {
    return a + b;
  }
}

运行命令truffle develop,以开发模式打开一个Truffle终端。一旦终端启动完成,使用下面命令在MyContract中部署实例:

truffle(develop)> compile
truffle(develop)> sender = web3.eth.accounts[0]
truffle(develop)> opts = { from: sender, to: null, data: MyContract.bytecode, gas: 4600000 }
truffle(develop)> txHash = web3.eth.sendTransaction(opts)

下面检查合约是否成功部署。运行如下代码:

truffle(develop)> receipt = web3.eth.getTransactionReceipt(txHash)
truffle(develop)> myContract = new MyContract(receipt.contractAddress)
truffle(develop)> myContract.add(10, 2)
{ [String: ‘12’] s: 1, e: 1, c: [ 12 ] }

我们深入查看一下上面的工作。当一个新的合约部署到以太坊区块链上时,首先完成的是创建合约对应的账户¹。在上例中我以看到,构造函数(constructor)中记录了合约的地址。为进一步确认,可查看receipt.logs[0].data中保存的就是合约生成的32个字节的地址,receipt.logs[0].topics中保持的是“Log(address)”字符串的Keccak-256哈希(即SHA3)。 K

调用合约中函数时,后台的运行结构图

下一步,随交易发送的数据将以字节码方式执行。它初始化存储中的状态变量,并确定所创建的合约体。该过程在合约的全生命周期中仅执行一次。初始化的代码并非存储在合约中,它实际上生成了要存储的字节码作为其返回值。谨记,一旦合约账户创建完成,就无法更改合约的代码²。

鉴于初始化过程返回了合约体中将要存储的字节码,因此从构造函数的逻辑无法访问此代码,这点具有意义的。下面以练习一中的Impossible合约为例:

contract Impossible {
  function Impossible() public {
    this.test();
  }
function test() public pure returns(uint256) {
    return 2;
  }
}

如果我们尝试编译该合约,那么就会得到警告,指出在构造函数中引用了 this (即“referencing this within the constructor function”)。该警告并不影响编译的继续进行。但是一旦部署新实例,就会出现终止执行并还原状态(revert)。这是因为运行尚未存储的代码是毫无意义的³。另一方面,因为账户已经创建,我们可以访问合约的地址,但是其中并未存储任何代码。

此外,代码执行还会产生另一种情况,例如更改了存储、创建更多账户、做更多的消息调用等。下面以AnotherContract合约代码为例:

contract AnotherContract {
  MyContract public myContract;
  function AnotherContract() public {
    myContract = new MyContract();
  }
}

在Truffle终端中运行下面命令,查看合约的运行情况:

truffle(develop)> compile
truffle(develop)> sender = web3.eth.accounts[0]
truffle(develop)> opts = { from: sender, to: null, data: AnotherContract.bytecode, gas: 4600000 }
truffle(develop)> txHash = web3.eth.sendTransaction(opts)
truffle(develop)> receipt = web3.eth.getTransactionReceipt(txHash)
truffle(develop)> anotherContract = AnotherContract.at(receipt.contractAddress)
truffle(develop)> anotherContract.myContract().then(a => myContractAddress = a)
truffle(develop)> myContract = MyContract.at(myContractAddress)
truffle(develop)> myContract.add(10, 2)
{ [String: ‘12’] s: 1, e: 1, c: [ 12 ] }

另一方面,鉴于Solidity结构最终也将编译为指令码,因此合约也可以使用CREATE指令码(opcode)创建。上面介绍的两种合约创建方式的工作机制相同。

下面,我们将介绍消息调用的工作机制。

消息调用(Message Call)

合约间可以通过消息调用实现相互调用。一个Solidity合约在每次调用另一个合约的函数时,就会生成一次消息调用。每次调用具有发送者、接受者、二进制内容(payload)、值以及GAS数量。限制消息调用的深度为不大于1024层。

Solidity为地址类型提供了原生调用方法,工作如下:

address.call.gas(gas).value(value)(data)

其中,gas是要传递的GAS数量,address是调用的地址,value是要传递的以太币wei数,data是要发送的二进制内容。谨记,valuegas是可选参数,使用应谨慎。因为默认情况下,低层调用将会将发送者几乎全部剩余的GAS发送出去。

图 GAS消费结构图

从上图可见,合约可以确定每次调用中要传递的GAS数量。每次调用都会因为“GAS耗尽”(OOG,out-of-gas)异常而终止执行。为避免出现安全问题,调用中至少会保留发送者GAS数量的1/64。这使得发送者可以处理调用的OOG异常、完成执行而不会耗尽GAS,进而也不会触发异常。

产生异常的结构图

下面以练习二中的Caller合约为例:

contract Implementation {
  event ImplementationLog(uint256 gas);
function() public payable {
    emit ImplementationLog(gasleft());
    assert(false);
  }
}
contract Caller {
  event CallerLog(uint256 gas);
  Implementation public implementation;

  function Caller() public {
    implementation = new Implementation();
  }
function () public payable {
    emit CallerLog(gasleft());
    implementation.call.gas(gasleft()).value(msg.value)(msg.data);
    emit CallerLog(gasleft());
  }
}

其中,Caller合约只有一个回调函数,实现所有接收到的调用重定向到Implementation实例。该实例只是通过每个接收到的调用上的assert(false)抛出,调用将消费所有提供的GAS,进而在传递调用给Implementation之前和之后,将GAS数量记录到Caller中。下面启动一个Truffle终端运行如下命令,查看运行情况:

truffle(develop)> compile
truffle(develop)> Caller.new().then(i => caller = i)
truffle(develop)> opts = { gas: 4600000 }
truffle(develop)> caller.sendTransaction(opts).then(r => result = r)
truffle(develop)> logs = result.receipt.logs
truffle(develop)> parseInt(logs[0].data) //4578955
truffle(develop)> parseInt(logs[1].data) //71495

如结果所示,71495大体上构成了4578955的第64个部分。该例子清晰地验证了,代码处理了内部调用抛出的OOG异常。

Solidity还提供了call操作码,支持在内联汇编(inline assembly)中管理调用:

call(g, a, v, in, insize, out, outsize)

其中,g是要传递的GAS数量,a是被调用地址,v是要传递的以太币wei数,in指定了保存调用数据的insize字节的内存地址,outoutsize指定了返回数据的内存存储地址。汇编调用与函数二者的唯一不同之处在于,汇编调用支持我们处理返回数据,而函数只会返回1或0指示函数处理成功与否。

EVM支持一类特殊的消息调用变体,称为“delegatecall”。同上,Solidity在提供内联汇编版本的同时,还提供了内建的地址方法。二者的不同之处在于,对于低层调用,目标代码在调用合约的上下文内执行,而msg.sendermsg.value并非如此。⁴

更好地理解delegatecall的工作机制,我们对下面的例子进行分析。首先给出Greeter合约的代码:

contract Greeter {
  event Thanks(address sender, uint256 value);
function thanks() public payable {
    emit Thanks(msg.sender, msg.value);
  }
}

如上,Greeter合约只定义了一个thanks函数,发出一个承载了msg.valuemsg.sender数据的事件。在Truffle终端中使用如下命令运行该方法:

truffle(develop)> compile
truffle(develop)> someone = web3.eth.accounts[0]
truffle(develop)> ETH_2 = new web3.BigNumber(‘2e18’)
truffle(develop)> Greeter.new().then(i => greeter = i)
truffle(develop)> opts = { from: someone, value: ETH_2 }
truffle(develop)> greeter.thanks(opts).then(tx => log = tx.logs[0])
truffle(develop)> log.event                     //Thanks
truffle(develop)> log.args.sender === someone   //true
truffle(develop)> log.args.value.eq(ETH_2)      //true

运行结果确认了该函数的功能。注意Wallet合约的代码:

contract Wallet {
  Greeter internal greeter;

  function Wallet() public {
    greeter = new Greeter();
  }

  function () public payable {
    bytes4 methodId = Greeter(0).thanks.selector;
    require(greeter.delegatecall(methodId));
  }
}

该合约只定义了一个回调函数,通过delegatecall执行Greeter#thanks方法。下面通过Wallet合约调用Greeter#thanks合约,在Truffle终端查看运行情况:

truffle(develop)> Wallet.new().then(i => wallet = i)
truffle(develop)> wallet.sendTransaction(opts).then(r => tx = r)
truffle(develop)> logs = tx.receipt.logs
truffle(develop)> SolidityEvent = require(‘web3/lib/web3/event.js’)
truffle(develop)> Thanks = Object.values(Greeter.events)[0]
truffle(develop)> event = new SolidityEvent(null, Thanks, 0)
truffle(develop)> log = event.decode(logs[0])
truffle(develop)> log.event                    // Thanks
truffle(develop)> log.args.sender === someone  // true
truffle(develop)> log.args.value.eq(ETH_2)     // true

从结果中可以看到,delegatecall函数保持了msg.valuemsg.sender

这意味着合约可以在运行时从不同的地址动态地加载代码。存储、当前地址和余额依然指向调用合约,只有代码是取自于被调用地址。这意味着可在Solidity中实现“软件库”⁵。

关于delegatecalls,我们还需要了解一件事情。如上所述,被调用合约的存储是可以被所执行代码访问的。下面查看Calculator合约的代码:

contract ResultStorage {
  uint256 public result;
}
contract Calculator is ResultStorage {
  Product internal product;
  Addition internal addition;
function Calculator() public {
    product = new Product();
    addition = new Addition();
  }

  function add(uint256 x) public {
    bytes4 methodId = Addition(0).calculate.selector;
    require(addition.delegatecall(methodId, x));
  }
function mul(uint256 x) public {
    bytes4 methodId = Product(0).calculate.selector;
    require(product.delegatecall(methodId, x));
  }
}
contract Addition is ResultStorage {
  function calculate(uint256 x) public returns (uint256) {
    uint256 temp = result + x;
    assert(temp >= result);
    result = temp;
    return result;
  }
}
contract Product is ResultStorage {
  function calculate(uint256 x) public returns (uint256) {
    if (x == 0) result = 0;
    else {
      uint256 temp = result * x;
      assert(temp / result == x);
      result = temp;
    }
    return result;
  }
}

其中,Calculator合约只有两个函数,即addproductCalculator合约并不知道如何执行相加或相乘运算,而是将相应的调用分别代理(delegate)给AdditionProduct合约。所有这些合约共享相同的状态变量结果,并存储每次计算的结果。下面在Turffle终端运行命令,查看运行情况:

truffle(develop)> Calculator.new().then(i => calculator = i)
truffle(develop)> calculator.addition().then(a => additionAddress=a)
truffle(develop)> addition = Addition.at(additionAddress)
truffle(develop)> calculator.product().then(a => productAddress = a)
truffle(develop)> product = Product.at(productAddress)
truffle(develop)> calculator.add(5)
truffle(develop)> calculator.result().then(r => r.toString()) // 5
truffle(develop)> addition.result().then(r => r.toString())   // 0
truffle(develop)> product.result().then(r => r.toString())    // 0
truffle(develop)> calculator.mul(2)
truffle(develop)> calculator.result().then(r => r.toString()) // 10
truffle(develop)> addition.result().then(r => r.toString())   // 0
truffle(develop)> product.result().then(r => r.toString())    // 0

可以确认,我们使用了Calculator合约的存储。此外,所执行的代码存储在Product合约和Addition合约中。

对于call函数,同样存在Solidity汇编操作码版本的delegatecall。下面给出Delegator合约的代码,注意其中对delegatecall的调用方式:

contract Implementation {
  event ImplementationLog(uint256 gas);
function() public payable {
    emit ImplementationLog(gasleft());
    assert(false);
  }
}
contract Delegator {
  event DelegatorLog(uint256 gas);
  Implementation public implementation;
function Delegator() public {
    implementation = new Implementation();
  }
function () public payable {
    emit DelegatorLog(gasleft());
    address _impl = implementation;
assembly {
     let ptr := mload(0x40)
     calldatacopy(ptr, 0, calldatasize)
     let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
    }

    emit DelegatorLog(gasleft());
  }
}

这里,我们使用了内联汇编去执行delegatecall。你可能注意到,其中没有值参数,这是因为msg.value不会发生更改。你可能会有疑问,为什么这里我们加载的是0x40地址?calldatacopycalldatasize是什么?我们将在本系列的下一篇文章中详细介绍。下面,我们可以在Truffle终端中运行同上的命令,验证合约的行为。

再次强调,重要的是要清楚理解delegatecall的工作机制。每此触发的调用将被从当前合约发送,而非代理调用的合约。此外,所执行代码可以读写调用者合约的存储。如果合约并未正确地实现,甚至是存在非常小的错误,都可能导致上百万美元的损失。下文列出了以太坊历史上一些最严重的错误:

HackPedia: 16 Solidity Hacks/Vulnerabilities, their Fixes and Real World Examples

该文列举了Solidity被破解和漏洞的完全列表、修复情况,以及一些真实世界的破解实例。

数据管理(Data Management)

EVM根据不同的应用场景,采用不同的方式管理不同类型的数据。除了合约代码之外,合约所管理的数据大体可分为四类:栈(Stack)、调用数据(calldata)、内存和存储。

栈(tack)

EVM本身就是一种栈机器。也就是说,EVM的操作并非基于注册函数,而是基于虚拟栈。栈的最大规模是1024,其中栈条目(item)的大小是256比特。事实上,EVM是一种256比特的字(word)机器,这种设计便于Keccak256哈希模式和椭圆曲线密码(elliptic-curve)的计算。下图给出了大部分操作码输入参数的来源。

EVM提供了多种操作码,用于直接操作栈。其中包括:

  • POP:从栈中移除条目。
  • PUSHn:将后面n个字节条目置于栈中,n的大小介于1到32之间。
  • DUPn:复制第n个栈条目,n的大小介于1到32之间。
  • SWAPn:交换栈中第一个和第n个条目的位置,n的大小介于1到32之间。

调用数据(Calldata)

调用数据是只读的字节地址编码空间,用于存储交易或调用的数据参数。不同于栈,要使用调用数据,必须要准确地指定字节偏移量和要读取的字节数。

调用数据的EVM操作码包括:

  • CALLDATASIZE:指定交易数据的规模。
  • CALLDATALOAD:加载32字节的交易数据到栈中。
  • CALLDATACOPY:拷贝交易数据的指定字节数到内存中。

Solidity为上述操作码提供了内联编译版本,分别是calldatasizecalldataloadcalldatacopy。其中,calldatacopy需要指定三个参数(t, f, s),将f地址处的调用数据拷贝s个字节到t地址。此外,Solidity支持通过msg.data访问调用数据。

你可能注意到,我们在以前文章的一些例子中使用了部分操作码。下面,我们再看一下delegatecall的内联汇编代码块:

assembly {
  let ptr := mload(0x40)
  calldatacopy(ptr, 0, calldatasize)
  let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
}

要将调用代理给_impl地址,我们必须提交msg.data。鉴于delegatecall操作码在内存中操作数据,我们首先需要将调用数据拷贝到内存中。这里,我们使用calldatacopy,将所有的调用数据拷贝到指定内存指针处。注意,我们使用了calldatasize

下面,我们看一下另一个使用调用数据的例子。在练习三的目录中,可以看到Calldata合约的代码如下:

contract Calldata {
function add(uint256 _a, uint256 _b) public view 
  returns (uint256 result) 
  {
    assembly {
      let a := mload(0x40)
      let b := add(a, 32)
      calldatacopy(a, 4, 32)
      calldatacopy(b, add(4, 32), 32)
      result := add(mload(a), mload(b))
    }
  }
}

上面的代码将返回由参数传递而来的两个数字的相加运算结果。注意,这里我们再一次加载了从0x40处读取的内存指针,原因将在本文稍后给出解释。我们在变量a中存储内存指针,并在变量b中存储a的32个字节之后的位置。然后我们使用calldatacopy将首个参数存储在a中。你应该注意到,数据是从调用数据的第四个位置处拷贝的,而非首个位置处。这是因为调用数据的头四个字节保存了被调用函数的签名,这里是bytes4(keccak256("add(uint256,uint256)"))。这是EVM用于识别调用时需要的函数。然后,我们存储b中第二个参数,拷贝调用数据随后32个字节。最后,我们只需要计算加载在内存中两个值的和。

在Truffle终端中运行下面命令,测试运行结果:

truffle(develop)> compile
truffle(develop)> Calldata.new().then(i => calldata = i)
truffle(develop)> calldata.add(1, 6).then(r => r.toString())    // 7

内存(Memory)

内存是一种易失性、字节可寻址的读写空间,用于在合约执行期间存储数据,主要是将参数传递给内部函数。鉴于内存是易失性区域,因此在每次消息调用开始,都要执行清除内存操作。所有位置的最初定义为零。对于调用数据,内存可采用字节级别寻址,但一次只能读取32字节。

一旦一个字写入了一块以前从未使用的内存,我们就称之为内存“扩展”了。除了内存写入需要一定代价外,内存扩展也是有代价的,前724个字节的扩展代价是线性的,之后的扩展代价呈二次方增长。

EVM提供了三个操作内存区域的操作码:

  • MLOAD:将字从内存加载到栈中。
  • MSTORE:将字保存到内存中。
  • MSTORE8:将一个字节保存到内存中。

Solidity同样对这些操作码提供了相应的内联汇编版本。

关于内存,我们还需要了解另一个关键点。Solidity总是在位置“0x40”处存储空闲内存指针,即对存储器中第一个未使用的字的引用。这就是为什么我们在操作内联汇编时需要加载这个字。这是因为头64字节内存是为EVM保留的,这样可以确保不会覆盖Solidity内部使用的内存。例如,在上面给出的delegatecall例子中,我们加载此指针,存储给定的调用数据,实现数据转发。这是因为内联汇编操作码delegatecall需要从内存中获取其负载。

此外,如果查看Solidity编译器的字节码输出,那么我们就会发现所有字节码是以0x6060604052…开始的,这表示了:

PUSH1   :  EVM操作码0x60
0x60    :  自由内存指针。
PUSH1   :  EVM操作码0x60。
0x40    :  自由内存指针的内存位置。
MSTORE  :  EVM操作码0x52。

在汇编层级操作内存必须要谨慎,因为存在覆盖保留区域的风险。

存储(Storage)

存储是一种持久的、字可寻址的读写空间,是合约存储其中持久信息的地方。不同于内存,存储是持久性区域,只能使用字作为地址。它是2²⁵⁶个槽的键值映射,其中每个槽32字节。除了合约自身的存储,合约既不能读取也不能写入其它任何存储。所有位置最初定义为零。

在EVM的所有操作中,将数据保存到存储是需GAS数量最高的操作之一。这笔费用并非一成不变的。将存储槽从零值修改为非零值需要2万个GAS。存储相同的非零值或将非零值设置为零时需要5千个GAS。但是,对于后一种应用场景,即将非零值设置为零时,会提供15000个GAS的返还款。

EVM提供了两个操作存储的操作:

  • SLOAD:将字从存储加载到栈中。
  • SSTORE:将字保存到存储。

同样,Solidity内联编译也支持这些操作码。

Solidity自动将合约中每个已定义的状态变量映射到存储的相应插槽中。映射策略非常简单:固定大小的变量(即除映射和动态数组之外的所有变量)从存储的位置0开始连续布局。

对于动态数组,p槽位存储数据长度,数组数据将由p哈希(即keccak256(p))确定槽位数。

对于映射,不使用槽位,对应于键k的值由keccak256(k,p)定位。谨记,keccak256 的参数(kp)总是填充为32字节。

为解释其中的工作机制,我们分析下面给出代码例子。在练习三合约目录中,提供了Storage合约的代码如下:

contract Storage {
uint256 public number;
  address public account;
  uint256[] private array;
  mapping(uint256 => uint256) private map;
function Storage() public {
    number = 2;
    account = this;
    array.push(10);
    array.push(100);
    map[1] = 9;
    map[2] = 10;
  }
}

打开一个Truffle终端,测试合约的存储结构。首先,编译并创建一个新的合约实例: Now, let’s open a truffle console to test its storage structure. First, we will compile and create a new contract instance:

truffle(develop)> compile
truffle(develop)> Storage.new().then(i => storage = i)

确保地址0保存数值2,地址1保存了合约的地址:

truffle(develop)> web3.eth.getStorageAt(storage.address, 0)  // 0x02
truffle(develop)> web3.eth.getStorageAt(storage.address, 1)  // 0x..

检查存储位置2保存了数组长度:

truffle(develop)> web3.eth.getStorageAt(storage.address, 2)  // 0x02

最后,检查存储位置3是未使用的,映射值的存储方式如我们上面所介绍:

truffle(develop)> web3.eth.getStorageAt(storage.address, 3) 
// 0x00
truffle(develop)> mapIndex = ‘0000000000000000000000000000000000000000000000000000000000000003’
truffle(develop)> firstKey = ‘0000000000000000000000000000000000000000000000000000000000000001’
truffle(develop)> firstPosition = web3.sha3(firstKey + mapIndex, { encoding: ‘hex’ })
truffle(develop)> web3.eth.getStorageAt(storage.address, firstPosition)
// 0x09
truffle(develop)> secondKey = ‘0000000000000000000000000000000000000000000000000000000000000002’
truffle(develop)> secondPosition = web3.sha3(secondKey + mapIndex, { encoding: ‘hex’ })
truffle(develop)> web3.eth.getStorageAt(storage.address, secondPosition)
// 0x0A

很好,上面演示了Solidity存储策略,正如我们所理解的!要了解更多Solidity是如何映射状态变量到存储中,可参阅官方文档.

希望本文有助于大家更好地理解EVM在以太坊架构中的功能。

¹ 以太坊黄皮书中提出,“新账户地址定义为:对仅包含发送者和帐户nounce的结构做RLP编码,对所得到编码求Keccak哈希值,取该哈希值最右边开始的160位。”

² zeppelin_os的支柱之一就是合同的可升级性。 Zeppelin一直在探索实施这一目标的不同策略。 在这里阅读更多相关信息。

³ Solidity在调外部函数前,会先验证该函数的地址中是否具有字节码。否则,终止函数执行并还原状态。

作者简介

Vaibhav Saini是一家由MIT Cambridge 创新中心孵化的初创企业TowardsBlockchain的联合创始人。Saini也是一名高级区块链开发人员,具有Ethereum、Quorum、EOS、Nano、Hashgraph、IOTA等多种区块链平台的开发经验。他目前是德里印度理工学院(IIT Delhi)的一名大二学生。

《深入EVM:EVM工作机制的深层分析(终极版)》上有2条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注