比特币中的智能合约存在于交易的输入输出中, 交易输入中的解锁脚本去解交易输出中的锁定脚本, 来完成合约执行.

要理解以太坊的智能合约, 需要暂且把比特币的智能合约忘掉, 这样可能会更容易理解. 当初我学完比特币后, 开始学的以太坊, 总拿比特币中的知识去往以太坊来靠, 结果各种晕. 其实以太坊相对比特币更容易理解.

要理解以太坊中的智能合约, 首先需要理解这么几个以太坊中的重要概念:

基础概念

  • 账户 比特币的实现并没有账户这个抽象. 在比特币中要获得某人账户的余额, 需要把这个人能解锁的所有锁定脚本中的余额全加起来. 比特币钱包中余额就把比特币区块链中所有交易遍历一遍找到钱包拥有者所能解锁的输出脚本, 并将其中的金额加起来实现的. 当然, 钱包为什么能很快展现出余额, 是因为使用了缓存. 而在以太坊的实现中是直接就有账户这个抽象的, 发起交易就是从一个账户转钱给别一个账户, 一个账户的余额减少, 另一个账户的余额增加, 和咱们熟悉的银行转账一样. 这种账户在以太坊中称为外部账户, 简称账户. 以太坊账户的余额是存在于以太坊区块链中的, 直接就可以获得, 不需要像比特币那么费劲遍历输出脚本. 账户有地址, 地址就是公私钥中的公钥, 这个与比特币类似.

在以太坊中还有另一个账户, 叫合约账户, 简称合约. 在以太坊中说合约, 其实说的是合约账户. 以太坊中的合约有余额, 有地址, 最重要的是还有自己的存储. 应用可以利用合约的存储来存一些应用需要的东西, 之后的例子会说明.

为了方便, 以下说起账户都是指外部账户, 说起合约都是指合约账户.

  • 交易与消息 以太坊中交易就是指从账户(简称账户)发起的由账户的私钥进行签名的一个消息体. 以太坊中的交易可以从账户发到另一个账户, 也可以由账户发到一个合约. 从一个账户到另一个账户的交易类似比特币中的简单交易, 将一些以太币转到另一个账户. 从一个账户到一个合约的交易, 把账户所拥有的以太币转到合约的余额里并触发合约的执行. 这个有点像比特币中的支付给脚本(P2SH);

以太坊中的消息是指由一个合约发起的到另一个合约的消息体, 把合约中的以太币转到另一个合约的余额里并触发另一个合约的执行. 可以理解为一个合约去调用另一个合约.

交易与消息有如下区别:

  • 交易只能由账户发出. 消息只能由合约发出.
  • 交易的对方可以是账户也可以是合约, 消息的对方只能是合约.
  • 交易必须由账户的私钥签名, 消息不用签名.

了解了账户, 合约, 交易, 消息的概念后, 那么他们之间是什么关系呢? 在以太坊中他们是如何串起来的?

整个流程是这样的, 账户发起交易, 交易触发合约, 合约发送消息到其他合约, 也就是合约调用其他合约, 伴随着交易与合约的执行, 账户的余额, 合约的余额, 合约的存储都可能会发生变化.

可以这么理解, 整个区块链就是个数据库, 记录了账户的余额, 合约的余额, 合约的存储, 而想要改变这个数据库中的值必须发起交易, 用交易以及交易所触发的合约来改变这个数据库中的值. 交易的发起只能来自账户, 所以账户是整个区块链状态改变的源动力.

为了加深理解, 断续阐述一下交易与合约的关系.

交易与合约关系

以太坊中交易的结构如下:

  • 交易的接收者. 可以是合约也可以是账户.
  • 签名. 账户私钥对交易进行签名.
  • 转账的金额.
  • 一个可选的data字段. 详情见之后交易与合约关系的讨论.
  • 交易费. 燃气值GAS与燃气单价GASPRICE, 这两个相乘就是交易费. 详情见:https://media.consensys.net/ethereum-gas-fuel-and-fees-3333e17fe1dc, 这里不赘述了.

