Разработка децентрализованных приложений на платформе Ethereum: быстрый старт | Часть 2

Примерное время чтения: 7 мин.

Ты че самый умный [контракт] ?

Введение

Продолжаю писать про Ethereum и умные контракты. Погружение в Solidity.

Повестка на сегодня: немного теории по Ethereum’у, память аккаунтов, call, модифайеры, библиотеки.

Цена Wei

В рамках Ethereum’а, как вычислительной системы 1ETH - это очень много, столько газа может потреблять сложнейший контракт, а цены на деплой и общение с обычными контрактиками, указываеются в Wei’ях.
Создатели Эфириума идейные люди, название каждого ‘деления’ от ETH - имена людей, добившихся заметного прогресса в области криптографии и криптовалют.
Wei - это 1/10^18(ETH). Мало.

А в рамках Ethereum’а, как валюты, заметно падение по отношению к доллару. Сейчас стоимость одной эфирки~180$, в то время как 12 июня (месяц назад) она почти достигла 400$.

Таким образом 1 Wei сейчас стоит примерно 180/10^18*60 = 1.08*10^(-13) Рубля.

Память контрактов в EVM

Контракт может хранить данные в: Стэке, Памяти и Хранилище (Stack, Memory, Storage).
Хранилище (Storage) - собственная память контракта, дорогая для чтения и записи. Контракт имеет доступ только к своему Хранилищу.
Память (Memory) - память, которая обнуляется при каждом вызове контракта. Цена на нее растет в квадратичной зависимости от размера.
Стэк (Stack) - память, в который совершает вычисления EVM.

Разминка

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pragma solidity ^0.4.0;
contract test {
// Контракт для демонстрации работы modifier'ов
// altjsus@gmail.com
bool test_bool1 = false;
bool test_bool2 = true;
modifier test_modifier(bool _test_bool){
// modifier - механизм для изменении тела существующих функций на лету.
// например может использоваться для "управления доступом"
// может принимать какое-то значение (bool _test_bool), может не принимать
if ( _test_bool == false ) throw;
// throw - выкидывает ошибку, прекращает выполнение
_;
// _ - специальное обозначние для продолжения кода функции,
// к которой был применен modifier
}
event success(string sucess_message);
function test1(uint input1) test_modifier(test_bool1) returns (uint){
// пример того, как используются modifier'ы
success("Noice!");
return input1;
}
function test2(uint input2) test_modifier(test_bool2) returns (uint){
success("Noice!");
return input2;
}
}

Очевидно, что после деплоя контракта test1() всегда будет ‘падать’, а test2() возвращать переданное значение. Проверим?

1
2
> browser_test_sol_test.test1.call(666)
0

Окей, а без call?

1
2
> browser_test_sol_test.test1(666)
"0x2688521156dc7baa1a09d375f88d99b486660c9dc0997da9c7dfb93db2ac129a"

Майнинг…

1
> eth.getTransaction("0x2688521156dc7baa1a09d375f88d99b486660c9dc0997da9c7dfb93db2ac129a")

Вопрос? Что ожидаемо увидеть здесь? Пустую транзакцию? Пустой input, превышение требуемого газа над затраченным?
Ничего подобного, вернется ‘здоровая’ транзакция, со своим местом в блокчейне.
В таком случае как проверить ее успешность?

Для этого в данном примере я воспользовался event’ом (уверен, что это не единственный способ).
Если транзация вызывает какое-либо событие об это можно узнать из логов транзакции, которые можно получить с помощью eth.getTransactionReceipt("") - чека транзакции.

1
> eth.getTransactionReceipt("0x2688521156dc7baa1a09d375f88d99b486660c9dc0997da9c7dfb93db2ac129a")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
blockHash: "0x56b9ba8fb05cb0f3c62a9535037049e60e9163589cf60590f8d3e22f4d0f5ebb",
blockNumber: 273,
contractAddress: null,
cumulativeGasUsed: 90000,
from: "0xbd0212a4e65c46bd7536cc8ba38a83bafb2801ef",
gasUsed: 90000,
logs: [],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
root: "0x68cbe9bcfada3a36a319e14f7b7f2e5ae8050f4f2f67beec4b4adb9d3b541c5e",
to: "0x52ec16e00319068ef07d0f012e9b6abd75a9d7cc",
transactionHash: "0x2688521156dc7baa1a09d375f88d99b486660c9dc0997da9c7dfb93db2ac129a",
transactionIndex: 0
}

