📜 ⬆️ ⬇️

Dive into Ethereum development. Part 2: Web3.js and gas

In the previous article, we described the deployment of contracts and interaction with them through the user interface of the Mist wallet, but this is not suitable for actual development. We need a library that will allow you to work with the blockchain from the code of the user application. In this article we will briefly review what the Web3.js library is like, having felt it from the Geth console. And one more important topic, which certainly interests not only developers, but also potential customers - how much are transactions on the blockchain, because each of them requires gas, which is bought for broadcasting.


We already know how to deploy contracts from Mist. But Mist is only an application that provides a graphical interface to the main functions. It works on the Geth client. Geth is included in the Ethereum protocol's Go implementation. Go-implementation is not the only one, one of the most common is for example Parity - Rust-implementation. It is worth noting that in Parity, you can use another test network, Kovan, using Proof-of-Authority — the same distribution algorithm for building blocks as Rinkeby (instead of Proof-of-Work in Ropsten). In addition, Parity does not use Mist, but a wallet on the web interface. But we will focus on the Go-implementation for now. Geth is the entry point to the Ethereum network. To demonstrate his work, we can use the command line provided to them. The command line interprets plain javascript. Let's try to access the same contract that we created through Mist in the previous article. To begin, save the address and interface of the contract in any file, they will be needed later. Since Mist uses Geth, we close Mist in order not to create conflicts (This does not apply to the Mist Windows implementation, which, as it turned out, requires running Geth to work).

"Hello command line!"


Run geth with the console (you can see how to install geth here ):
')
$ geth console --testnet 2>>geth.log 

The --testnet flag connects us to the Ropsten test network. To connect to Rinkeby, use the --rinkeby flag --rinkeby . Additionally, we redirect messages from the standard stream (stderr) of geth to the geth.log file, otherwise they will be interfered with in the console.
As a result, the team will display a welcome message. To begin with, let's try to find out the balance of our wallet by the following team (not necessarily ours, any other one, but we have our address: it was displayed in the greeting called coinbase):

 > eth.getBalance(eth.coinbase) 

Sample result: 22159430784000000000

The number is so big because it is displayed not in ether, but in wei - the smallest possible unit of measurement for the amount of ether, it is also used in the code when manipulating the ether. To display the number in the usual ether, as in the wallet Mist, you can add a conversion:

 > web3.fromWei( eth.getBalance(eth.coinbase) ) 22.159430784 

We proceed to the opening of the contract. Assign the contract address to a variable:

 > var address = "0x65cA73D13a2cc1dB6B92fd04eb4EBE4cEB70c5eC"; 

Assign the contract interface to a variable:

 > var abi = [ { "constant": false, "inputs": [ { "name": "newString", "type": "string" } ], "name": "setString", "outputs": [], "payable": false, "type": "function" }, { "constant": true, "inputs": [], "name": "getString", "outputs": [ { "name": "", "type": "string", "value": "Hello World!" } ], "payable": false, "type": "function" } ]; 

Create a contract object:

 > var contract = web3.eth.contract(abi); 

This object can be used to open an existing contract or to deploy a new one. In this case, we need the first, for this we execute the command:

 > var stringHolder = contract.at(address) 

Each command displays undefined - do not pay attention. Instead of the address and the interface, substitute your own values, and if you are on the same test network (Ropsten) as we are while creating our example, you can use our contract. Call the contract functions:

 > stringHolder.getString() "Hello World!" > stringHolder.setString("Hello my baby, hello my honey!"); Error: invalid address at web3.js:3879:15 at web3.js:3705:20 at web3.js:4948:28 at map (<native code>) at web3.js:4947:12 at web3.js:4973:18 at web3.js:4998:23 at web3.js:4061:16 at apply (<native code>) at web3.js:4147:16 

We see that the getString method worked correctly, and setString caused an error. In this case, this error occurs due to the fact that transactions must be carried out from any account that can pay off the air. Unlock the account with the following command (you will need to enter the password from the private key) and run setString again with an additional option, specifying from which account to perform the transaction:

 > web3.personal.unlockAccount(eth.coinbase); Unlock account 0x<  > Passphrase: true > stringHolder.setString("Hello my baby, hello my honey!", {from: eth.coinbase}); "0x5f9c3a61c79df36776713f7373b902feea802cf6d3903195f8070ff2d376c669" 

