以太坊研究

开篇文章记录实习期间对以太坊相关技术的研究与学习进展。

整个技术概念和背景比较多,因此在记录的时候以工作/研究的来源作为分类依据,可能在部分内容上有所重复。

Intro

以太坊(Ethereum)是一个建立在区块链技术之上的去中心化应用平台。它允许任何人在平台中建立和使用通过区块链技术运行的去中心化应用。以太坊是区块链,区块链不是以太坊。


区块链基础

参考页面:

FISCO BCOS 技术文档

共识机制

区块链作为一个分布式系统,可以由不同的节点共同参与计算、共同见证交易的执行过程,并确认最终计算结果。协同这些松散耦合、互不信任的参与者达成信任关系,并保障一致性,持续性协作的过程,可以抽象为“共识”过程,所牵涉的算法和策略统称为共识机制。

节点

节点的标识采用公私钥机制,生成一串唯一的NodeID,以保证它在网络上的唯一性。

根据对计算的参与程度和数据的存量,节点可分为共识节点和观察节点。共识节点会参与到整个共识过程,作为记账者打包区块、作为验证者验证区块以完成共识过程。观察节点不参与共识,同步数据,进行验证并保存,但可以作为数据服务者提供服务。

共识算法

共识算法需要解决几个核心问题:

  1. 选出在整个系统中具有记账权的角色,做为leader发起一次记账。
  2. 参与者采用不可否认和不能篡改的算法,进行多层面验证后,采纳leader给出的记账。
  3. 通过数据同步和分布式一致性协作,保证所有参与者最终收到的结果都是一致且无错的。

区块链领域常见的共识算法有公链常用的工作量证明(Proof of Work)、权益证明(Proof of Stake)、委托权益证明(Delegated Proof of Stake),以及联盟链常用的实用性拜占庭容错共识PBFT(Practical Byzantine Fault Tolerance),Raft等


以太坊白皮书

参考页面:

官方白皮书

以太坊账户

以太坊通过“账户”来组成系统的状态,每个账户有一个20字节的地址,状态转换是指账户之间价值和信息的直接转移。一个以太坊账户包含四个字段:

  • nonce,用于确保每笔交易只能处理一次的计数器
  • 账户当前的以太币余额
  • 账户的合约代码(不一定有)
  • 账户的存储(默认空)

以太币是以太坊内部的主要加密燃料,用于支付交易费。 通常有两类账户:由私钥控制的外部账户以及由其合约代码控制的合约账户。 外部账户没有代码,持有者可以通过创建和签署交易从外部账户发送消息;在合约账户中,每次合约账户收到消息时,其代码都会激活,允许该账户读取和写入内部存储,继而发送其他消息或创建合约。

这里面的合约是存在于以太坊执行环境中的”自治代理“。当被交易或消息“触发”时,合约总是执行特定的代码段,并直接控制自已的以太币余额和键/值存储,以跟踪永久变量。

消息和交易

  1. 在以太坊中,术语“交易”用来指代已签名的数据包,数据包存储着将要从外部账户发送的消息。 交易包含如下参数:

    • 消息接收者

    • 用于识别发送者身份的签名

    • 从发送者转账到接收者的以太币金额

    • 一个可选数据字段

    • STARTGAS 值,表示允许交易运行的最大计算步骤数

    • GASPRICE 值,表示发送者每个计算步骤支付的费用

    STARTGASGASPRICE 字段对于以太坊的反拒绝服务模型很重要,前者可以防止代码中出现无意或者恶意的无限循环或其他计算浪费,后者则会要求攻击者支付其攻击所消耗的资源对应的费用。

  2. 合约能够向其他合约发送“消息”。 消息是从未序列化的虚拟对象,只存在于以太坊执行环境中。 消息包含如下参数:

    • 消息发送者(隐含的)
    • 消息接收者
    • 随消息一起转账的以太币金额
    • 一个可选数据字段
    • STARTGAS

以太坊转换函数

以太坊状态转换