Логи пусты - значит event не триггернулся.

В свою очередь второй метод:

1
2
> browser_test_sol_test.test2.call(666)
666

Работает по call. При этом логи транзакции будут похожи на:

1
2
> browser_test_sol_test.test2()
"0x46d4301299f7c0746ee664d0c064860e27992d1235d9d5a19e7e4e5df952e89e"

Майнинг…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> eth.getTransactionReceipt("0x46d4301299f7c0746ee664d0c064860e27992d1235d9d5a19e7e4e5df952e89e").logs
[{
address: "0x52ec16e00319068ef07d0f012e9b6abd75a9d7cc",
blockHash: "0x39dbd318f8a5341edd5608d7728e5e0cfba3cc10ddb18e326c5b72ebd45f49d2",
blockNumber: 275,
data: "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064e6f696365210000000000000000000000000000000000000000000000000000",
logIndex: 0,
removed: false,
topics: ["0x68f068b2f0d02fca99bc8285b62fce8668fe16b7c6a810351ea25bff358c0dd8"],
transactionHash: "0x46d4301299f7c0746ee664d0c064860e27992d1235d9d5a19e7e4e5df952e89e",
transactionIndex: 0
}]
> console.log(web3.toAscii(eth.getTransactionReceipt("0x46d4301299f7c0746ee664d0c064860e27992d1235d9d5a19e7e4e5df952e89e").logs[0].data))
Noice!

Нойс!

По поводу событий: хочу упомянуть, что web3 позволяет навешать на события (event) watch - и логировать сообщение после того, как сработало событие.

1
> browser_test_sol_test.success()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
callbacks: [],
filterId: "0x483c0939bbaf1d1ab546416764a97e11",
getLogsCallbacks: [],
implementation: {
getLogs: function(),
newFilter: function(),
poll: function(),
uninstallFilter: function()
},
options: {
address: "0x52ec16e00319068ef07d0f012e9b6abd75a9d7cc",
from: undefined,
fromBlock: undefined,
to: undefined,
toBlock: undefined,
topics: ["0x68f068b2f0d02fca99bc8285b62fce8668fe16b7c6a810351ea25bff358c0dd8"]
},
pollFilters: [],
requestManager: {
polls: {},
provider: {
newAccount: function(),
send: function github.com/ethereum/go-ethereum/console.(*bridge).Send-fm(),
sendAsync: function github.com/ethereum/go-ethereum/console.(*bridge).Send-fm(),
sign: function(),
unlockAccount: function()
},
timeout: null,
poll: function(),
reset: function(keepIsSyncing),
send: function(data),
sendAsync: function(data, callback),
sendBatch: function(data, callback),
setProvider: function(p),
startPolling: function(data, pollId, callback, uninstall),
stopPolling: function(pollId)
},
formatter: function(),
get: function(callback),
stopWatching: function(callback),
watch: function(callback)
}

Список полезных функций событий.

Контракт “Рейтинг”

Предисловие

Вдохновился на написание данного контракта первой серией третьего сезона сериала “Черное зеркало” (s03ep1):

Благодаря технологиям дополненной реальности люди способны в непрерывном режиме видеть социальные профили друг друга и выставлять оценки за любой поступок. При этом каждый человек оценивается сообразно своей усреднённой оценке: людям со статусом меньше 3,0 может быть запрещён доступ в некоторые общественные места, «двойки» считаются социопатами, «единички» — маньяками, а люди со статусом 4,5 и выше, как более ценные члены общества пользуются различными льготами и преимуществами. - Wiki)

