合约

什么是合约?

合约是存放在以太坊区块链具有特定地址的代码(它的功能)和数据(它的状态)集合。 合约账户之间可以相互传递消息以实现图灵完备运算。 合约以以太坊特定的二进制字节码通过以太坊虚拟机(EVM)运行于区块链上。

合约通常是以名为 Solidity 的高级语言编写,并被编译为字节码上传到区块链上。

See also

其它的语言也是存在的,特别是Serpent和LLL,将会在 以太坊高级语言 章节的文档中进一步描述。

Dapp开发相关资源 列出了集成开发环境,开发工具支持你用这些语言进行开发,并提供测试和部署等相关功能。

以太坊高级语言

合约以以太坊特定的二进制字节码通过以太坊虚拟机(EVM)运行于区块链上,通常是以高级语言编写并被编译成机器码部署在区块链上。

下面有几种不同的可以用来编写以太坊智能合约的高级语言。

Solidity

Solidity是一种类似JavaScript的语言,允许你开发智能合约并可以被编译成EVM字节码,现在已经是以太坊的旗舰语言并且是最流行的。

Serpent

Serpent是一种类似Python的语言,允许你开发智能合约并被编译成EVM字节码,它提供最大的限度是整洁和简单,将低级语言的许多效率优势与编程风格的易用性结合在一起,同时为智能合约编程添加特定的域功能。Serpent使用 LLL 编译。

LLL

`Lisp Like Language(LLL)<https://github.com/ethereum/libethereum/tree/develop/liblll>`_ 是类似于Assembly的低级语言。 这意味着非常简单和简约; 基本上只是一个很小的封装,直接在EVM中编码。

Mutan (不推荐)

Mutan 是由Jeffrey Wilcke开发的静态类似C语言的开发语言,目前已经不再维护。

编写合约

没有实现Hello World程序的语言是不完整的,在以太坊的环境中,Solidity没有一个明确的方式可以”输出”一个字符串。 最接近的方式就是实用*日志事件*将一个字符串放入区块链中:

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

这条合约每次执行后,会通过Print并带有”Hello World”参数,将一条日志放入区块链中。

See also

Solidity文档 有详细的Solidity编程示例和指南。

编译合约

可以通过多种形式的机制进行solidity合约的编译。

Note

有关solc和编译Solidity合约代码的更多信息可以查看 这个链接

在geth中设置solidity编译器

如果你启动了 geth 节点,你可以通过如下命令来检查哪些编译器可以使用。

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

这个命令返回当前可用的编译器的字符串数组。

Note

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

编译一个简单的合约

我们来编译一个简单的合约代码:

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

这个合约提供了一个名为 multiply 的函数,输入一个正整数 a 返回结果 a * 7

你已经准备好了编译solidity代码的环境,使用 geth 的JS命令台 eth.compile.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; } }'
  }
}

Note

编译器支持`RPC <https://github.com/ethereum/wiki/wiki/JSON-RPC>`__ ,因此你可以使用 web3.js 并通过RPC/IPC连接到 geth

下面的例子显示了如何通过使用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 { function multiply(uint a) returns(uint d) { return a * 7; } }"],"id":1}' http://127.0.0.1:8100

编译器为源代码中的每个单独的合约生成一个合约对象,命令 eth.compile.solidity 会返回合约名和合约对象的映射。这个例子中我们的合约名为 test ,所以命令 eth.compile.solidity(source).test 会返回名为test的合约对象,并包含如下相关域:

code
编译生成的以太坊虚拟机字节码
info
编译器输出的额外元数据
source
源代码
language
合约使用的编程语言(Solidity, Serpent, LLL)
languageVersion
合约语言的版本号
compilerVersion
编译合约代码所使用编译器的版本号
abiDefinition
应用程序二进制接口定义 Application Binary Interface Definition
userDoc
提供给用户的 NatSpec Doc
developerDoc
提供给开发者的 NatSpec Doc

编译器最直观的输出结构(codeinfo)反应出两个完全不同的 部署路径 ,编译出的EVMcode会给发给区块链上特定交易,剩下的(info)会存放在去中心化的区块链云端作为完善代码的元数据。

如果你的源代码包含多个合约,那么输出会包含每一个合约的入口信息,合约的展开信息可以通过名字来获取,你可以通过查看当前的GlobalRegistrar合约来尝试一下效果:

contracts = eth.compile.solidity(globalRegistrarSrc)

创建并部署一个合约

在开始这个章节前,请确保你有一个解锁的账户并且里面有一些资金。

