访问合约和交易

RPC

在前面的章节中,我们介绍了合约的编写、部署和交互。现在到了深入了解以太坊网络和智能合约交互细节到时间。

以太坊节点都提供 RPC 接口,这些接口向Ðapp提供访问以太坊区块链和节点支持的功能,例如编译智能合约代码。它使用 JSON-RPC 2.0 规范(不支持通知或变量命名)的子集作为序列化协议,并且支持HTTP和IPC。

如果你对细节不关心仅仅只关心javascript库的使用,那么你可以跳过下面的章节,直接阅读: Using Web3

协议

RPC接口使用了一些不属于JSON-RPC 2.0规范的协议。

  • 数字使用十六进制编码,这个决定是因为一些语言不支持或有限支持超大数字类型。防止这种类型错误的发生,数值被编码为十六进制,并且交由开发者进行妥善处理,请参考wiki上的示例 hex encoding section
  • 默认的区块编号,一些RPC方法接受区块编号,在一些情况下,不能能获取区块编号或者非常不方便获得区块编号。在这种情况下,默认区块编号可以是这些字符串中的其中一个 [“earliest”, “latest”, “pending”] 。查看 wiki page 来获取RPC方法可用的默认区块参数列表。

部署合约

我们将通过不同的步骤,仅使用RPC接口来部署如下的合约。

contract Multiply7 {
   event Print(uint);
   function multiply(uint input) returns (uint) {
      Print(input * 7);
      return input * 7;
   }
}

需要做的第一件事,就是确保HTTP RPC接口被启用 ,这意味着我们需要在启动时给geth提供 --rpc 标识,给eth提供 -j 标识。在这个例子中我们使用一个geth私有开发节点,这样的话我们就不需要使用正式网络上的以太币。

> geth --rpc --dev --mine --minerthreads 1 --unlock 0 console 2>>geth.log

我们在这个端口启用HTTP RPC接口: http://localhost:8545

Note

geth支持 CORS(跨域资源共享),查看参数 --rpccorsdomain 来获取更多信息。

我们可以通过 curl 获取账户地址和余额来验证接口是否已经运行,请注意下面例子中的数据和你本地的节点不一样,如果你想尝试如下的命令请修改请求的参数。

> curl --data '{"jsonrpc":"2.0","method":"eth_coinbase", "id":1}' localhost:8545
{"id":1,"jsonrpc":"2.0","result":["0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a"]}

> curl --data '{"jsonrpc":"2.0","method":"eth_getBalance", "params": ["0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a", "latest"], "id":2}' localhost:8545
{"id":2,"jsonrpc":"2.0","result":"0x1639e49bba16280000"}

还记得我们前面说过数字都是以十六进制编码的吗?在这个情况中账户余额返回以wei为单位的十六进制编码字符串,如果我们想得到以ether为单位的数值我们可以通过命令台使用web3。

> web3.fromWei("0x1639e49bba16280000", "ether")
"410"

现在我们在私有开发链上有了一些以太币,可以进行合约的部署了,第一步需要验证solidity编译器处于可用状态,我们可以通过 eth_getCompilers 这个RPC方法来获取当前可用的编译器。

> curl --data '{"jsonrpc":"2.0","method": "eth_getCompilers", "id": 3}' localhost:8545
{"id":3,"jsonrpc":"2.0","result":["Solidity"]}

我们看到solidity编译器可用,如果不可用请参考 相关文档 说明。

下一步把Multiply7合约编译为可以发送给以太坊虚拟机的字节码。