Код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
pragma solidity ^0.4.12;
// В Solidity нельзя напрямую сравнивать строки :(
// Для этого подключаю библиотеку StringUtils
library StringUtils {
// source https://github.com/ethereum/dapp-bin/blob/master/library/stringUtils.sol
function compare(string _a, string _b) returns (int) {
bytes memory a = bytes(_a);
bytes memory b = bytes(_b);
uint minLength = a.length;
if (b.length < minLength) minLength = b.length;
for (uint i = 0; i < minLength; i ++)
if (a[i] < b[i])
return -1;
else if (a[i] > b[i])
return 1;
if (a.length < b.length)
return -1;
else if (a.length > b.length)
return 1;
else
return 0;
}
function equal(string _a, string _b) returns (bool) {
return compare(_a, _b) == 0;
}
function indexOf(string _haystack, string _needle) returns (int)
{
bytes memory h = bytes(_haystack);
bytes memory n = bytes(_needle);
if(h.length < 1 || n.length < 1 || (n.length > h.length))
return -1;
else if(h.length > (2**128 -1))
return -1;
else
{
uint subindex = 0;
for (uint i = 0; i < h.length; i ++)
{
if (h[i] == n[0])
{
subindex = 1;
while(subindex < n.length && (i + subindex) < h.length && h[i + subindex] == n[subindex])
{
subindex++;
}
if(subindex == n.length)
return int(i);
}
}
return -1;
}
}
}
contract Rate {
using StringUtils for StringUtils;
// укзаание на использоание библиотеки StringUtils
// вообще-то библиотеки должны подключаться с гитхаба
// директивой ` import "github.com/..."; `,
// по какой-то причине у меня не вышло заставить ее работать так
enum Rating {The_worst, Bad, Not_bad, Nice, Awesome} Rating rating;
// перечисление возможных рейтингов от 0 до 5
modifier reviewer_registred(){
// modifier проверяет есть ли пользователь,
// с адресом отправителя
// в маппинге пользователей
if( find_reviewer_id_by_address(msg.sender) > 0 ){
_;
}else{
throw;
}
}
struct Item{
// сущность для оценкию name - идентификатор.
// можно присвативать name - фио, пасспортные данные, адрес, id вживленного в кожу чипа,
// надежнее всего последовательность ДНК :-P
string name;
mapping (uint => Review) reviews;
}
struct Review{
Rating rating;
string additional_info;
Reviewer owner;
}
struct Reviewer{
address adr;
string email;
}
bool registred;
uint reviewers_number;
uint items_number;
uint reviews_number;
mapping (uint => Item) items;
mapping (uint => Reviewer) reviewers;
mapping (uint => Review) reviews;
function find_or_create_item(string n) reviewer_registred returns (uint item_id) {
if (items_number == 0){
throw;
}
else{
for (uint i = 0; i < items_number; i++){
string memory str = items[i].name;
// ключевое слово memory - ссылается на способы хранение данных в контракте
// чтобы не засорять хранилище использую memory
if (StringUtils.equal(str, n)){
// использование метода библиотеки
return i;
//item найден
}else{
item_id = items_number++;
items[item_id] = Item(n);
return item_id;
// item создан
}
}
}
}
function leave_review(uint item_id, Rating rt, string info) reviewer_registred{
uint reviewer_id = find_reviewer_id_by_address(msg.sender);
reviews_number++;
Item it = items[item_id];
it.reviews[reviews_number] = Review(rt, info, reviewers[reviewer_id]);
}
function register_reviewer(string e){
reviewers_number++;
reviewers[reviewers_number] = Reviewer(msg.sender, e);
registred = true;
}
function find_reviewer_id_by_address(address looking_for_adr) returns (uint reviewer_id){
uint result = 0;
for (uint i = 0; i < reviewers_number; i++){
address found_adr = reviewers[i].adr;
if( found_adr == looking_for_adr ){
result = i;
break;
// нашел и сразу брейк, чтобы не тратить вычислительные силы
}
}
return result;
}
}

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

Сколько будет стоить деплой и использование такого контракта в главной сети Эфира?
Полагаясь на Remix, который в графе Gas Estimates пишет: Creation: 520 + 489600 стоимость создания контракта в главной сети в рублях будет около 5.3×10^(-8).
По-моему такие цены вполне оправдывают использование Ethereum’a как распределенной базы данных для проектов (скорость доступа, да, уже другой вопрос).

C вашего позволения я не буду тестить его в коммандной строке в этот раз: неудобная она у Solidity.

Заключение

Буду писать про криптовалюты, эфир и блокчейн по мере свободного времени.

Обо все багах, ошибках, опечатках пишите мне на почту или в комменты.

Полезные сурсы, ссылки по теме