The transaction number is returned. By this number you can track the transaction on the website etherscan: ropsten.etherscan.io for Ropsten and rinkeby.etherscan.io for Rinkeby, by entering the transaction number in the search, or by running the command:

 > web3.eth.getTransaction("0x5f9c3a61c79df36776713f7373b902feea802cf6d3903195f8070ff2d376c669"); 

Do not forget to substitute the number of your transaction instead of ours.

See the structure with transaction details. Now you can run getString and see that the string has changed:

 > stringHolder.getString(); "Hello my baby, hello my honey!" 

How gas is consumed


In the last article we already wrote what gas is. For convenience, we recall:
The broadcast is needed for any operations to change data, they are paid for the so-called gas - an abstract unit of measure, which serves to assess the work required to complete the transaction. It is necessary for the independence of this assessment from the current market value of the air. When sending a transaction, you can specify how much air you pay for each unit of gas and the maximum amount of gas you are willing to pay. The more you allocate - the more priority your transaction is for potential miners. After all, in essence, the payment for gas is the payment for the work of miners to complete your transaction and include it in the next block. Therefore, when mining, in addition to a fixed fee for the block found - at the time of writing it is 5 airs - the miner also receives payment for transactions, as a rule it is a few hundredths of aired. The amount of gas per transaction depends on the computational complexity of operations on data.

In order to demonstrate how the payment and the gas flow occurs during the execution of the transaction, we will create a new contract. At the same time, we will use another compilation method and deploy - via the command line.

1. Deploy contract for demonstration


Consider the following contract:

 pragma solidity ^0.4.10; contract UselessWorker { int public successfullyExecutedIterations = 0; function doWork(int _iterations) { successfullyExecutedIterations = _iterations; for (int i = 0; i < _iterations; i++) { keccak256(i); } } } 

The contract contains only one doWork function, which takes the number of iterations int _iterations as a parameter, and then counts the keccak256 hash from the loop counter. So we can give a different amount of work and see how the amount of required gas depends on it. The only variable stored in the contract — successfullyExecutedIterations — is used to save the number of cycles performed during the last run. It is needed to demonstrate what happens in case of excess gas flow.

Save the contract text to a file UselessWorker.sol. To compile, we will use the solc compiler, solidity (you can find the installation instructions by reference ):

 $ solc --bin --abi UselessWorker.sol 

Using the --bin and --abi we tell the compiler to generate the binary code and interface. A command is given a response similar to the following:

 ======= UselessWorker.sol:UselessWorker ======= Binary: Contract JSON ABI [{"constant":true,"inputs":[],"name":"successfullyExecutedIterations","outputs":[{"name":"","type":"int256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_iterations","type":"int256"}],"name":"doWork","outputs":[],"payable":false,"type":"function"}] 

Run geth to do the deployment:

 $ geth console --testnet 2>>geth.log 

To begin with, assign the binary code and interface to the variables, copying them from the compiler output. Please note that you should add 0x before the binary code :

 > var bin = ""; > var abi = [{"constant":true,"inputs":[],"name":"successfullyExecutedIterations","outputs":[{"name":"","type":"int256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_iterations","type":"int256"}],"name":"doWork","outputs":[],"payable":false,"type":"function"}]; 

Create a contract object as in the case of opening:

 > var contract = web3.eth.contract(abi); 

Let's execute the contract. Deploy is also a transaction that must be performed on behalf of a specific account, consumes gas and requires waiting. Therefore, the contract contract, in addition to the constructor arguments, which in this case are not required, and the array of parameters (from, data, gas), accepts a callback (in this case, the simplest, issuing either the error text or the address of the contract). But you first need to unlock the account:

 > web3.personal.unlockAccount(eth.coinbase); 

Then you can execute the command with sending the transaction from this account:

 > var uselessWorker = contract.new( {from: eth.coinbase, data: bin, gas: 1000000}, function(e, contract) { if (e) { console.log(e); } else { if (contract.address) { console.log ("mined " + contract.address); } } }); 

