分类目录归档:区块链

比特币的UTXO和以太坊的Account Nonce

比特币和以太坊都是目前知名的数字货币,然而,两者在实现上采用了截然不同的模型。

比特币采用的是未消费输出(Unspent Transaction Output,简写UTXO),而以太坊则采用账户余额体系。

比特币的交易模型如下

一笔交易 TX 包含多个输入(input)和多个输出(output,一个输出包含两个内容,一个是比特币数量,单位为satoshi,另一个就是接受比特币的收款人的public key),当TX 1,2,3,4,5,6都还没有发生的时候,TX 0的output0和output1都没有被别的交易的输入而引用,此时,TX 0的output0和output1就被称之为未消费输出(UTXO),当TX 0的output0被TX 1的input0引用之后,TX 0的output0就被消费了。

不难看出,比特币为了防止双花(double spend),一个输出不能同时被两个输入而引用。如果A的一个UTXO有100个比特币,而我们希望转20个给B,那么,我们需要将这100个比特币的UTXO消费掉,将其中80个发送到一个新的收款人为A的UTXO中,20个发送收款人为B的UTXO中,交易完成以后,之前的有100个比特币的输出就从UTXO变成了Spent Transaction Output了。

对于比特币来说,输出的源头就是挖矿而产生的coinbase交易,一个coinbase交易的输入是一个coinbase,输出为区块奖励。挖矿奖励的coinbase交易为一个区块的第一个交易。

我们不难看到,对于比特币来说,如果我们需要计算一个地址有多少比特币余额,我们必须将该地址对应的所有的UTXO余额累加起来,才能得到账户的余额,然而,如果我们的账户拥有10个UTXO,那么,理论上,我们可以同时发送10笔交易,这笔交易同时打包到一个区块之中,只要每一笔交易引用的UTXO是不同的就好。

总结,比特币的UTXO模型,使得单个账户可同时发送多笔交易,且多笔交易之间可以不互相产生影响,这使得比特币的交易天生有着很好的并发友好特性。

而对于以太坊来说,由于采用了账户余额体系,因此,以太坊在防止双花(Double Spend)上就没有办法采用比特币的方法了。为此,以太坊规定,每一个账户有一个nonce值,这个nonce值等于该账户的累计发起的交易数量,如果该账户发起一笔交易,那么,交易的数据中必须包含一个nonce值,该值必须大于账户的nonce值,否则,该交易属于非法交易。如果交易的nonce值减去账户的nonce大于1,那么,该笔交易暂时不能被包含到区块中,必须等到nonce值为账户的nonce值加一的交易被打包以后,该笔交易才能被打包到区块中。如果有两笔交易都是同样的nonce值,那么,只有其中一笔能够成功。

以太坊采取了账户体系,并通过状态变化(State Transition)记录了交易对账户的影响。

我们不难发现以太坊的nonce值是必要的。

首先,如果没有nonce值的话,黑客可以通过重放攻击来偷盗数字货币,假设A向B转行了1 ETH,而且以太坊没有nonce机制,那么,B可以将之前成功的转账交易数据在以太坊网络上重复发送,这样,B就可以源源不断的获取ETH,而这显然不是A期待的。

其次,nonce值的设计确保了交易一定是顺序执行的,避免了双花(Double Spend)问题。

    然而,以太坊的设计也造成了一些天然的缺点。

  1. 如果用户发送了一笔交易,并且选择了很低的矿工费(如gasPrice设置为0),那么,该笔交易可能数日甚至几个月都无法得到确认,这会导致用户的账号在很长的时间内都无法发起转出交易。而比特币就没有这个问题。
  2. 官方的Ethereum Wallet和Mist都没有提供取消交易的功能,实际上,我们可以通过发送和上一笔交易相同的nonce的新的交易,且新的交易为自己转账给自己,并选择高矿工费(如gasPrice为50或者100 Gwei),那么上一笔交易将会被以太坊网络抛弃。然而,由于Ethereum Wallet和Mist既不支持用户自定义nonce,也不支持自己转账给自己,所以,事实上客户没有好的办法取消自己的交易。
  3. 尽管以太坊的出块速度比比特币高很多,平均15秒出一个快,且理论上以太坊没有设置block limit,实际上以太坊的TPS仍然很低,只能达到10左右。这是因为有很多复杂交易,例如创建新的合约,一笔交易就上百万的gas消耗,而以太坊的区块gas limit通常也就800万左右,一个区块能够容纳的交易个数就有限了。尽管矿工可以提升gas limit,由于单个区块交易越多,矿工计算量就越大,所以,矿工不会轻易提升区块的gas limit,毕竟小区块更容易挖出来,从而挣到block reward。基于此考虑,以太坊的TPS很难提升。

