深入浅出谈以太坊智能合约

原创 2017年09月13日 16:16:04

本文节选自图书《区块链开发指南》
区块链方向投稿,文章纠错,寻求报道可邮件联系 jiawd@csdn.net

什么是合约?

合约是代码(它的功能)和数据(它的状态)的集合,存在于以太坊区块链的特定地址。 合约账户能够在彼此之间传递信息,进行图灵完备的运算。合约依靠被称作以太坊虚拟机(EVM) 字节代码(以太坊特有的二进制格式)上的区块链运行。

合约很典型地用诸如Solidity等高级语言写成,然后编译成字节代码上传到区块链上。

也有其他语言可以用于编写智能合约如Serpent和LLL,在下一节会进一步阐述。去中心化应用开发资源列出了综合的开发环境,帮助你用这些语言开发的开发者工具,提供测试和部署支持等功能。

以太坊高级语言

合约依靠被称作以太坊虚拟机(EVM) 字节代码(以太坊特有的二进制格式)上的区块链运行。然而,合约是很典型地用诸如Solidity等高级语言写成的,它会用以太坊虚拟机编译器编译成字节代码上传到区块链。

下面是开发者可以用来为以太坊写智能合约的高级语言。

  1. Solidity
    Solidity是和JavaScript相似的语言,你可以用它来开发合约并编译成以太坊虚拟机字节代码。它目前是以太坊最受欢迎的语言。
  2. Serpent
    Serpent是和Python类似的语言,可以用于开发合约编译成以太坊虚拟机字节代码。它力求简洁, 将低级语言在效率方面的优点和编程风格的操作简易相结合,同时合约编程增加了独特的领域特定功能。Serpent用LLL编译。
  3. LLL
    Lisp Like Language (LLL)是和Assembly类似的低级语言。它追求极简;本质上只是直接对以太坊虚拟机的一点包装。
  4. Mutan (弃用)
    Mutan是个静态类型,由Jeffrey Wilcke 开发设计的C类语言。它已经不再受到维护。

写合约

没有Hello World程序,语言就不完整。Solidity在以太坊环境内操作,没有明显的“输出”字符串的方式。我们能做的最接近的事就是用日志记录事件来把字符串放进区块链,示例如下:

contract HelloWorld {
event Print(string out);
function() { Print("Hello, World!"); }
}

每次执行时,这个合约都会在区块链创建一个日志入口,印着“Hello,World!”参数。
另请参阅:Solidity docs里有更多写Solidity代码的示例和指导。

编译合约

solidity合约的编译可以通过很多机制完成。

  • 通过命令行使用solc编译器实现。
  • 在geth或eth提供的javascript控制台使用web3.eth.compile.solidity (这仍然需要安装solc 编译器)实现。
  • 通过在线Solidity实时编译器实现。
  • 通过建立solidity合约的Meteor dapp Cosmo实现。
  • 通过Mix IDE实现。
  • 通过以太坊钱包实现。

注意:关于solc和编译Solidity合约代码的更多信息可在此查看。

1. 在geth设置solidity编译器

如果你启动了geth节点,就可以查看哪个编译器可用。示例如下:

\> web3.eth.getCompilers();
["lll", "solidity", "serpent"]

这一指令会返回到显示当前哪个编译器可用的字符串。
注意:solc编译器和cpp- ethereum一起安装。或者,你可以自己创建。
如果你的solc可执行文件不在标准位置,可以用—solc标志为solc可执行文件指定一个定制路线。示例如下:

$ geth --solc /usr/local/bin/solc

或者你可以通过控制台在执行期间设置这个选项:

\> admin.setSolc("/usr/local/bin/solc")
solc, the solidity compiler commandline interface
Version: 0.2.2-02bb315d/.-Darwin/appleclang/JIT linked to libethereum-1.2.0-8007cef0/.-Darwin/appleclang/JIT
path: /usr/local/bin/solc

2. 编译一个简单合约

让我们来编译一个简单的合约源,示例如下:

source = "contract test { function multiply(uint a) returns(uint d) { return a * 7; } }"

这个合约提供了一个单一方法multiply,它和一个正整数a调用并返回到a*7。

下面准备在geth JS控制台用eth.compile.solidity()编译solidity代码:

