Pull to refresh

Погружение в разработку на Ethereum. Часть 2: Web3.js и газ

Reading time 12 min
Views 58K
В предыдущей статье мы описали деплой контрактов и взаимодействие с ними через пользовательский интерфейс кошелька Mist, но это не подходит для реальной разработки. Нужна библиотека, которая позволит работать с блокчейном из кода пользовательского приложения. В этой статье мы кратко рассмотрим что представляет собой библиотека Web3.js, пощупав ее из консоли Geth. И еще одна важная тема, которая наверняка интересует не только разработчиков, но и потенциальных заказчиков — сколько стоят транзакции на блокчейне, ведь каждая из них требует газа, который покупается за эфир.


Мы уже знаем как деплоить контракты из Mist. Но Mist — это лишь приложение, предоставляющее графический интерфейс к основным функциям. Работает оно на основе клиента Geth. Geth входит в Go-реализацию протокола Ethereum. Go-реализация не единственная, одной из распространенных является к примеру Parity — Rust-реализация. Стоит отметить, что в Parity можно использовать еще одну тестовую сеть — Kovan, использующую Proof-of-Authority — тот же алгоритм распределения работ по созданию блоков, что и Rinkeby (вместо Proof-of-Work в Ropsten). Кроме того для Parity используется не Mist, а кошелек на веб-интерфейсе. Но мы остановимся пока на Go-реализации. Geth является входной точкой в сеть Ethereum. Для демонстрации его работы мы можем воспользоваться предоставляемой им командной строкой. Командная строка интерпретирует обычный JavaScript. Попробуем получить доступ к тому же контракту, который мы создали через Mist в предыдущей статье. Для начала сохраним адрес и интерфейс контракта в какой-нибудь файл, они понадобятся позже. Так как Mist использует Geth, закроем Mist, чтобы не создавать конфликтов (Это не касается Windows-реализации Mist, которая, как выяснилось, требует запущенный Geth для работы).

«Hello command line!»


Запустим geth с консолью (как установить geth можно посмотреть здесь):

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

Флаг --testnet подключает нас к тестовой сети Ropsten. Для подключения к Rinkeby используйте вместо этого флаг --rinkeby. Дополнительно мы перенаправляем сообщения из стандартного потока (stderr) geth’а в файл geth.log, иначе они будут мешаться в консоли.
В результате работы команды выведется приветственное сообщение. Для начала попробуем узнать баланс нашего кошелька следующей командой (не обязательно нашего, можно любого другого, но у нас есть наш адрес: он отобразился в приветствии под названием coinbase):

> eth.getBalance(eth.coinbase)

Пример результата: 22159430784000000000

Число такое большое потому, что отображается не в ether, а в wei — минимально возможной единице измерения количества эфира, она же используется в коде при манипуляциях с эфиром. Для отображения числа в привычном ether, как в кошельке Mist, можно добавить преобразование:

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

Приступим к открытию контракта. Присвоим адрес контракта в переменную:

> var address = "0x65cA73D13a2cc1dB6B92fd04eb4EBE4cEB70c5eC";

Присвоим интерфейс контракта в переменную:

> 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" } ];

Создадим объект контракта:

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

Этот объект может использоваться для открытия существующего контракта либо для деплоя нового. В данном случае нам нужно первое, для этого выполним команду:

> var stringHolder = contract.at(address)

Каждая команда выводит undefined — не обращайте внимания. Вместо адреса и интерфейса подставьте свои значения, а если вы находитесь в той же тестовой сети (Ropsten), что и мы во время создания нашего примера — то сможете использовать наш контракт. Вызовем функции контракта:

> 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

Видим что метод getString отработал корректно, а setString — вызвал ошибку. В данном случае эта ошибка происходит из-за того, что транзакции должны выполняться от какого-либо аккаунта, который может расплатиться эфиром. Разблокируйте аккаунт следующей командой (потребуется ввести пароль от приватного ключа) и запустите setString снова с дополнительной опцией, задающей от какого аккаунта выполнять транзакцию:

> web3.personal.unlockAccount(eth.coinbase);
Unlock account 0x<адрес вашего аккаунта>
Passphrase:
true

> stringHolder.setString("Hello my baby, hello my honey!", {from: eth.coinbase});
"0x5f9c3a61c79df36776713f7373b902feea802cf6d3903195f8070ff2d376c669"

Возвращается номер транзакции. По этому номеру можно отследить транзакцию на сайте etherscan: ropsten.etherscan.io для Ropsten и rinkeby.etherscan.io для Rinkeby, введя номер транзакции в поиске, либо выполнив команду:

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

Не забудьте подставить номер своей транзакции вместо нашего.

Увидите структуру с подробностями транзакции. Теперь можно выполнить getString и увидеть, что строка изменилась:

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

Как расходуется газ


