智能合约是运行在区块链公链上的一种代码,该代码由Solidity编写,并通过区块链的智能合约虚拟机来执行,以达到对区块链编程的目标。可以将区块链公联理解为操作系统,Solidity是编写该操作系统应用程序的编程语言,智能合约虚拟机则是编程语言编译之后的代码运行环境。
区块链通常被理解为超级账本,账户与账户之间可以通过交易来完成转账,只是这种转账方式与传统的银行转账有很大的不同。
所有的交易信息都会形成一个结构化的账本,它们会被区块链的节点(矿工)按照一定的方式和时间间隔组织起来,存储在区块链节点中。这个用于存储交易信息的结构体就是区块,除了交易信息,区块还要存储一些额外的信息以保证交易数据的完整性和可靠性,区块数据结构如下:
不同的公链有各自不同的涉及,关于区块的生产间隔,不同的区块链网络有不同的设定,比如以太坊出块的时间间隔约15秒,而比特币网络则需要10min才生成一个区块。
区块数据包含区块头与区块体,区块体存储具体的交易及交易相关的原始数据,区块头存储的是原始数据的Hash信息,任何对原始数据的修改,都会引起区块头Hash值的变化,这样对任何信息的篡改都很容易被察觉到并被验证为假数据。
区块的作用就是将不同时间阶段内的交易数据按照一定的格式和数量,打包成结构化数据,方便存储和管理。只有被打包到区块中并且被全公链网络认可的交易,才算真正的有效交易。
区块头和区块体数据也会被当作输入数据做一次Hash运算,其运算结果会被存储在下一个区块的区块头中,这样任何区块内容的修改都会反映到区块的Hash值上,而区块的Hash值又是下一个区块的输入数据,它又会被当作新区块的数据参与一次新区块的Hash运算,随着时间的推移和交易量的增加,所有的区块会通过保存前一个区块的Hash运算结果的方式组成一条链。
==将交易打包成数据块,再将数据块以Hash值的方式组织成链式结构,这就是区块链定义的来源。==由分布在全世界的节点组成,任何人都可以公开查询,但又无法任意修改。
区块被增加到区块链之前,并不是所有区块都可以生成区块数据,也不是所有区块数据都能被增加到区块链成为最新的数据,这个过程有一定的门槛,需要筛选出一个值得信任的节点来生成数据,然后由其他节点来验证其生成数据的有效性。这个生产区块的过程会得到数字货币的激励,因此很多节点会加入生产区块的竞争。如果某个节点生产的区块数据得到了其他节点的验证,则其他节点会将最新的区块存储到本地,然后加入下一个数据块的生产竞争,这个过程被称为挖矿,而生成数据的节点被称为矿工。
需要注意的是,在矿工将交易打包成区块之后,还需要查找一个数字n,这个数字满足不等式:
n < = 2 256 / H d n <= {2^{256}}/{H_d} n<=2256/Hd
计算有效数字n的过程是需要付出算力的,而这个算力付出的过程是值得信赖的,这就是区块链为何可以在没有中心管理者的情况下正常运作,且任何人都可以在无须信任的情况下参与区块链业务。
成为出块人就可以成功拿到奖励,奖励分为系统的奖励和交易中的交易手续费,在每一笔交易数据中,转账人都可以手动设置手续费,这些手续费用于奖励矿工打包的工作。在转账时设置的手续费越高,转账时间就越短,转账速度就越快。
在挖矿的过程中,矿工需要付出算力来查找一个满足条件的数字,这种算力的付出是无法伪造的,必须付出相应的算力和电力之后才能得到正确的数字。当某一个节点发现该数字之后,其他节点可以很快验证该数字的有效性,验证并不用花费太多的算力和能源。这种为了持续生成区块而被所有网络节点认可的方案就叫做共识算法,而付出算力来证明自己工作的共识算法被称作PoW(Proof of Work)。
目前比较流行的共识算法由DPoS、BFT和PoST等:
因为整个区块链系统是点对点的对等网络,没有统一的中心机构协调各个节点的行为,所以在生成区块时,各个节点的行为都是相互独立的,很有可能同时由多个矿工在同一区块高度生成出2个以上的区块来。这些区块打包的交易很可能是不一样的,同时满足条件的数字n不是唯一的,多个矿工之间生成的数字n是不一样的,但是同样是满足不等式的。在这种情况下,网络中的其他节点很可能同步到不同的区块数据,并且这些数据在数学上都是合法的、有效的。当不同的节点中的不同的区块作为当前最新区块时,就会存在分叉的情况,即不同的矿机对同一高度的区块生产了内存不一样的新区快,并且这些矿工都找到了满足不等式的数字n。
区块链对于交易的组织方式及账户的管理方式决定了只有私钥持有者才有权修改本账户下的数字货币信息。但是并不意味着区块链是绝对安全的,由于区块链网络是无主的,每个持有数字货币的账户和区块打包的矿工都无须审批、无须信任,随时随地可以参与,因此,只要拥有足够的算力就可以对整个网络发起攻击,比如著名的51%算力攻击。除了算力攻击,还有仅属于区块链点对点网络特有的攻击,比如女巫攻击、日食攻击等。
除了拥有上述区块链的基本功能,以太坊还在比特币网络的基础上增加了以太坊智能合约虚拟机,即EVM。在增加了EVM之后,以太坊就成了一个可编程的去中心化平台,任何系统开发者在支付一定的部署费用之后,就可以拥有一套完全去中心化的业务系统,这样的业务系统被称为DAPP。
首先,以太坊可以视为大型的状态机,由分布在全球的以太坊节点来运行这个状态机,每一次交易的产生都会修改状态机的状态,将交易打包成区块之后,可以以区块为单位来衡量状态的迁移。
以太坊的状态是由以太坊上的所有账户组成的。也就是说,以太坊上的所有账户组成了以太坊的全局状态,以太坊账户的地址与账户状态的一个映射结构。以太坊账户又分为两种:一种是外部账户,也就是用于存放用户余额和转账的账户;另一种是智能合约账户,是在部署智能合约时生成的一个关于该智能合约的区块链地址及其状态的映射关系。
以太坊上的账户之间会有系统调用:
在以太坊之前的公链项目中,如果需要修改某条公链的某些特征或者增加对某种场景的支持,开发者必须在原有的公链设计的基础上修改系统底层源代码,并重新维护一套公链生态,这往往被称为硬分叉。
而以太坊EVM的出现,使得任何需要实现某一行业具体逻辑的开发者,无须复制以太坊的整套代码,然后修改出符合自己逻辑的公链,而是基于以太坊现有的公链网络和矿机组织、共识社区,通过EVM提供的API来编写智能合约,就可以完成一套区块链系统,一套满足自己业务需求的系统,该系统具有区块链所有的通行特征:去中心化、公开透明、无法篡改等。
因此,EVM的出现使得对区块链编程成为可能,具有EVM的以太坊公链技术可以视为是对原有区块链技术的一次重大革新。
EVM作为一个离线的、独立的运行环境,它无法访问外界的文件系统、网络接口等资源,目前EVM的指令中尚未支持对这些资源的操作。当编写的智能合约被编译成EVM能够理解的代码之后,EVM会在自己独立的运行环境中执行用户编写的智能合约程序。
通过智能合约编译器,可以将智能合约编译成虚拟机能够理解的指令,然后通过以太坊账户发起一个交易,将这些指令部署到区块链上,部署成功后会得到一个以太坊的地址,这个地址指明了该合约代码存放的位置。
当合约被调用时,可以根据合约的地址找到指令集的存储位置,虚拟机通过程序指令计数器记录当前指令的执行位置,随着程序的执行,指令计数器也会随之变化,它存储了下一条需要执行的指令的地址(偏移地址)。在指令中会有JUMP之类的跳转指令,因此指令计数器的数值并非总按顺序逐渐增加。
EVM的指令执行不在寄存器中执行,而是在一个被称作栈的内存空间中进行。这个栈最多可以容纳1024条栈指令,每条栈指令可以达到256位,也就是说EVM是256位的计算机,这个长度的指令特别适合Keccak-256 Hash数据及椭圆函数运算。
智能合约的指令中包含了操作码和操作数,EVM加载到栈中之后,根据操作码运算,在执行操作码的过程中,会产生中间状态的临时数据,这些数据会被存放在栈空间上、内存中或者持久存储的空间中。指令中也有从内存或者持久存储空间中读取数据的操作,有些操作需要在区块链状态的基础上进行下一步的操作,这些状态信息就存储在持久存储空间上;有些操作是存储的运算,这些数据一般通过内存进行读写。使用内存和持久存储空间需要支付一定的GAS费用。GAS指的是EVM执行指令时消耗的代价,这个代价以以太坊的数字货币ETH来表示。
1996年,Nick Szabo在文章《Smart Contracts: Building Blocks For Digital Markets》中提出了智能合约的概念。==所谓“合约”,就是条文、合同一类的东西,里面记录了发生的条件与对应执行的条款,以支持确权等操作;所谓"智能",就意味着自动化、可编程。==所以,智能合约就是可编程的合同,也可以理解为一段自动执行的条文合同,在计算机中,就是一段自动执行的程序片段。它更易于合约保存,并且由确定的算法运行,给定输入,就得到对应的输出,极大保障了合约的执行力。
以自动售货机做类比,可以帮助我们更好地理解智能合约的核心特征。当使用者选择好要购买的货物并完成支付,出货逻辑就会被触发,用户就能得到想要的货物,而这个过程不需要人工介入,节省了售卖货物的人力成本。如果要破坏这个合约,就得物理破坏售卖机。像POS刷卡机、EDI(电子数据交换)等,也可作此种类比。
以太坊公链是操作系统,EVM是区块链代码的运行环境,而Solidity则是区块链的编程语言,通过编程语言编写的逻辑模块被称为智能合约。智能合约产生价值的最基本前提是有一个强有力的底层介质用于储存,让其不可被物理破坏。
以太坊与比特币最大的不同在于可通过智能合约执行复杂的逻辑操作。在以太坊上,智能合约的语言是Solidity,它是图灵完备且较为上层的语言,极大地扩展了智能合约的能力范畴,降低了智能合约编写难度。
智能合约在区块链上的可执行代码是一种类似汇编语言的指令集,这些指令集通过EVM的解释和执行,对区块链的状态进行读写,实现合约规定的业务逻辑。因此通过Solidity这种高级编程语言,加上Solidity编译器,可以将高级语言编译成汇编指令集码,再将其部署到区块链上执行。
账户发起创建合约交易时,以太坊交易中会加载合约创建代码,矿工在打包交易时会执行该合约的初始化代码,并生成智能合约对应的EVM代码和该合约对应的账户地址,当该交易所在的区块被成功打包并同步到其他节点时,其他节点就可以通过消息调用来访问该合约对外开放的接口和功能。
从编程角度而言,智能合约就是一段代码。相比常规代码,智能合约具有许多差别与限制,例如:
这些特点使得目前智能合约生态以链上资源的治理为核心。就像以太坊上各式各样的ERC标准与治理方案;EOS上有各种资源模型,比如CPU、RAM、兼经济模型、Rex、Bancor协议等。
显然,就目前的生态而言,智能合约对现实世界的影响力有限。但事物总是在发展的。目前,已有许多致力于突破这些限制的研究,典型的有Oracle(谕言机,但常被称为预言机),它允许智能合约和链外进行交互,这样就能大大提高智能合约的使用场景,彷佛一台电脑通上了网;再比如那些突破链自身性能瓶颈的尝试,例如支付通道、跨链、plasma、rollup,它们都从不同角度打破安全与性能的枷锁。
毋庸置疑,智能合约将扮演着越来越重要的角色,将来随着以太坊2.0的落地,也许会开启新一个区块链时代。
以太坊采用了Solidity作为智能合约语言,Solidity 是一门为实现智能合约而创建的高级编程语言,能在允许以太坊程序的节点上运行。该语言吸收了C++、JavaScript的一些特性,例如它是静态类型语言,支持继承、库等。
除了Solidity,每个平台的智能合约技术也有所不同,接下来将从公有链、联盟链作为切入,介绍其他平台所采用的技术。
公有链
首先,不妨先认识三大公链的智能合约技术。
链 | 合约语言 | 底层技术 | 图灵完整 | 备注 |
---|---|---|---|---|
比特币 | 脚本语言 | 脚本语言 | 否 | 未被官宣为智能合约,但具备某些特征 |
以太坊 | Solidity | EVM | 是 | 是最活跃的合约语言之一 |
EOS | C++/… | WASM | 是 | WASM是底层字节码,理论支持多种上级语言 |
联盟链
除了公链,联盟链也是重要的区块链类型。比之公链,联盟链共识的复杂度被大大缩减,因此具有更高的执行效率。
联盟链受企业级机构青睐,一般而言,相关机构之间会形成联盟,通过联盟链来共享数据。联盟链可覆盖供应链金融、司法存证、溯源等多种场景,未来还会与IOT、AI等技术结合。
在当今联盟链生态中,除去采用chaincode的Fabric,大部分平台都采用Solidity作为智能合约语言,FISCO BCOS即是如此。
当前,Solidity可谓占据了智能合约的C位,掌握Solidity是学习智能合约和区块链的重要一环。后面系列也将对如何用Solidity编写、运行以及测试智能合约作深入介绍解析。
除了Solidity,WebAssembly、Libra的Move等一些智能合约语言也在发展中,可以保持关注。
一套完整的区块链DAPP,除智能合约这些可以查询和改变区块链状态的代码外,还需要用户操作界面及连接用户操作与智能合约代码的接口。
首先,用户通过Web界面或者收集App将操作数据发送到一个传统的业务服务器,该业务服务器时传统互联网中心化的服务器,但是与传统系统不同的是,该系统没有像传统互联网设计那样将数据放入中心化的数据库存储,而是通过一个Web3.0接口,将数据传送到以太坊区块链公链上。
该接口是一个JSON RPC协议,该协议由很多代码实现。目前最流行的是运行在Web容器中的Web.js3模块。Solidity编程语言经过编译之后,除了交易需要的合约初始化代码之外,还有ABI接口等描述文件,Web3.js通过这些描述文件,可以构建与以太坊智能合约虚拟机进行通信的模块,通过JS代码将用户的操作数据传入以太坊公链上的合约地址,智能合约虚拟机会根据函数签名和加载的函数参数,在虚拟机内执行编译成EVM Code的智能合约。
如果涉及区块链数据的读取,则虚拟机会读取区块链上的区块数据。如果虚拟机的指令代码修改以太坊公链的状态,那么通过调用相关的状态机指令,并消耗一定的GAS之后,就可以将修改操作提交到以太坊区块链公链网络中,这些操作往往以交易的方式体现。
在虚拟机执行任务结束后,其对区块链状态的修改会被矿工打包。当状态修改被全网共识时,虚拟机对公链网络的状态修改也相应成功。可以通过查询相关的执行结果,将执行状态返回给用户交互系统,这样终端用户就可以通过交互系统查看DAPP操作的执行结果。
prama solidity >=0.6.4;
contract SimpleStorage { //使用关键字contract定义合约结构体
uint storedData; //定义一个成员变量
function set(uint x) public { //定义关于该变量的设置函数
storedData = x;
}
function get() public view returns (uint) { //定义关于该变量的读取函数
return storedData;
}
}
这段Solidity代码的功能是存取storedData字段。该字段被称为“状态变量”,会由区块链持久存储。
用户可以将这段代码部署在以太坊或类似的区块链上,部署成功就意味着该智能合约不可再被修改,只要底层区块链不被销毁,这段合约就一直存在。任何人都可通过“合约地址”来调用该合约接口,每次调用信息都会被记录在链上。
在解释这段代码如何运行之前,先回顾下传统java程序的运行方式。首先,用户编译完java代码后,会以字节码的形式保存在磁盘上;然后用户会调用程序,这由JVM来托管执行;程序执行期间可能会通过日志来记录调用参数,也可能会和磁盘进行IO。
Solidity的执行与此类似。不同的是介质由硬盘换成了区块链,由单机变为分布式。 代码部署后,以字节码的形式存储在每一个节点上。当用户要求调用某个函数时,调用请求将会被囊括在交易中,并被打包到某个区块上,一旦全网对该区块形成共识,就意味着调用是合法的。
接下来,EVM会来调用字节码,它负责存取底层的状态变量,好比传统编程的IO。
光从代码来看,合约开发似乎不过如此,单个合约只需要围绕着字段进行操作,对于很多简单业务而言,不过是CRUD而已。但其复杂性也恰恰在于此,合约在区块链环境上执行,是不可修改的。所以如果出现了bug,就必须部署新的合约,这对于合约的可维护性提出了挑战。并且,一旦业务复杂起来,容易出现安全漏洞,导致链上资产损失。同时,还要考虑完成代码编写、逻辑执行、数据存储的成本问题。 综上所述,写合约不难,但写好合约,却需要一定功底。
本示例是一个简单的数字货币的智能合约。
01 pragma solidity >=0.6.4;
02
03 contract Coin {
04 address public minter; //定义一个地址类型的变量
05 // 定义一个地址到余额数量的映射
06 mapping (address => uint) public balances;
07 // 声明一个记录日志的事件
08 event Sent(address from, address to, uint amount);
09 constructor() public{ // 合约的构造函数
10 minter = msg.sender;
11 }
12 //定义修改余额数据结构的函数,修改方式是直接增加某个地址的余额值
13 function mint(address receiver, uint amount) public {
14 require(msg.sender==minter,"Inconsistent deployer.");
15 require(amount < 1e60,"Quantity out of bounds.");
16 balances[receiver] += amount;
17 }
18 //定义转移余额的函数
19 function send(address receiver, uint amount) public {
20 require(amount <= balances[msg.sender],"Insufficient balance.");
21 balances[msg.sender] -= amount;
22 balances[receiver] += amount;
23 emit Sent(msg.sender,receiver,amount);
24 }
25}
第4行代码定义了一个区块链地址类型(address)的变量,该变量为public类型,定义这个变量是为了记录铸造该合约的账户地址,该地址在合约函数构造函数中进行了赋值。
第6行代码定义了一个映射关系数据结构,这个结构定义于区块链地址下,关于该数字货币的余额,目前基于以太坊的数字货币,只要符合ERC20协议,很多第三方开源的数字货币钱包都可以显示数字货币的余额,其实这个余额就是该Coin合约中,balances变量存储的数值,不同的地址存储了不同的数值,在数字货币钱包中,通过查询以太坊公链下某一地址下的ERC20数字货币合约的balances变量,就可以显示数字货币余额。
第9~11行代码定义了该合约的构造函数,该构造函数仅仅在合约部署到以太坊公链时执行一次,合约部署成功之后就无法修改此合约的逻辑了,
第10行代码中的msg.sender表示的是包含该合约初始化代码的交易发起者的区块链地址。也就是说Coin这个合约的铸造者就是部署本合约的账户,该账户可以是外部账户,也可以是智能合约账户。
第13~17行代码是一个铸币函数,所谓的铸币函数,本质上就是修改balances这个数据结构的内容,因为该结构存储了各个区块链地址下的数字货币的余额,所以调用这个函数,需要检查调用者的区块链地址是否与智能合约初始化时合约的部署者一致,如果不一致则该函数执行失败,同时要检查本次铸币的数量不能高于1e60个,如果数字货币总量超过这个数字,铸币操作也会失败,如果以上条件都满足,那么就会在receiver这个账户中增加amount个数字货币。如果以上require的条件不满足,该合约调用失败并会导致以太坊状态机发生回滚,以太坊的状态会恢复到调用该函数之前。
第9~24行代码是一个转账函数,在检查调用者有充足的余额后,在调用者名下减少amount个智能合约代表的数字货币,同时在receive这个地址下增加相应数量的数字货币,在函数被成功执行之后,将本次执行时间emit一个事件,该事件产生log,并被永久的保存在以太坊区块链上。该事件需要首先在第8行代码进行声明,然后在后面的函数中调用。由本函数可知,所谓的数字货币转账实际上只是修改balances下存储的数据而已。