以太坊状态转换函数APPLY(S,TX) -> S'可如下定义:

  1. 检查交易格式是否正确、签名是否有效以及Nonce值是否与发送者账户的Nonce值匹配;若否,返回错误
  2. 通过STARTGAS*GASPRICE计算出交易费用,并从签名中确认发送地址。从发送者的账户余额中减去费用,并增加发送者的Nonce值;若账户余额不足,则返回错误
  3. 初始化GAS=STARTGAS,并根据交易中的字节数量为每个字节扣除相应数量的燃料
  4. 将交易数值从发送者账户转移至接收账户;若接收账户不存在,则创建该账户;若接收账户是合约,则运行该合约的代码,直到代码执行完毕或者燃料耗尽
  5. 若由于发送者余额不足或代码运行耗尽了燃料,而导致转账失败,则回滚支付费用之外的所有状态变化,并将费用支付给矿工账户
  6. 若一切正常,则将剩余燃料的费用退还给发送者,并将为所消耗燃料而支付的费用发送给矿工

Example:

假设合约的存储一开始为空,发送了一个价值为10个以太币的交易,消耗2000份燃料,燃料单价为0.001个以太币,数据包含64个字节,字节0-31表示数字2,字节32-63表示字符串CHARLIE,则状态转换函数的执行过程如下:

  1. 检查交易是否有效、格式是否正确
  2. 检查交易发起者是否至少有2000*0.001=2个以太币,若有,则从发送者账户中扣除2个以太币
  3. 初始化燃料=2000份,假设交易长度为170个字节,每个字节消耗5份燃料,减去850份燃料,剩余1150份燃料
  4. 从发送者账户减去10个以太币并增加到合约账户
  5. 运行合约代码,假设运行代码消耗187份燃料,则剩余燃料为963份
  6. 向发送者账户增加963*0.001=0.963个以太币,并返回产生的状态

相关技术文档

参考页面:

官方开发文档

区块

概念

区块是一批交易的组合,多个区块连接在一起形成区块链,并且每个区块包含上一个区块的哈希,这样可以有效地防止欺诈行为,因为当某个区块被篡改后,其后面的所有区块都会无效。

区块的存在是为了确保以太坊网络中的所有参与者保持同步状态并就交易的确切历史达成共识。交易和哈希被存放在一个区块内,并通过间歇提交的方式给所有参与者足够的时间来达成共识。

工作方式

为了维护交易历史,每个区块都被严格排序(创建的每个新区块都包含一个其父块的引用),区块内部保存的交易也严格排序;某位验证者在网络上构建完区块后,区块将传播到整个网络,所有的节点都会将该区块添加到其区块链的末尾,然后挑选新的验证者来创建下一个区块。

数据结构

一个区块分为区块头和区块体两个部分。

区块头包含的字段信息如下:

1
2
3
4
slot:区块所属的时隙
proposer_index:提出区块的验证者的ID
parent_root:父区块的哈希值
state_root:状态对象的根哈希

区块体包含的字段信息如下:

1
2
3
4
5
6
7
8
9
10
randao_reveal:用于选择下一个区块提议者的值
eth1_data:有关存款合约的信息
graffiti:用于标记区块的任意数据
proposer_slashings:将要受到惩罚的验证者的列表
attester_slashings:将要受到惩罚的验证者的列表
attestations:支持当前区块的认证列表
deposits:存入存款合约中的新存款的列表
voluntary_exits:将要退出网络的验证者的列表
sync_aggregate:用于服务轻客户端的验证者子集
execution_payload:由执行客户端传送的交易

以太坊虚拟机EVM

以太坊虚拟机是所有以太坊帐户和智能合约依存的环境。 当智能合约被编译成二进制文件后,被部署到以太坊上。用户通过调用智能合约的接口,来触发智能合约的执行操作。EVM执行智能合约的代码,修改当前以太坊网络上的数据(状态)。被修改的数据,会通过共识,确保一致性。

参考页面:

以太坊虚拟机图解

FISCO BCOS技术文档

EVM与节点的交互,抽象出EVMC接口标准(EVM Connector API)。EVMC主要定义了两种调用的接口:

  • Instance接口:节点调用EVM的接口,定义了节点对虚拟机的操作,包括创建、销毁、设置等
  • Callback接口:EVM回调节点的接口,定义了EVM对节点的操作,主要是对状态的读写、区块信息的读写等

EVM本身不保存状态数据,节点通过instance接口操作EVM,EVM反过来,调Callback接口,对节点的状态进行操作。

EVMC接口

节点和客户端

以太坊是一个由计算机组成的分布式网络,这些计算机即为节点;可验证区块和交易数据的软件在这些节点上运行,即成为客户端。

