修改go-ethereum源码以支持智能合约批量转账

智能合约单笔转账函数可以在单笔交易里给多个地址转账以太坊,批量转账可以在一个交易里面给多个账户转账,这样转账速度可以几十倍几百倍的提高。

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个账户,应该是比较合适的。









猜你喜欢

转载自blog.csdn.net/liuzhijun301/article/details/80163154