智能合约单笔转账函数可以在单笔交易里给多个地址转账以太坊,批量转账可以在一个交易里面给多个账户转账,这样转账速度可以几十倍几百倍的提高。
1 智能合约以及java端代码
合约代码如下:function multiTransferEther(address[] addrs,uint256[] scores,uint256 sum,uint256 amount) public view returns(bool) { require(sum>0 && amount>0 && addrs.length>0 && addrs.length==scores.length); for(uint j=0;j<addrs.length;j++){ uint256 val = amount.mul(scores[j]).div(sum); require (val>0 && this.balance>=val); addrs[j].transfer(val); } return true; }
java端使用web3j来访问geth代码如下:
//调用合约的算法函数来转账以太币 //fromAddress:转出方地址 toAddress:转入方地址 //ratio:分子 sum:分母 amount:分配额度,转出金额=ratio*amount/sum //password:fromAddress的密码 coinTypeAddress:合约地址 public static String multiTransferEther(String fromAddress, List<Address> toAddressList, List<Uint256> scoresList,BigInteger sum,BigInteger amount,String password,String coinTypeAddress) throws Exception{ EthGetTransactionCount ethGetTransactionCount = getWeb3jClient().ethGetTransactionCount( fromAddress, DefaultBlockParameterName.LATEST).sendAsync().get(); BigInteger nonce = ethGetTransactionCount.getTransactionCount(); BigInteger gasPrice = Contract.GAS_PRICE; BigInteger gasLimit = Contract.GAS_LIMIT.divide(new BigInteger("5")); List<Type> inputParameters = new ArrayList<>(); inputParameters.add(new DynamicArray(toAddressList)); inputParameters.add(new DynamicArray(scoresList)); inputParameters.add(new Uint256(sum)); inputParameters.add(new Uint256(amount)); Function function = new Function("batchTransferEther", inputParameters, Collections.<TypeReference<?>>emptyList()); String functionEncoder = FunctionEncoder.encode(function); //PersonalUnlockAccount personalUnlockAccount = getEthparity().personalUnlockAccount(fromAddress, password).sendAsync().get(); PersonalUnlockAccount personalUnlockAccount = getWeb3jClient().personalUnlockAccount(fromAddress,password).send(); if (personalUnlockAccount.accountUnlocked()) { // send a transaction Transaction transaction = Transaction.createFunctionCallTransaction( fromAddress, nonce, gasPrice, gasLimit, coinTypeAddress, new BigInteger("0"), functionEncoder); gNoce = gNoce.add(new BigInteger("1")); EthSendTransaction transactionResponse = getWeb3jClient().ethSendTransaction(transaction).sendAsync().get(); if(transactionResponse.hasError()){ String message=transactionResponse.getError().getMessage(); BigInteger afterbalance=getSmartChartBalance(fromAddress,coinTypeAddress); System.out.println("transaction failed,info:"+message); return message; }else{ String hash=transactionResponse.getTransactionHash(); return hash; } } return null; } }
采用这种方法,可以一次往多个地址里面转账,比如一次往60个地址里转账,效率是非常高的。但是一次往90个地址里转账,会发现转账不成功。java端和geth端都不会报错,什么错误提示都没有。看来一次转账过多会出问题,那么这个边界在哪里?
2 批量转账的极限在哪里
对于3个地址的交易,生成的交易数据如下:
geth客户端将调用智能合约的参数都保存在Input数组里面,web3j调用时,需要在http请求里面讲Input数据打包发送给geth客户端。multiTransferEther函数接受的参数包括地址数组addrs和积分数组scores。数组长度就是接受转账的账号个数N。N越大,转账效率越高。N当然越大越好。但是N越大,http请求包越大。以太坊里面单个http请求包的参数大小限制是1Mbit,也就是128K字节。所以N受到http包体大小128K的限制。另外一个限制是收到以太坊transaction对象size的限制。以太坊源码里面tx_pool.go里面的函数validateTx限制transaction对象大小是32K。
// core/tx_pool.go的validateTx函数里有:
func (pool *TxPool) validateTx(tx*types.Transaction, local bool) error {
..........
iftx.Size() > 32*1024 {
returnErrOversizedData
}
........
}
geth源码还用单笔交易的gas限制了交易数据不能太大。另外一个限制是在智能合约执行时用智能合约的gas限制智能合约交易的大小。如果这三个判断出现问题,就会抛出错误,交易就会被回滚。
3 修改源码以扩大单笔交易量
(1)修改单笔交易size限制。
tx_pool.go中函数validateTx中,有代码:
if(tx.Size() > 32*1024) {
returnErrOversizedData
}
将32改成320,即从32K改成320K。
(2)修改计算交易gas费用的函数
state_transition.go中函数IntrinsicGas():
func IntrinsicGas(data []byte, contractCreation, homestead bool) (uint64, error) { // Set the starting gas for the raw transaction var gas uint64 if contractCreation && homestead { gas = params.TxGasContractCreation } else { gas = params.TxGas } // Bump the required gas by the amount of transactional data if len(data) > 0 { // Zero and non-zero bytes are priced differently var nz uint64 for _, byt := range data { if byt != 0 { nz++ } } // Make sure we don't exceed uint64 for all data combinations if (math.MaxUint64-gas)/params.TxDataNonZeroGas < nz { return 0, vm.ErrOutOfGas } gas += nz * params.TxDataNonZeroGas z := uint64(len(data)) - nz if (math.MaxUint64-gas)/params.TxDataZeroGas < z { return 0, vm.ErrOutOfGas } gas += z * params.TxDataZeroGas } return gas, nil }
该函数根据交易字节码来计算交易需要消耗的gas。将params.TxDataNonZeroGas和params.TxDataZeroGas都修改为0,这俩个参数在protocol_params.go中。当修改为0后,97行和103行的除法会出问题,将除法改成乘法即可。修改后的函数:
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data. func IntrinsicGas(data []byte, contractCreation, homestead bool) (uint64, error) { // Set the starting gas for the raw transaction var gas uint64 if contractCreation && homestead { gas = params.TxGasContractCreation } else { gas = params.TxGas } // Bump the required gas by the amount of transactional data if len(data) > 0 { // Zero and non-zero bytes are priced differently var nz uint64 for _, byt := range data { if byt != 0 { nz++ } } // Make sure we don't exceed uint64 for all data combinations //if (math.MaxUint64-gas)/params.TxDataNonZeroGas < nz {//lzj change if (math.MaxUint64-gas) < nz *params.TxDataNonZeroGas{ return 0, vm.ErrOutOfGas } gas += nz * params.TxDataNonZeroGas z := uint64(len(data)) - nz //if (math.MaxUint64-gas)/params.TxDataZeroGas < z { //lzj change if (math.MaxUint64-gas) < z *params.TxDataZeroGas{ return 0, vm.ErrOutOfGas } gas += z * params.TxDataZeroGas } return gas, nil }
(3)修改运行智能合约交易时的gas限制
调用智能合约函数,最后的合约执行函数在interpreter.go的run函数中:
/ Run loops and evaluates the contract's code with the given input data and returns // the return byte-slice and an error if one occurred. // // It's important to note that any errors returned by the interpreter should be // considered a revert-and-consume-all-gas operation except for // errExecutionReverted which means revert-and-keep-gas-left. func (in *Interpreter) Run(contract *Contract, input []byte) (ret []byte, err error){ // Increment the call depth which is restricted to 1024 in.evm.depth++ defer func() { in.evm.depth-- }() // Reset the previous call's return data. It's unimportant to preserve the old buffer // as every returning call will return new data anyway. in.returnData = nil // Don't bother with the execution if there's no code. if len(contract.Code) == 0 { return nil, nil } ........ cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize) if err != nil || !contract.UseGas(cost) { return nil, ErrOutOfGas } ....... }
修改contract.UseGas函数:
// UseGas attempts the use gas and subtracts it and returns true on success func (c *Contract) UseGas(gas uint64) (ok bool) { if c.Gas < gas { //return false //need change return true } c.Gas -= gas return true }
现在可以愉快的转账了,用web3j一次转账900个账号:
void batchTransferEther() throws Exception { List<Address> addrs = new ArrayList<>(); List<Uint256> scores = new ArrayList<>(); for(int i=0;i<300;i++) { addrs.add(new MyAddress(addr1)); addrs.add(new MyAddress(addr2)); addrs.add(new MyAddress(addr3)); scores.add(new Uint256(100)); scores.add(new Uint256(100)); scores.add(new Uint256(100)); } multiTransferEther(addr0,addrs,scores,new BigInteger("100000"),Convert.toWei("300", Convert.Unit.ETHER).toBigInteger(),"123",contract_addr); }
转账前addr1有余额190.2ether,现在转账了300*100*300/100000=90ether,查看结果:
转账成功。注意查看txSize 当时是57.91kB。现在用web3j一次转账1200个账号,java控制台报了个Invalid response received:okhttp3.internal.http.RealResponseBody@1b1422ce的错误:
继续跟踪错误,java源码的HttpService.java报错Request Entity Too Large:
此时请求Http请求体数据154251字节,超过128K报错。
批量转账800个账户,应该是比较合适的。