В прошлой статье мы уже писали что такое газ. Для удобства напомним:
Эфир нужен для любых операций по изменению данных, им оплачивается так называемый gas — абстрактная единица измерения, которая служит для оценки требующейся работы по выполнению транзакции. Она нужна для независимости этой оценки от текущей рыночной стоимости эфира. При отправке транзакции можно задать сколько эфира вы платите за каждую единицу газа и максимальное количество газа, которое вы готовы оплатить. Чем больше вы выделяете — тем более приоритетна ваша транзакция для потенциальных майнеров. Ведь по сути плата за gas — это оплата работы майнеров по выполнению вашей транзакции и включению ее в очередной блок. Поэтому при майнинге кроме фиксированной платы за найденный блок — на момент написания это 5 эфиров, — майнер также получает плату за транзакции, как правило это несколько сотых эфира. Количество газа за транзакцию зависит от вычислительной сложности операций над данными.

Для того чтобы наглядно продемонстрировать как происходит оплата и расход газа при выполнении транзакции создадим новый контракт. Заодно будем использовать другой метод компиляции и деплоя — через командную строку.

1. Деплой контракта для демонстрации


Рассмотрим следующий контракт:

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

Контракт содержит лишь одну функцию doWork, которая принимает количество итераций int _iterations в качестве параметра, после чего считает хеш keccak256 от счетчика цикла. Так мы сможем давать разное количество работы и смотреть как от этого зависит количество требуемого газа. Единственная переменная, сохраняющаяся в контракте — successfullyExecutedIterations, — служит для сохранения количества выполненных циклов при последнем запуске. Она нужна для демонстрации того, что происходит в случае превышения расхода газа.

Сохраним текст контракта в файл UselessWorker.sol. Для компиляции воспользуемся solc — компилятором solidity (инструкцию по установке можете найти по ссылке):

$ solc --bin --abi  UselessWorker.sol

Флагами --bin и --abi говорим компилятору генерировать бинарный код и интерфейс, На команду выдается ответ подобный следующему:

======= UselessWorker.sol:UselessWorker =======
Binary:
606060405260008055341561001357600080fd5b5b60fd806100226000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680637ec94a1314604757806397286e7114606d575b600080fd5b3415605157600080fd5b6057608d565b6040518082815260200191505060405180910390f35b3415607757600080fd5b608b60048080359060200190919050506093565b005b60005481565b600081600081905550600090505b8181121560cc5780604051808281526020019150506040518091039050505b808060010191505060a1565b5b50505600a165627a7a72305820b0949297821556e9ed7f4941b7ae793486db6ee48e86486dc58fa3040b224d160029
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"}]

Запустим geth для выполнения деплоя:

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

Для начала присвоим в переменные бинарный код и интерфейс, скопировав их из вывода компилятора. Обратите внимание, что перед бинарным кодом надо добавить 0x:

> var bin = "0x606060405260008055341561001357600080fd5b5b60fd806100226000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680637ec94a1314604757806397286e7114606d575b600080fd5b3415605157600080fd5b6057608d565b6040518082815260200191505060405180910390f35b3415607757600080fd5b608b60048080359060200190919050506093565b005b60005481565b600081600081905550600090505b8181121560cc5780604051808281526020019150506040518091039050505b808060010191505060a1565b5b50505600a165627a7a72305820b0949297821556e9ed7f4941b7ae793486db6ee48e86486dc58fa3040b224d160029";

> 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"}];

Создадим объект контракта как и в случае открытия:

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

Выполним деплой контракта. Деплой — это тоже транзакция, которая должна выполняться от имени определенного аккаунта, расходует газ и требует ожидания. Поэтому деплой контракта, кроме аргументов конструктора, которые в данном случае не требуются, и массива параметров (from, data, gas), принимает коллбэк (в данном случае простейший, выдающий либо текст ошибки, либо адрес контракта). Но сначала требуется разблокировать аккаунт:

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

После чего можно выполнять команду с отправкой транзакции от этого аккаунта:

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

Далее ожидайте подобного ответа:

mined 0xaad3bf6443621f24099ee4f51a22c8d7e9f63548

Это означает, что контракт задеплоен, можно вызывать функции этого контракта:

> uselessWorker.successfullyExecutedIterations();
0

Обратите внимание, что мы не описывали такой функции в контракте, она создается автоматически для каждого public поля.

2. Эксперименты с расходом газа


Начнем с вычисления примерного количества газа, которое потребуется при вызове функции, для этого можно вызывать метод estimateGas() на интересующем методе контракта, при этом в качестве параметров в estimateGas передаются параметры интересующего метода. В нашем случае можно вызывать так:

> uselessWorker.doWork.estimateGas(1);
41801

> uselessWorker.doWork.estimateGas(2);
41914

> uselessWorker.doWork.estimateGas(3);
42027

Видим, что каждый цикл по расчетам должен тратить 113 газа. Сколько это будет стоить в эфире? Для этого нужно знать какая цена газа будет использоваться. Если она не указывается при отправке транзакции, можно посмотреть значение по умолчанию (сразу делаем преобразование в ether):

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