Further expect a similar answer:

 mined 0xaad3bf6443621f24099ee4f51a22c8d7e9f63548 

This means that the contract is closed, you can call the functions of this contract:

 > uselessWorker.successfullyExecutedIterations(); 0 

Please note that we did not describe such a function in the contract; it is created automatically for each public field.

2. Experiments with gas flow


We start by calculating the approximate amount of gas that will be required when calling a function. To do this, you can call the estimateGas() method on the contract method of interest, and the parameters of the method of interest are passed as parameters in estimateGas . In our case, you can call this:

 > uselessWorker.doWork.estimateGas(1); 41801 > uselessWorker.doWork.estimateGas(2); 41914 > uselessWorker.doWork.estimateGas(3); 42027 

We see that each cycle according to calculations must spend 113 gas. How much will it cost on the air? To do this, you need to know what price of gas will be used. If it is not specified when sending a transaction, you can see the default value (immediately convert to ether):

 > web3.fromWei( eth.gasPrice ); 1e-7 

This means 1 gas by default in this case is 0.0000001 ether. This price is not fixed and changed even in the course of how we executed the current commands. Therefore, your values ​​are likely to be different. The commission for the transaction in this way will be equal to the price of gas multiplied by the amount of gas. We calculate the price for one cycle (the price includes not only the cycle itself, but also some initial price just for accepting a transaction):

 > web3.fromWei( eth.gasPrice * uselessWorker.doWork.estimateGas(1) ); "0.0041801" 

At this price, about 10,000 cycles should give a commission in the tenths of the ether. Check:

 > web3.fromWei( eth.gasPrice * uselessWorker.doWork.estimateGas(10000) ); "0.1171752" 

Really. But what happens if you specify not 10,000, for example, 1,000,000? The result will be the following:

 > web3.fromWei( eth.gasPrice * uselessWorker.doWork.estimateGas(1000000) ); "0.4704624" 

This disproportion arises from the fact that there is also the maximum amount of gas that can be consumed. This number also has a default value, but in this case it cannot be viewed. Let's see what happens if we explicitly set the gas limit, which we are ready to pay. But first, check to see if the price has changed:

 > web3.fromWei( eth.gasPrice ); 2.8e-7 

She grew up almost 3 times (it happened in less than half an hour). The default price is taken as the market price, therefore it changes dynamically. To keep the estimate for the same price that we used at the beginning, we will assign our price for gas (in wei) to a variable:

 > var fixedGasPrice = 100000000000; 

We calculate the price for 100 thousand cycles (a million can run too long) with our price, but with a modified gas limit (gas parameter).

 > web3.fromWei( fixedGasPrice * uselessWorker.doWork.estimateGas(100000, {gas: 100000000}) ); "1.1341816" 

Limit the amount of gas to 1000:

 > web3.fromWei( fixedGasPrice * uselessWorker.doWork.estimateGas(100000, {gas: 1000}) ); "0.0001" 

In this case, all the supplied gas is consumed. No changes on the blockchain will be saved, but the miner still gets paid, as he did the work until the gas ran out. Therefore, it is important to correctly specify the gas limit. The estimateGas function may not always provide reliable data, because its execution is based on the current state of the blockchain, which may be different during the execution of this transaction, which will result in a different gas flow.

Let us turn to the actual execution of the methods and comparison with the predicted values. First, let's see and remember the balance of your account:

 > initialBalance = eth.getBalance(eth.coinbase); 5006820644000000000 

Suppose we want to perform 10 cycles. Calculate how much gas should be required for this call:

 > uselessWorker.doWork.estimateGas( 10 ); 42818 

We use our fixedGasPrice price for this operation, but we will set the maximum amount of gas to 42000 (this is probably not enough, since it is less than the predicted value). Therefore, our payment, taking into account the consumption of all the gas provided, should be in wei:

 > predictedCost = 42000 * fixedGasPrice; 4200000000000000 

What will be on the air:

 > web3.fromWei(predictedCost); "0.0042" 