\> contract = eth.compile.solidity(source).test
{
code: '605280600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b60376004356041565b8060005260206000f35b6000600782029050604d565b91905056',
info: {
language: 'Solidity',
languageVersion: '0',
compilerVersion: '0.9.13',
abiDefinition: [{
constant: false,
inputs: [{
name: 'a',
type: 'uint256'
} ],
name: 'multiply',
outputs: [{
name: 'd',
type: 'uint256'
} ],
type: 'function'
} ],
userDoc: {
methods: {
}
},
developerDoc: {
methods: {
}
},
source: 'contract test { function multiply(uint a) returns(uint d) { return a
*
7; } }'
}
}

注意:编译器通过RPC因此也能通过web3.js,对浏览器内任何通过RPC/IPC连接到geth的Ðapp可用。

下面的例子会向你展示如何通过JSON-RPC接合geth来使用编译器。

\$ geth --datadir ~/eth/ --loglevel 6 --logtostderr=true --rpc --rpcport 8100 --rpccorsdomain ' * ' --mine console 2>> ~/eth/eth.log
$ curl -X POST --data '{"jsonrpc":"2.0","method":"eth_compileSolidity","params":["contract test {

单源编译器输出会给出你合约对象,每个都代表一个单独的合约。eth.compile.solidity 的实际返还值是合约名字到合约对象的映射。由于合约名字是test,eth.compile.solidity(source).test会给出包含下列领域的测试合约对:

  • Code:编译的以太坊虚拟机字节代码。
  • Info:从编译器输出的额外元数据。
  • Source:源代码。
  • Language:合约语言 (Solidity,Serpent,LLL)。
  • LanguageVersion:合约语言版本。
  • compilerVersion:用于编译这个合约的solidity编译器版本。
  • abiDefinition:应用的二进制界面定义。
  • userDoc:用户的NatSpec Doc。
  • developerDoc:开发者的NatSpec Doc。

编译器输出的直接结构化(到code和info)反映了两种非常不同的部署路径。编译的以太坊虚拟机代码和一个合约创建交易被发送到区块,剩下的(info)在理想状态下会存活在去中心化云上,公开验证的元数据则执行区块链上的代码。

如果你的源包含多个合约,输出会包括每个合约一个入口,对应的合约信息对象可以用作为属性名称的合约名字检索到。你可以通过检测当前的GlobalRegistrar代码来试一下:

contracts = eth.compile.solidity(globalRegistrarSrc)

创建和部署合约

开始阅读这一节之前,确保你有解锁的账户和一些资金。

现在在区块链上创建一个合约,方法是用上一章节的以太坊虚拟机代码作为数据给空地址发送交易。示例如下:

注意:用在线Solidity实时编译器或Mix IDE程序会更容易完成。