合并后的以太坊由两部分组成:执行层和共识层。这两层网络是由不同的客户端软件运行的。

  • 执行客户端:侦听网络中广播的新交易,在EVM中执行它们,并保存所有当前以太坊数据的最新状态和数据库。
  • 共识客户端:实现了权益证明共识算法,使网络能够根据来自执行客户端的经验证数据达成一致。

共识机制

以太坊采用的是权益证明的共识算法。

权益证明

  • 验证节点必须向存款合约中质押 32 个以太币,作为抵押品防止发生不良行为
  • 在每个时隙(12 秒的时间间隔)中,会随机选择一个验证者作为区块提议者。 他们将交易打包并执行,然后确定一个新的“状态”。 他们将这些信息包装到一个区块中并传送给其他验证者。
  • 其他获悉新区块的验证者再次执行区块中包含的交易,确定他们同意对全局状态提出的修改。 假设该区块是有效的,验证者就将该区块添加进各自的数据库。
  • 如果验证者获悉在同一时隙内有两个冲突区块,他们会使用自己的分叉选择算法选择获得最多质押以太币支持的那一个区块。

智能合约

智能合约是一个运行在以太坊链上的程序,其本身也是一个合法账户,可以成为交易的对象。但无法被人操控,个人用户可以通过提交交易来执行智能合约的某一个函数与其进行交互,且该交互是不可逆的,即无法被删除或回滚。

任何人都可以编写智能合约并部署到区块链网络上,只需要有足够的以太币即可。

智能合约语言

主要用的是Solidity和Vyper,较为主流的是Solidity语言,以下是Solidity实现一个合约的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.7.0;

contract Coin {
// The keyword "public" makes variables
// accessible from other contracts
address public minter;
mapping (address => uint) public balances;

// Events allow clients to react to specific
// contract changes you declare
event Sent(address from, address to, uint amount);

// Constructor code is only run when the contract
// is created
constructor() {
minter = msg.sender;
}

// Sends an amount of newly created coins to an address
// Can only be called by the contract creator
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
require(amount < 1e60);
balances[receiver] += amount;
}

// Sends an amount of existing coins
// from any caller to an address
function send(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "Insufficient balance.");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}
  • address public mintermapping (address => uint) public balances 分别定义了一个公共地址变量和一个公共映射变量,可以从其他合约访问它们。
  • event Sent 定义了一个事件,用于在某些特定情况下通知客户端。
  • constructor 函数是合约创建时运行的构造函数。
  • function mint 函数用于创建新的加密货币,并将其发送到指定的地址。只有合约创建者可以调用此函数。
  • function send 函数用于从任何调用者向指定地址发送现有的加密货币。
  • 两个函数中都使用了 require 语句进行条件检查,确保调用者有足够的余额或满足其他要求,否则函数将抛出异常。

智能合约结构

合约的任何数据都必须指定分配到一个位置:要么是存储,要么是内存。具体内容可参考另一篇关于Solidity的文章(Solidity笔记

  • 环境变量

    除了用户自定义的变量,还有一些特殊的全局变量,用于提供有关区块链或当前交易的信息,例如:

    • block.timestamp:uint256类型,当前区块的时间戳
    • msg.sender:address类型,当前调用消息的发送者
  • View函数

    View函数必须保证不会修改状态,以下操作被认为是修改状态:

    • 修改状态变量
    • 产生事件
    • 创建其他合约
    • 使用selfdestruct
    • 通过调用发送以太币
    • 调用任何未标记为view或者pure的函数
    • 使用底层调用
    • 使用包含某些操作码的内联程序组
  • 构造函数

    constructor 函数只在首次部署合约时执行一次。 与许多OOP语言中的 constructor 函数类似,状态变量会初始化到指定的值。

    1
    2
    3
    4
    5
    6
    7
    // 初始化合约数据,设置 `owner`为合约的创建者。
    constructor() public {
    // 所有智能合约依赖外部交易来触发其函数。
    // `msg` 是一个全局变量,包含了给定交易的相关数据,
    // 例如发送者的地址和交易中包含的 ETH 数量
    owner = msg.sender;
    }
  • 自定义函数

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    pragma solidity >=0.4.0 <=0.6.0

    contract ExampleDapp{
    string dapp_name;

    constructor() public{
    dapp_name = "Example Dapp";
    }

    function read_name() public view returns(string){
    return dapp_name;
    }

    function update_name(string value) public{
    dapp_name = value;
    }
    }

智能合约库

通过import+路径的方式导入其他合约或者包