如下, 调用以太坊接口, 获得某个交易的内容:

var transaction = web3.eth.getTransaction('0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b');
console.log(transaction);
/*
{
  "hash": "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b",
  "nonce": 2,
  "blockHash": "0xef95f2f1ed3ca60b048b4bf67cde2195961e0bba6f70bcbea9a2c4e133e34b46",
  "blockNumber": 3,
  "transactionIndex": 0,
  "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
  "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f",
  "value": BigNumber,
  "gas": 314159,
  "gasPrice": BigNumber,
  "input": "0x57cb2fc4"
}
*/

以太坊中交易与合约关系如下:

  • 合约的创建是由交易创建的. 发起一个接收者为空的交易, data中写入合约代码, 就会创建一个合约.

如下, 调用以太坊接口, 创建一个合约:

// compiled solidity source code using https://chriseth.github.io/cpp-ethereum/
var code = "603d80600c6000396000f3007c01000000000000000000000000000000000000000000000000000000006000350463c6888fa18114602d57005b6007600435028060005260206000f3";

web3.eth.sendTransaction({data: code}, function(err, transactionHash) {
  if (!err)
    console.log(transactionHash); // "0x7f9fade1c0d57a7af66ab4ead7c2eb7b11a91385"
});
  • 合约的调用是可以由交易来触发调用的. 发起一个接收者为合约地址的交易, data写入调用合约的代码, 则触发合约相关方法的执行.这种方式一般用于对合约的状态修改(余额, 存储).
  • 合约的调用可以直接调用, 不用发起交易. 这种一般用于查询合约状态(余额, 存储).

接下来与比特币中的智能合约作个对比.

与比特币智能合约区别

以太坊中的智能合约比比特币中的智能合约更强大, 更灵活.

  • 以太坊中的智能合约与交易是分开的单独存储. 比特币中的智能合约存在于交易的输入与输出中, 而以太坊中的合约与交易是分开存储的.
  • 以太坊的智能合约对开发人员更友好. 可以用都比较熟悉的类似javascript语言的代码来编写合约. 合约进行编译后运行在以太坊虚拟机evm上. 这里的虚拟机evm可以类比为java虚拟机jvm.
  • 以太坊的合约语言图灵完备, 而比特币是图灵不完备的. 比如比特币为了防止在支付时发生阻塞, 不支持循环语句. 而以太坊支持, 以太坊根据代码执行所占资源收取交易费来阻止恶意循环.
  • 以太坊合约有状态, 比特币合约没有. 以太坊中的合约其实全称为合约账户, 有自己的状态. 比如合约的余额, 合约的存储. 而比特币的合约是没有状态的. 这大大限制了合约的作用.
  • 以太坊合约可以获取区块中的数据, 比特币不能. 比如以太坊中的合约可以拿到上一个区块的hash值等一些区块的信息而比特币拿不到区块的任何信息.
  • 由于以太坊智能合约的强大, 导致合约可以写的很复杂, 而复杂必然会导致合约更容易产生bug. 像比特币正因为合约语法简单, 限制多, 所以非常健壮, 诞生以来未出现严重bug.

以太坊做了整个区块链的基础设施, 而把合约的编写抛给了用户, 让用户可以根据自己的业务需求只编写合约, 而不用费劲再自己搭一个区块链. 那么接下来我们就通过几个合约例子, 来加深理解一下以太坊智能合约.

写几个合约

一个简单的交易合约

在以太坊中, 像小帅支付给小强5个以太币这样的简单交易是不需要写智能合约的. 直接调用以太坊接口就可以:

web3.eth.sendTransaction(transactionObject [, callback])
/*
transactionObject中包含了小帅的地址, 小强的地址, 以太转多少以太币.
*/