Это значит 1 газ по умолчанию в данном случае стоит 0.0000001 эфира. Эта цена не фиксирована и изменялась даже в процессе того как мы выполняли текущие команды. Поэтому ваши значения скорее всего будут отличаться. Комиссия за транзакцию таким образом будет равна цене на газ умноженной на количество газа. Рассчитаем цену для одного цикла (в цену входит не только сам цикл, но и некоторая начальная цена просто за принятие транзакции):

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

При такой цене около 10000 циклов должно дать комиссию в районе десятых долей эфира. Проверим:

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

Действительно. Но что будет если указать не 10000, а например 1000000? Результат будет следующий:

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

Такая непропорциональность возникает из-за того, что есть еще и максимальное количество газа, которое может быть израсходовано. У этого количества так же есть значение по умолчанию но в данном случае его нельзя посмотреть. Посмотрим что будет, если явно задавать лимит газа, который мы готовы оплачивать. Но сначала проверим, не изменилась ли цена:

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

Выросла почти в 3 раза (у нас это произошло менее чем за пол часа). Цена по умолчанию принимается рыночной цене, поэтому динамически меняется. Чтобы оценка сохранялась для той же цены, что мы использовали в начале, присвоим свою цену за газ (в wei) в переменную:

> var fixedGasPrice = 100000000000;

Рассчитаем цену за 100 тысяч циклов (миллион может выполняться слишком долго) с нашей ценой, но измененным лимитом газа (параметр gas).

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

Ограничим количество газа до 1000:

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

В этом случае расходуется весь предоставляемый газ. Никаких изменений на блокчейне не будет сохранено, но майнер все равно получает оплату, так как выполнял работу пока не кончился газ. Поэтому важно правильно указывать лимит газа. Функция estimateGas не всегда может предоставлять достоверные данные, потому что ее выполнение опирается на текущее состояние блокчейна, которое во время выполнения настоящей транзакции может быть другим, что повлечет другой расход газа.

Перейдем к реальному исполнению методов и сравнению с предсказанными значениями. Сначала посмотрим и запомним баланс своего аккаунта:

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

Допустим мы хотим выполнить 10 циклов. Вычислим сколько газа должно потребоваться на этот вызов:

> uselessWorker.doWork.estimateGas( 10 );
42818

Используем для этой операции нашу цену fixedGasPrice, но зададим максимальным количеством газа 42000 (этого скорее не хватит, так как меньше предсказанного значения). Поэтому наша оплата, с учетом расхода всего предоставленного газа, должна составить в wei:

> predictedCost = 42000 * fixedGasPrice;
4200000000000000

Что в эфире будет:

> web3.fromWei(predictedCost);
"0.0042"

Выполним транзакцию с заданием нашего лимита и цены (но перед этим разблокируем аккаунт):

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

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

По номеру транзакции можно отследить ее статус, выполнив:

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

В результат записывается структура, выглядящая следующим образом:

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

Если в результате у вас nil — значит транзакция еще не добавилась в блок, надо подождать и повторить команду.

В этой структуре мы видим, что расход газа gasUsed оказался 42000, как и ожидалось. Проверим, изменился ли наш баланс:

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

Расход оказался 0.0042 эфира, как и ожидалось. Проверим, изменились ли данные в контракте:

> uselessWorker.successfullyExecutedIterations();
0

В переменную ничего не сохранилось, несмотря на то, что присваивание в контракте выполнялось перед циклом. Таким образом видно, что в случае нехватки газа изменения откатываются полностью. Но эта транзакция все равно добавляется в блок, ведь работа была проведена, поэтому мы можем видеть номер блока и всю остальную информацию. Однако в информации мы не видим никаких других признаков ошибки, кроме того, что был потрачен весь газ. Как же определить статус транзакции в редком, но возможном случае, когда израсходован весь газ, но больше и не требовалось? К сожалению пока нет более простого способа чем либо смотреть через сайт (например вот ссылка для нашей транзакции, можно искать любую другую транзакцию и видеть завершилась ли она с ошибкой), либо заново симулировать отправку той же самой транзакции с помощью средств дебага.

Проверим теперь что будет, если указать лимит чуть больше предсказанного. Также сохраним начальный баланс:

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

На этот раз выделим 43000 газа, можно ожидать изменение баланса

> 43000 * fixedGasPrice;
4300000000000000

Выполним транзакцию с заданием нового лимита:

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

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

И получим результат ее выполнения:

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

Использовано 42817 газа (на 1 меньше предсказанного).
Расход эфира:

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

Ровно столько, сколько необходимо для оплаты использованного газа.

Сохранены ли изменения в контракте?

> uselessWorker.successfullyExecutedIterations();
10

Да, сохранены.

Продолжение следует


На этом пока все. Документацию по geth можно увидеть здесь.

Документация по Solidity.

В geth используется web3.js библиотека, документацию по ней можно посмотреть здесь.
Это одна из самых распространенных библиотек для подключения к Ethereum-блокчейну. Мы же специализируемся на разработке на Ruby on Rails, поэтому задаемся целью найти подходящий ruby-интерфейс. В следующей статье мы опишем что из этого выйдет.

Ссылка на часть 1
Tags:
Hubs:
+17
Comments 7
Comments Comments 7

Articles