> curl --data '{"jsonrpc":"2.0","method": "eth_compileSolidity", "params": ["contract Multiply7 { event Print(uint); function multiply(uint input) returns (uint) { Print(input * 7); return input * 7; } }"], "id": 4}' localhost:8545
{"id":4,"jsonrpc":"2.0","result":{"Multiply7":{"code":"0x6060604052605f8060106000396000f3606060405260e060020a6000350463c6888fa18114601a575b005b60586004356007810260609081526000907f24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da90602090a15060070290565b5060206060f3","info":{"source":"contract Multiply7 { event Print(uint); function multiply(uint input) returns (uint) { Print(input * 7); return input * 7; } }","language":"Solidity","languageVersion":"0.2.2","compilerVersion":"0.2.2","compilerOptions":"--bin --abi --userdoc --devdoc --add-std --optimize -o /tmp/solc205309041","abiDefinition":[{"constant":false,"inputs":[{"name":"input","type":"uint256"}],"name":"multiply","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"","type":"uint256"}],"name":"Print","type":"event"}],"userDoc":{"methods":{}},"developerDoc":{"methods":{}}}}}}

现在我们已经编译完成了代码,我们需要决定花费多少瓦斯来部署它,RPC接口的 eth_estimateGas 方法可以给我们提供一个参考值。

> curl --data '{"jsonrpc":"2.0","method": "eth_estimateGas", "params": [{"from": "0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a", "data": "0x6060604052605f8060106000396000f3606060405260e060020a6000350463c6888fa18114601a575b005b60586004356007810260609081526000907f24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da90602090a15060070290565b5060206060f3"}], "id": 5}' localhost:8545
{"id":5,"jsonrpc":"2.0","result":"0xb8a9"}

最终,我们部署好了合约。

> curl --data '{"jsonrpc":"2.0","method": "eth_sendTransaction", "params": [{"from": "0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a", "gas": "0xb8a9", "data": "0x6060604052605f8060106000396000f3606060405260e060020a6000350463c6888fa18114601a575b005b60586004356007810260609081526000907f24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da90602090a15060070290565b5060206060f3"}], "id": 6}' localhost:8545
{"id":6,"jsonrpc":"2.0","result":"0x3a90b5face52c4c5f30d507ccf51b0209ca628c6824d0532bcd6283df7c08a7c"}

交易被节点接受并且交易的哈希被返回,我们可以通过这个哈希来追踪交易。

下一步来决定我们部署合约的地址,交易的每一次执行都会创建一个收据,这个收据包含交易的各种信息,例如交易包含在哪个区块中、交易过程花费了多少瓦斯等。如果一个交易创建了一个合约,它也会包含合约的地址,我们可以通过RPC接口的 eth_getTransactionReceipt 方法来获取收据。

> curl --data '{"jsonrpc":"2.0","method": "eth_getTransactionReceipt", "params": ["0x3a90b5face52c4c5f30d507ccf51b0209ca628c6824d0532bcd6283df7c08a7c"], "id": 7}' localhost:8545
{"id":7,"jsonrpc":"2.0","result":{"transactionHash":"0x3a90b5face52c4c5f30d507ccf51b0209ca628c6824d0532bcd6283df7c08a7c","transactionIndex":"0x0","blockNumber":"0x4c","blockHash":"0xe286656e478a1b99030e318d0f5c3a61a644f25e63deaa8be52e80da1e7b0c47","cumulativeGasUsed":"0xb8a9","gasUsed":"0xb8a9","contractAddress":"0x6ff93b4b46b41c0c3c9baee01c255d3b4675963d","logs":[]}}

我们可以看到合约创建于 0x6ff93b4b46b41c0c3c9baee01c255d3b4675963d ,如果你获取到null收据,那么说明交易还没有被打包进区块中,请检查你的矿工还在工作并且等待一会再重新确认。

与智能合约交互

我们的合约已经部署完毕,可以与之进行交互了。前面已经说过,我们有 两种方法 <interacting_with_a_contract> 可以与合约进行交互: sendTransactioncall 。在下面的例子中,我们将使用sendTransaction的方式来与合约中的multiply方法交互。

通过阅读 eth_sendTransaction 的文档,我们发现需要提供一些参数,分别需要指定:fromtodatafrom 是我们发起交易账户的公开地址, to 是合约的地址, data 参数有一点复杂,它的内容包含了哪个函数将被调用并且规定了调用的参数,这是通过ABI进行实现的,ABI定义了如何为以太坊虚拟机(EVM)定义和编码数据,你可以阅读 ABI细节wiki

data 的字节序列是一个函数选择器,定义了哪个函数将会被调用,它是通过Keccak哈希计算函数名称和调用参数,然后取其哈希值的前4字节并用十六进制进行编码,我们前面定义的乘法函数接受一个256位的整型数,这让我们有:

> web3.sha3("multiply(uint256)").substring(0, 8)
"c6888fa1"

更多细节,请点击 函数选择器

下一步是对参数进行编码,我们的参数只有一个uint256,我们假设这个值为6, 参数编码规则 中的如下章节指出了我们应该如何编码uint256。

int<M>: enc(X) 是X的大端二进制补码编码,0xff填充在负数的高位(左侧),用零填充使得长度为32字节。

这样编码结果为 0000000000000000000000000000000000000000000000000000000000000006

将函数编码和参数编码进行组合,就得到我们的 data ,也就是 0xc6888fa100000000000000000000000000000000000000000000000000000006.

我们来尝试调用:

> curl --data '{"jsonrpc":"2.0","method": "eth_sendTransaction", "params": [{"from": "0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a", "to": "0x6ff93b4b46b41c0c3c9baee01c255d3b4675963d", "data": "0xc6888fa10000000000000000000000000000000000000000000000000000000000000006"}], "id": 8}' localhost:8545
{"id":8,"jsonrpc":"2.0","result":"0x759cf065cbc22e9d779748dc53763854e5376eea07409e590c990eafc0869d74"}

调用完毕后我们得到交易的哈希,如果我们获取收据我们可以得到如下内容:

{
   blockHash: "0xbf0a347307b8c63dd8c1d3d7cbdc0b463e6e7c9bf0a35be40393588242f01d55",
   blockNumber: 268,
   contractAddress: null,
   cumulativeGasUsed: 22631,
   gasUsed: 22631,
   logs: [{
      address: "0x6ff93b4b46b41c0c3c9baee01c255d3b4675963d",
      blockHash: "0xbf0a347307b8c63dd8c1d3d7cbdc0b463e6e7c9bf0a35be40393588242f01d55",
      blockNumber: 268,
      data: "0x000000000000000000000000000000000000000000000000000000000000002a",
      logIndex: 0,
      topics: ["0x24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da"],
      transactionHash: "0x759cf065cbc22e9d779748dc53763854e5376eea07409e590c990eafc0869d74",
      transactionIndex: 0
  }],
  transactionHash: "0x759cf065cbc22e9d779748dc53763854e5376eea07409e590c990eafc0869d74",
  transactionIndex: 0
}

收据中包含日志,这个日志是以太坊虚拟机在交易执行的过程中创建的,并且被包含在收据中。如果我们查看乘法函数,会发现Print事件将输入的参数乘以7,通过ABI编码规则,我们可以解码得到预期的结果42。除了data字段之外,值得注意的是topics字段,通过它可以得知是哪个事件创建了日志。

> web3.sha3("Print(uint256)")
"24abdb5865df5079dcc5ac590ff6f01d5c16edbc5fab4e195d9febd1114503da"

你可以阅读更多有关event、topics的内容,可以查看 Solidity教程

这里我们仅仅做了简单的介绍,想要查看完整可用的RPC方法列表请查看: RPC的wiki页面

Web3.js

从上面的例子中我们看到JSON-RPC接口的使用是非常云长乏味并且容易出错的,特别是我们需要使用ABI时,Web3.js是一个在以太坊RPC接口上工作的javascript库,它的目标是提供一个更友好的交互界面,同时减少出错的概率。

使用web3来部署输入乘7的合约将会是如下的样子:

var source = 'contract Multiply7 { event Print(uint); function multiply(uint input) returns (uint) { Print(input * 7); return input * 7; } }';
var compiled = web3.eth.compile.solidity(source);
var code = compiled.Multiply7.code;
var abi = compiled.Multiply7.info.abiDefinition;

web3.eth.contract(abi).new({from: "0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a", data: code}, function (err, contract) {
   if (!err && contract.address)
      console.log("deployed on:", contract.address);
   }
);

deployed on: 0x0ab60714033847ad7f0677cc7514db48313976e2

加载一个已经部署的合约并且通过交易来进行交互:

var source = 'contract Multiply7 { event Print(uint); function multiply(uint input) returns (uint) { Print(input * 7); return input * 7; } }';
var compiled = web3.eth.compile.solidity(source);
var Multiply7 = web3.eth.contract(compiled.Multiply7.info.abiDefinition);
var multi = Multiply7.at("0x0ab60714033847ad7f0677cc7514db48313976e2")
multi.multiply.sendTransaction(6, {from: "0xeb85a5557e5bdc18ee1934a89d8bb402398ee26a"})

注册一个名为 Print 的事件来创建日志。

multi.Print(function(err, data) { console.log(JSON.stringify(data)) })
{"address":"0x0ab60714033847ad7f0677cc7514db48313976e2","args": {"":"21"},"blockHash":"0x259c7dc07c99eed9dd884dcaf3e00a81b2a1c83df2d9855ce14c464b59f0c8b3","blockNumber":539,"event":"Print","logIndex":0, "transactionHash":"0x5c115aaa5418118457e96d3c44a3b66fe9f2bead630d79455d0ecd832dc88d48","transactionIndex":0}

更多信息请查看 web3.js的wiki页面

命令行

geth的 命令行 提供javascript运行时命令行接口,它可以连接到本地或远程的geth或eth节点,运行时会加载web3.js库供用户使用,这就使得用户可以通过web3.js命令来部署和调用智能合约,事实上,前面 Web3.js 中的代码可以直接拷贝到命令行中进行运行。

查看线上合约和交易

你可以通过多种方式来查看以太坊区块链上的合约和交易,点击查看 区块链查询

区块链查询网站

其他资源

  • EtherNodes - 节点的地理位置和客户端类型划分
  • EtherListen - 实时查看以太坊交易信息