var primaryAddress = eth.accounts[0]
var abi = [{ constant: false, inputs: [{ name: 'a', type: 'uint256' } ]
var MyContract = eth.contract(abi)
var contract = MyContract.new(arg1, arg2, ..., {from: primaryAddress, data: evmByteCodeFromPrevio

所有的二进制数据都以十六进制的格式序列化。十六进制字符串总会有一个十六进制前缀0x。

注意:注意arg1, arg2, …是合约构造函数参数,以备它要接受参数。如果合约不需要构造函数参数,就可以忽略这些参数。

值得指出的是,这一步骤需要你支付执行。一旦交易成功进入到区块,你的账户余额(你作为发送方放在from领域)会根据以太坊虚拟机的gas规则被扣减。一段时间以后,你的交易会在一个区块中出现,确认它带来的状态是共识。你的合约现在存在于区块链上。

以不同步的方式做同样的事看起来是这样:

MyContract.new([arg1, arg2, ...,]{from: primaryAccount, data: evmCode}, function(err, contract) {
if (!err && contract.address)
console.log(contract.address);
});

与合约互动

与合约互动典型的做法是用诸如eth.contract()功能的抽象层,它会返回到javascript对象,和所有可用的合约功能一起,作为可调用的javascript功能。

描述合约可用功能的标准方式是ABI定义。这个对象是一个字符串,它描述了调用签名和每个可用合约功能的返回值。示例如下:

var Multiply7 = eth.contract(contract.info.abiDefinition);
var myMultiply7 = Multiply7.at(address);

现在ABI中具体说明的所有功能调用都在合约实例中可用。你可以用两种方法中的一种来调用这些合约实例上的方法。

\> myMultiply7.multiply.sendTransaction(3, {from: address})
"0x12345"
> myMultiply7.multiply.call(3)
21

当用sendTransaction被调用的时候,功能调用通过发送交易来执行。需要花费以太币来发送,调用会永久记录在区块链上。用这种方式进行的调用返回值是交易散表。

当用call被调用的时候,功能在以太坊虚拟机被本地执行,功能返回值和功能一起返回。用这种方式进行的调用不会记录在区块链上,因此也不会改变合约内部状态。这种调用方式被称为恒定功能调用。以这种方式进行的调用不花费以太币。

如果你只对返回值感兴趣,那么你应该用call。如果你只关心合约状态的副作用,就应该用sendTransaction。

在上面的例子中,不会产生副作用,因此sendTransaction只会烧gas,增加宇宙的熵。

合约元数据

在之前的章节中,揭示了怎样在区块链上创建合约。现在来处理剩下的编译器输出,合约元数据或者说合约信息。

在与不是你创建的合约互动时,你可能会想要文档或者查看源代码。合约作者被鼓励提供这样的可见信息,他们可以在区块链上登记或者借助第三方服务,比如说EtherChain。管理员API为所有选择登记的合约提供便利的方法来获取这个捆绑。示例如下:

// get the contract info for contract address to do manual verification
var info = admin.getContractInfo(address) // lookup, fetch, decode
var source = info.source;
var abiDef = info.abiDefinition

这项工作的潜在机制是:

  • 合约信息被可以公开访问的URI上传到可辨认的地方。
  • 任何人都可以只知道合约地址就找到是什么URI。

仅通过2个步骤的区块链注册就可以实现这些要求。第一步是在被称作HashReg的合约中用内容散表注册合约代码(散表)。第二步是在UrlHint合约用内容散表注册一个url。这些注册合约是Frontier版本的一部分,已经参与到Homestead中。

要知道合约地址来查询url,获取实际合约元数据信息包,使用这一机制就足够了。

如果你是个尽职的合约创建者,请遵循以下步骤:

  1. 将合约本身部署到区块链
  2. 获取合约信息json文件
  3. 将合约信息json文件部署到你选择的任意url
  4. 注册代码散表 ->内容散表 -> url

JS API通过提供助手把这个过程变得非常容易。 调用admin.register从合约中提取信息,在指定文件中写出json序列,运算文件的内容散表,最终将这个内容散表注册到合约代码散表。一旦将那个文件部署到任意url,你就能用admin.registerUrl来注册url 和你区块链上的内容散表(注意,一旦固定的内容选址模式被用作文件商店,url-hint不再必要了) 。

source = "contract test { function multiply(uint a) returns(uint d) { return a
*
7; } }"
// compile with solc
contract = eth.compile.solidity(source).test
// create contract object
var MyContract = eth.contract(contract.info.abiDefinition)
// extracts info from contract, save the json serialisation in the given file,
contenthash = admin.saveInfo(contract.info, "~/dapps/shared/contracts/test/info.json")// send off the contract to the blockchain
MyContract.new({from: primaryAccount, data: contract.code}, function(error, contract){
if(!error && contract.address) {
// calculates the content hash and registers it with the code hash in `HashReg`
// it uses address to send the transaction.
// returns the content hash that we use to register a url
admin.register(primaryAccount, contract.address, contenthash)
// here you deploy ~/dapps/shared/contracts/test/info.json to a url
admin.registerUrl(primaryAccount, hash, url)
}
});

测试合约和交易

在为交易和合约排除故障时,你通常会需要一些低级的测试策略。这一章节将介绍一些你可以用到的排错工作和做法。为了测试合约和交易而不产生实际的后果,最好在私有区块链上测试。这可以通过配置一个替代网络ID (选择一个特别的数字)和/或不能用的端点来实现。推荐做法是,为了测试你用一个替代数据目录和端口,这样就不会意外地和实时运行的节点冲突(假定用默认运行。在虚拟机排错模式开启geth,推荐性能分析和最高的日志冗余级别):

geth --datadir ~/dapps/testing/00/ --port 30310 --rpcport 8110 --networkid 4567890 --nodiscover -

提交交易之前,你需要创建私有测试链(参阅测试网络相关章节),示例如下:

// create account. will prompt for password
personal.newAccount();
// name your primary account, will often use it
primary = eth.accounts[0];
// check your balance (denominated in ether)
balance = web3.fromWei(eth.getBalance(primary), "ether");
 // assume an existing unlocked primary account
primary = eth.accounts[0];
// mine 10 blocks to generate ether
// starting miner
miner.start(4);
// sleep for 10 blocks (this can take quite some time).
admin.sleepBlocks(10);
// then stop mining (just not to burn heat in vain)
miner.stop();
balance = web3.fromWei(eth.getBalance(primary), "ether");

创建交易之后,你可以用下面的命令来强制运行:

miner.start(1);
admin.sleepBlocks(1);
miner.stop();

你也可以用以下命令查看即将发生的交易:

// shows transaction pool
txpool.status
// number of pending txs
eth.getBlockTransactionCount(“pending”);
// print all pending txs
eth.getBlock(“pending”, true).transactions

如果你提交合约创建交易,可以检查想要的代码是否实际上嵌入到当前的区块链:

txhash = eth.sendTansaction({from:primary, data: code})
//... mining
contractaddress = eth.getTransactionReceipt(txhash);
eth.getCode(contractaddress)

《区块链开发指南》由 申屠青春 主编,由 宋波、张鹏、汪晓明、季宙栋、左川民 联合编著,中国三大区块链联盟的大伽联袂推荐。

这里写图片描述

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

共享汽车都在忙着应用区块链,你却还躺在“割韭菜”的泡沫里

作者 | Banbury责编 | 唐门教主区块链最近几年炒得很热,在掺杂着各种炒作、骗局尤以癫狂 ICO 割韭菜为甚的大环境下,但好在谈到具体的应用场景,从金融、互联网管理、医疗、版权、农业、慈善到能...

动手编写一个以太坊智能合约

一个以太坊节点提供一个RPC界面。这个界面给Ðapp(去中心化应用)访问以太坊区块链的权限和节点提供的功能,比如编译智能合约代码,它用JSON-RPC 2.0规范(不支持提醒和命名的参数) 的子集作为...

区块链开发(三)编写调试第一个以太坊智能合约

李赫 2016年9月10日 一、       智能合约IDE简介     目前以太坊上支持三种语言编写智能合约,     Solidity:类似JavaScript,这是以太坊官方推荐语言,也是最流行...

以太坊智能合约安全编程最佳实践smart-contract-best-practices

https://github.com/ConsenSys/smart-contract-best-practices Ethereum Contract Security T...

区块链开发(三)编写调试第一个以太坊智能合约

一、        智能合约IDE简介     目前以太坊上支持三种语言编写智能合约,     Solidity:类似JavaScript,这是以太坊官方推荐语言,也是最流行的智能合约语言。具体用...
  • fidelhl
  • fidelhl
  • 2016年09月13日 11:31
  • 3866

Docker容器化快速构建多集群以太坊网络并部署智能合约

本次打算把私链构建的脚本容器化,达到基于配置文件快速进行区块链网络构建的能力。 以太坊智能合约开发者可以基于以太坊的测试网络进行合约的测试,但是如果想进行持续集成和持续测试(CI&CT),建立一个本...
  • DDFFR
  • DDFFR
  • 2017年07月18日 18:49
  • 495

以太坊智能合约安全编程最佳实践smart-contract-best-practices

https://github.com/ConsenSys/smart-contract-best-practices Ethereum Contract Security Techniques ...
  • fidelhl
  • fidelhl
  • 2016年07月11日 08:08
  • 2173

搭建以太坊私有链和部署智能合约开发环境

前言搭建以太坊私有链和学习智能合约去年九月份做过一次,但是因为其他事情暂时搁下了,最近准备开始学习以太坊智能合约开发,以后会在论坛上发表一系列的相关博客,这次搭建解决了上次没完全解决的几个坑,相信很多...

以太坊私有链创建及智能合约的部署和交互

部署本机私有链 区块链说白了就是一个个块链接起来的一个链表结果,所以要在本机生成一个自己的私有链首先要做的就是自己先创建一个块作为第一个将要生成的区块链的第一个区块(区块链叫做创世块),所以先生成一个...

以太坊智能合约编程之菜鸟教程

译注:原文首发于ConsenSys开发者博客,原作者为Eva以及ConsenSys的开发团队。如果您想要获取更多及时信息,可以访问ConsenSys首页点击左下角Newsletter订阅邮件。本文的翻...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深入浅出谈以太坊智能合约
举报原因:
原因补充:

(最多只允许输入30个字)