然而,不论是比特币还是以太坊,亦或是其他的公有链的区块链平台,由于他们在设计上都考虑到了全球节点的同步,这就决定了出块速度不能太快,否则必然大量节点无法及时同步最新的状态,这就导致共识无法及时形成,很容易造成区块的分叉。

以太坊 Ethereum ERC20代币批量转账接口

以太坊(Ethereum)作为一个知名的区块链平台,大量的代币发行(Initial Coin Offering)通过以太坊进行,而代币通常为以太坊上一个遵循了ERC20规范的智能合约。

如果一个以太坊智能合约实现了以下接口,那么,这个智能合约即为一个ERC20代币。

// ----------------------------------------------------------------------------
// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
// ----------------------------------------------------------------------------
contract ERC20Interface {
    function totalSupply() public constant returns (uint);
    function balanceOf(address tokenOwner) public constant returns (uint balance);
    function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
    function transfer(address to, uint tokens) public returns (bool success);
    function approve(address spender, uint tokens) public returns (bool success);
    function transferFrom(address from, address to, uint tokens) public returns (bool success);

    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

然而,我们不难看到,ERC20规范中只有一对一转账的transfer和transferFrom,如果我们要一次实现向成千上万个地址转账,那么,我们就需要产生上万个transfer交易,这未免太低效了。

所以,不少ERC20代币都实现了批量转账的接口。
如近期爆出漏洞的BEC(https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code)实现了batchTransfer函数。

SMT(https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code)也实现了allocateTokens函数。

他们都可以实现一笔以太坊交易(Transaction)完成对多个账户的代币转账或初始化。

本文提出了一种实现一对多转账的方法,该方法名称为transferMultiple

首先,本文默认已用了SafeMath库

library SafeMath {
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }
        uint256 c = a * b;
        assert(c / a == b);
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
}

接下来,transferMultiple

transferMultiple实现了从msg.sender向count个_tos地址转账,且_tos[i]获得_values[i]的代币。

首先,第一个for循环进行了前置检查,确保了每一个_tos地址都是非0地址,同时,计算了转账的总额,并将总额记录到total变量中。在计算过程中,为了防止溢出,我们采用了SafeMath库,并且每一次都要比较当前计算出来的总额total和上一笔总额total_prev,确保total大于等于total_prev,双重保证不会整数溢出导致转账故障。

其次,第二个for循环不直接调用transfer方法,而是直接修改内部变量,这是因为前置检查已经做了,如果再次调用transfer函数的话,会再次执行额外的不必要的前置检查,会增加消耗的gas。

function transferMultiple(address[] _tos, uint256[] _values, uint count)  payable public returns (bool success) {
        uint256 total = 0;
        uint256 total_prev = 0;
        uint i = 0;

        for(i=0;i= total_prev);
        }

        require(total <= balanceOf(msg.sender);

        for(i=0;i<=count-1;i++){
            balances[msg.sender] = SafeMath.sub(balances[msg.sender], _values[i]);
            balances[_tos[i]] = SafeMath.add(balances[_tos[i]], _values[i]);
            Transfer(msg.sender, _tos[i], _values[i]);
            //以上三行也可以替换为下一行,好处是不需要假设客户的余额保存在类型为mapping的balances变量中,坏处是会额外增加很多不必要的前置检查,额外消耗gas
            //transfer(_tos[i], _values[i]);
        }

        return true;
    }

大家一定很关心,那么,我用transferMultiple一次实现一对一万转账行不行呢。
实际测试表明,一对四十转账的时候,大约消耗的gas在130万左右,而截止目前本文写作之时,以太坊一个区块的gas上限大约为800万,所以,大家不难看出,一次实现一对二百四十转账就差不多将区块的gas上限占满了。如果一次转账的收款对象数量太多,完全会因为超出区块gas上限而导致交易无法成功。