底层也是通过用私钥签名, 用公钥进行验签来实现的. 比如小帅发起向小强支付5个以太币的交易, 该交易带有小帅的私钥签名. 以太坊节点收到这个交易后会用小帅的公钥去验证交易的签名.

接下来看一下以太坊中多签交易的实现.

一个多签交易合约

在比特币中的多签交易的实现, 是把比特币支付给一个需要多人签名才能解锁输出脚本中. 在以太坊中, 则是把以太币支付给一个需要提供多人签名才能转出余额的合约中. 也就是先把以太币转到一个合约里, 而合约代码控制了合约余额的转出需要多人签名.

在以太坊中实现多签交易的合约代码如下:

pragma solidity 0.4.15;
contract SimpleMultiSig {

  uint public nonce;                // (only) mutable state
  uint public threshold;            // immutable state
  mapping (address => bool) isOwner; // immutable state
  address[] public ownersArr;        // immutable state

  //构造方法, 创建一个合约只当具有owners中的threshold_个签名, 才可以把合约中的币转出去.
  function SimpleMultiSig(uint threshold_, address[] owners_) {
    require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ != 0);

    address lastAdd = address(0);
    for (uint i=0; i<owners_.length; i++) {
      require(owners_[i] > lastAdd);
      isOwner[owners_[i]] = true;
      lastAdd = owners_[i];
    }
    ownersArr = owners_;
    threshold = threshold_;
  }

  //把合约中的币转到destination这个地址. 其中sigV, sigR, sigS是签名相关数据.
  function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) {
    require(sigR.length == threshold);
    require(sigR.length == sigS.length && sigR.length == sigV.length);

    //签名算法: https://github.com/ethereum/EIPs/issues/191
    bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce);

    address lastAdd = address(0); // cannot have address(0) as an owner
    //以下证明签名中包含至少threshold个正确签名.
    for (uint i = 0; i < threshold; i++) {
        address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
        require(recovered > lastAdd && isOwner[recovered]);
        lastAdd = recovered;
    }

    //如果程序能走到这里, 证明至少有threshold个正确签名.
    nonce = nonce + 1;
    require(destination.call.value(value)(data));
  }

  function () payable {}
}

以上是一个简单版的多签合约, 以太坊官方的多签合约比这复杂很多, 功能也相对更加强大.

官方多签合约:https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol 官方多签规范协议:https://github.com/ethereum/EIPs/issues/763 官方多签有更多的功能, 比如限制每天的提取限额, 大额需要多签, 小额免多签, 替换owner等. 从官方多签合约可以看出以太坊合约的灵活与强大.

一个简单的ICO合约

ICO就是首次发行代币, 背后的实现就是数字代币合约, 以下是一个以太坊中简单的数字代币合约的实现:

pragma solidity ^0.4.0;

contract Coin {
    //谁可以发行币
    address public minter;
    //每个账户的代币余额, 类似php中的二维数组, java中的map.
    mapping (address => uint) public balances;

    //转账事件, client可以订阅收到这个事件
    event Sent(address from, address to, uint amount);

    //合约构造方法, 创建合约时执行, 只执行一次.
    //合约一旦创建, minter值不能再修改. 因为此合约没有提供修改miner的方法.
    function Coin() public {
        minter = msg.sender;
    }

    //改变账户币的余额
    function mint(address receiver, uint amount) public {
        if (msg.sender != minter) return;
        balances[receiver] += amount;
    }

    //转账, 将交易发起者(msg.sender)的amount代币转给receiver.
    function send(address receiver, uint amount) public {
        if (balances[msg.sender] < amount) return;
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        Sent(msg.sender, receiver, amount);
    }
}

可以看到实现一个数字代币, 特别简单.

所以搞一个ICO骗钱在技术没有成本, 主要地要把界面, 宣传搞好. 像之前这个来钱太他妈容易了. 无本万利啊.

以上是一个最简版的, 官方有一个数据代币的教程, 稍微复杂点:https://www.ethereum.org/token