你现在可以通过前面章节的EVM代码来向一个空地址 发起一笔交易

Note

这个可以通过更容易的方式完成,也就是通过 实时在线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: evmByteCodeFromPreviousSection})

所有的二进制数据都会被序列化为十六进制格式,十六进制的字符串总是以 0x 作为前缀。

Note

请注意 arg1, arg2, ... 是合约的构造参数,可以接受任何输入,如果合约不需要任何构造参数那么这些参数可以被忽略。

值得指出的是执行这些步骤你需要支付一些费用,一旦的交易被打包进区块,你账户的余额会根据以太坊虚拟机的瓦斯费用规则进行扣除,经过一些时间,你的交易会出现在一个状态被确认是一致的区块中,你的合约现在已经存在于区块链中。

异步执行这些步骤的方法如下:

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 时,函数会在本地的虚拟机(EVM)上执行,调用的返回值就是函数的返回值。这种方式的调用不会被纪录在区块链中,因此也不会改变合约的内部状态,这种方式被称为常量函数调用。这种调用方式不会消耗以太币。

只关心返回值的情况下你应该使用 call ,如果你关心合约的状态变化那么就使用 sendTransaction

在上面的例子中,不涉及改变合约状态,因此 sendTransaction 调用只会白白燃烧燃料(gas)增加宇宙的熵。

合约元数据

在上个章节我们解释了如何在区块链上创建合约,接下来我们处理编译器输出的内容,合约元数据或者合约信息。

当与一个你还没有创建的合约进行交互时,你可能想要说明文档或者查看其源代码。合约作者被鼓励通过区块链或者第三方机构的服务来注册此类信息,例如: EtherChain 。API admin 为注册了这类信息的合约提供了便利的方法来查看。

// 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

这些合约信息通过两步区块链注册被打包: * 第一步:称为 HashReg 的合约通过内容哈希来注册合约代码。 * 第二步:称为 UrlHint 的合约通过内容哈希来注册url。 这些 注册合约 被作为前沿(Frontier)版本的一部分,同时被带入到家园(Homestead)版本中。

使用这个结构,只需要知道合约的地址,然后获取到url,进而获取合约相关的所有元数据。

如果你是一个称职的合约创建者,你需要遵循如下步骤:

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

JS API提供帮助让这些步骤变的非常简单,调用 admin.register 来得到合约摘要,将摘要序列化存储到指定的json文件中,计算文件的内容哈希,并最终将这些内容哈希注册为代码哈希。一单你将这些文件部署到任何url,你可以通过使用 admin.registerUrl 在区块链上注册你的内容哈希url(如果使用固定内容寻址模型作为文档存储那么rul-hint就不是必需的了)。

source = "contract test { function multiply(uint a) returns(uint d) { return a * 7; } }"
// 使用solc来编译
contract = eth.compile.solidity(source).test
// 创建合约对象
var MyContract = eth.contract(contract.info.abiDefinition)
// 合约的摘要信息,序列化到指定的json文件中
contenthash = admin.saveInfo(contract.info, "~/dapps/shared/contracts/test/info.json")
// 合约发送到区块链上
MyContract.new({from: primaryAccount, data: contract.code}, function(error, contract){
  if(!error && contract.address) {
    // 计算内容哈希并且将其通过 `HashReg` 注册为代码哈希
    // 使用地址来发送交易
    // 返回我们用来注册url的内容哈希
    admin.register(primaryAccount, contract.address, contenthash)
    // 将 ~/dapps/shared/contracts/test/info.json 部署到一个url上
    admin.registerUrl(primaryAccount, hash, url)
  }
});

测试合约和交易

通常要对合约和交易进行测试和调试,这个章节我们来介绍几种调试工具和实践方法。为了测试合约和交易,并且避免产生真实的影响,你最好在一条私有区块链上进行操作,这可以通过配置网络ID(选择一个独一无二的整数)来实现,并且不需要其他对等节点。推荐的做法是为测试设置其他的数据目录和端口,以避免可能的来自其他节点的影响(假设使用默认的参数运行)。通过调试模式来运行 geth ,并设置最高级别的日志:

geth --datadir ~/dapps/testing/00/ --port 30310 --rpcport 8110 --networkid 4567890 --nodiscover --maxpeers 0 --vmdebug --verbosity 6 --pprof --pprofport 6110 console 2>> ~/dapp/testint/00/00.log

提交任何交易前,你需要设置好你的测试链,详细内容请查看: test-networks

// 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)