Perform a transaction with the task of our limit and price (but before that unlock the account):

 > web3.personal.unlockAccount(eth.coinbase); > transaction = uselessWorker.doWork( 10, { from: eth.coinbase, gas: 42000, gasPrice: fixedGasPrice } ); "0xc0590a2cf39c3e4339253ecf11d124177b75502cea368adcf30d1b7d6933ef5a" 

By the transaction number, you can track its status by running:

 > result = web3.eth.getTransactionReceipt(transaction); 

The result is a structure that looks like this:

 { blockHash: "0x91b63b43856e62fd26ad7f401bfe556cc100e8adf4b5ac510261e91adb9953a3", blockNumber: 1375978, contractAddress: null, cumulativeGasUsed: 740323, from: "0x334731990b420d7fe77347545c45a689becfca08", gasUsed: 42000, logs: [], logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", root: "0x07dc7178ac2abaa2460cd94d5eb7bf6d7ed9ed09e3e74a4b53321b5c210932c0", to: "0xaad3bf6443621f24099ee4f51a22c8d7e9f63548", transactionHash: "0xc0590a2cf39c3e4339253ecf11d124177b75502cea368adcf30d1b7d6933ef5a", transactionIndex: 1 } 

If, as a result, you have nil , then the transaction has not yet been added to the block, you must wait and repeat the command.

In this structure, we see that gasUsed gas gasUsed turned out to be 42,000, as expected. Check if our balance has changed:

 > web3.fromWei( initialBalance - eth.getBalance(eth.coinbase) ); "0.0042" 

Consumption turned out to be 0.0042 ether, as expected. Check if the data in the contract has changed:

 > uselessWorker.successfullyExecutedIterations(); 0 

Nothing was saved to the variable, although the assignment in the contract was done before the loop. It is thus seen that in the event of a gas shortage, the changes roll back completely. But this transaction is still added to the block, because the work was carried out, so we can see the block number and all other information. However, in the information we see no other signs of error, except that all the gas was spent. How to determine the status of the transaction in a rare, but possible, case when all the gas was consumed, but no more was needed? Unfortunately, there is no easier way than to look through the site (for example, here is the link for our transaction, you can search for any other transaction and see if it ended with an error), or re-simulate sending the same transaction using debag.

Check now what will happen if you specify the limit a little more than predicted. Also save the initial balance:

 > initialBalance = eth.getBalance(eth.coinbase); 5002620644000000000 

This time we will allocate 43,000 gas, we can expect a change in the balance

 > 43000 * fixedGasPrice; 4300000000000000 

Let's execute transaction with the task of the new limit:

 > web3.personal.unlockAccount(eth.coinbase); > transaction = uselessWorker.doWork( 10, { from: eth.coinbase, gas: 43000, gasPrice: fixedGasPrice } ); 

And we get the result of its implementation:

 > result = web3.eth.getTransactionReceipt(transaction); 

 { blockHash: "0xe9793206faf5e923042488d4312d542db2c5189d25a0014d894179fce222705d", blockNumber: 1376028, contractAddress: null, cumulativeGasUsed: 131780, from: "0x334731990b420d7fe77347545c45a689becfca08", gasUsed: 42817, logs: [], logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", root: "0x51fd139db608c2fa833e5de205497368362853b8e778787e85180e1cde206151", to: "0xaad3bf6443621f24099ee4f51a22c8d7e9f63548", transactionHash: "0xd69e46cbb9c139cc2c56ae4860b2e73e4581a97fc016c2848dc6bd399e9b5196", transactionIndex: 2 } 

Used 42817 gas (1 less than predicted).
Air consumption:

 > web3.fromWei( initialBalance - eth.getBalance(eth.coinbase) ); "0.0042817" 

Exactly as much as necessary to pay for used gas.

Are there any changes to the contract?

 > uselessWorker.successfullyExecutedIterations(); 10 

Yes, saved.

To be continued


That's all for now. You can see the geth documentation here .

Solidity documentation.

In geth, the web3.js library is used; documentation on it can be found here .
This is one of the most common libraries for connecting to the Ethereum-blockchain. We specialize in developing Ruby on Rails, so we set ourselves the goal of finding a suitable ruby ​​interface. In the next article, we will describe what comes of it.

Link to part 1

Source: https://habr.com/ru/post/336770/


All Articles