Modify go-ethereum source code to support smart contract batch transfers

The smart contract single transfer function can transfer Ethereum to multiple addresses in a single transaction, and batch transfers can transfer funds to multiple accounts in one transaction, so that the transfer speed can be increased by dozens of times and hundreds of times.

1 Smart contract and java side code

The contract code is as follows:
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;
  }

The java side uses web3j to access the geth code as follows:

//Call the algorithm function of the contract to transfer ether
    //fromAddress: address of the transfer party toAddress: address of the transfer party
    //ratio: numerator sum: denominator amount: allocation amount, transfer amount = ratio*amount/sum
    //password: password fromAddress coinTypeAddress: contract address
    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;
    }
}

Using this method, you can transfer money to multiple addresses at a time, such as transferring money to 60 addresses at a time, which is very efficient. However, if you transfer money to 90 addresses at a time, you will find that the transfer is unsuccessful. Neither the java side nor the geth side will report an error, and there is no error message. It seems that too many transfers at one time will cause problems, so where is this boundary?

2 Where is the limit of batch transfer

For transactions with 3 addresses, the generated transaction data is as follows:


The geth client saves the parameters for calling the smart contract in the Input array. When web3j is called, it needs to package the Input data in the http request and send it to the geth client. The parameters accepted by the multiTransferEther function include the address array addrs and the integral array scores. The length of the array is the number N of accounts that accept transfers. The larger the N, the higher the transfer efficiency. Of course, the bigger the N, the better. But the larger N, the larger the http request packet. The parameter size limit of a single http request packet in Ethereum is 1Mbit, which is 128K bytes. So N is limited by the http package size of 128K. Another limitation is the size of the Ethereum transaction object received. The function validateTx in tx_pool.go in the Ethereum source code limits the transaction object size to 32K.

// The validateTx function of core/tx_pool.go contains:

func (pool *TxPool) validateTx(tx*types.Transaction, local bool) error {

         ..........

         iftx.Size() > 32*1024 {

                   returnErrOversizedData

         }

         ........

}

The geth source code also uses the gas of a single transaction to limit the transaction data to not be too large. Another limitation is the use of smart contract gas to limit the size of smart contract transactions during smart contract execution. If there is a problem with these three judgments, an error will be thrown and the transaction will be rolled back.

3 Modify the source code to expand the single transaction volume

(1) Modify the size limit of a single transaction.

         In the function validateTx in tx_pool.go, there is code:

         if(tx.Size() > 32*1024) {

                   returnErrOversizedData

         }

         Change 32 to 320, that is, change from 32K to 320K.

(2) Modify the function that calculates the transaction gas cost

         The function IntrinsicGas() in state_transition.go:

func IntrinsicGas(data []byte, contractCreation, homestead bool) (uint64, error) {
	// Set the starting gas for the raw transaction
	was 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
}

This function calculates the gas consumed by the transaction based on the transaction bytecode. Modify both params.TxDataNonZeroGas and params.TxDataZeroGas to 0, these two parameters are in protocol_params.go. When it is changed to 0, there will be a problem with the division of lines 97 and 103, and the division can be changed to multiplication. Modified function:

// 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
	was 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) Modify the gas limit when running smart contract transactions

         Call the smart contract function, and the final contract execution function is in the run function of interpreter.go:

/ 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
	}
	.......
}

Modify the contract.UseGas function:

// 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
}

Now you can transfer money happily, use web3j to transfer 900 accounts at a time:

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);
    }

Before the transfer, addr1 had a balance of 190.2ether, and now the transfer is 300*100*300/100000=90ether. Check the result:


The transfer was successful. Note that the txSize was 57.91kB at the time. Now using web3j to transfer 1200 accounts at a time, the java console reported an error of Invalid response received:okhttp3.internal.http.RealResponseBody@1b1422ce:

Continue to track the error, the HttpService.java of the java source code reports the error Request Entity Too Large:

At this time, the Http request body data is 154251 bytes, and an error is reported if it exceeds 128K.

It should be more appropriate to transfer 800 accounts in batches.









Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326010239&siteId=291194637