Чем хорош блокчейн?
Судя из названия блокчейн — это цепочка блоков. Так и есть. Но что дает эта цепочка? По сути это технология децентрализованного хранения данных с особой структурой, позволяющей быть уверенным, что манипуляции с данными происходили в рамках четко заданных правил. Обеспечивается эта уверенность тем, что массив данных хранится сразу у всех, кто подключился к сети блокчейна — это значит, что недостаточно будет просто подменить весь массив в одном месте. А еще каждая следующая порция данных, так называемый блок, содержит в себе хэш предыдущего блока, это дает два плюса:
- в готовую цепочку невозможно подставить промежуточный блок,
- сам блок нельзя изменить, не поменяв при этом его хэш, следовательно это невозможно сделать без нарушения целостности цепочки.
Дерево Меркла — дерево хешей, в данном случае используется для независимого подтверждения валидности отдельных транзакций. Транзакции — это и есть данные в блокчейне
Работу по добавлению блоков обеспечивают сами участники сети. Кому будет предоставлено право добавления следующего блока определяется специальным механизмом. Самые распространенные из таких механизмов — это Proof-of-Work и Proof-of-Stake. В первом блоки добавляют майнеры — участники сети, решающие вычислительно сложные задачи, конкурируя друг с другом за право создания блока на основе своего решения, а в награду за успешное создание блока получающие некоторое количество валюты этой сети. В Proof-of-Stake блоки добавляют валидаторы — участники сети, конкурирующие не за счет производительности, а на основе количества внутренней валюты этой сети на их аккаунте. Получают они при этом меньше, но и работы от них требуется меньше. В обоих случаях логика в том, что злоумышленнику для добавления поддельного блока придется потратить больше, чем удастся заработать. В первом случае — на оборудование для майнинга, соизмеримое по мощности с остальными майнерами вместе взятыми. Во втором случае — на покупку 50% валюты сети.
Ethereum
Существуют разные реализации блокчейнов, среди которых самыми популярными сейчас являются Bitcoin и Ethereum. В то время как Bitcoin — это реализация криптовалюты на базе blockchain, целью Ethereum является создание платформы, позволяющей решать самые разные задачи с помощью умных контрактов. Поэтому логично первое знакомство начать именно с Ethereum
Smart Contracts
Манипулирование данными в блокчейне обеспечивается так называемыми умными контрактами (smart contracts). Они описывают какие данные хранить на блокчейне и набор функций для операций над ними. Выполнение функций и получение доступа к данным осуществляется через предоставляемый каждым контрактом интерфейс. Этот интерфейс генерируется из исходного кода отдельно от компиляции и позволяет выполнять бинарный код. Данные для участников сети открыты, и чтение их ничего не стоит, ведь как уже было сказано, данные хранятся у всех участников сети. Изменение данных происходит посредством транзакций. Каждую транзакцию можно представить структурой следующего вида:
- Получатель транзакции
- Цифровая подпись отправителя
- Количество отправляемой валюты
- Произвольные данные (необязательно)
- Лимит газа на транзакцию
- Цена за единицу газа
Что такое газ из пунктов 5 и 6 будет рассказано в следующих пунктах и еще более подробно рассказано в отдельной статье.
Выполнение транзакций требует затрат внутренней валюты и ожидания когда очередной созданный майнером блок с вашей транзакцией включится в общую цепочку. Код контракта выполняется на компьютере майнера, в виртуальной машине EVM, а в награду майнер получает комиссию.
DApp
DApp — Decentralized Application или децентрализованное приложение. В идеале пишется как DApp, но мы будем использовать упрощенное написание. Приложение может быть построено на разных технологиях, но среди них есть и блокчейн со смарт контрактами. Можно сказать, что на данный момент DApp — это логика на смарт контрактах плюс некий пользовательский интерфейс. Хранение более-менее объемных данных и обмен сообщениями в идеальном DApp тоже должны быть децентрализованными, однако эти технологии только начинают появляться и заслуживают отдельной статьи. Блокчейн же обеспечивает хранение текущего состояния и реализует бизнес-логику через смарт-контракты.
Идеал, к которому стремится развитие децентрализованных приложений. Картинка позаимствована отсюда
Используя DApp, пользователь может получить доступ к блокчейну напрямую на своем компьютере, установив специальное ПО. Блокчейн также может использоваться для каких-то отдельных операций на стороне сервера привычных нам мобильных и веб приложений. Выбор зависит от конкретной задачи. Упрощенный вариант DApp можно представить в таком виде:
Картинка взята и переведена из презентации Игоря Баринова
Фронтенд и бэкенд в данном случае это классические элементы приложения, а функциональность с задействованием блокчейна выполняется на виртуальной машине EVM. Пользователю доступны стандартные функции виртуальной машины — такие как отправка транзакции или просмотр баланса аккаунта, — а также функции, описанные в смарт контрактах, например на языке solidity. Доступ к этой виртуальной машине предоставляется через RPC интерфейс.
Создание распределенных приложений должно, по нашему мнению, стать довольно востребованным направлением, так как они позволяют решать многие проблемы: отсутствие доверия к хранителю данных, уязвимые для атак серверы в централизованных системах, закрытость систем.
Первое подключение к блокчейну
Чтобы хоть как-то увидеть что значит быть участником сети мы скачали Mist (на момент написания последняя версия под номером 0.9.0) — кошелек Ethereum. Кошельком Mist называется потому, что в нем можно управлять своими аккаунтами и балансом на них. Основная валюта — ether (эфир), но можно выпускать собственные токены, они также будут отображаться в кошельке. Но Mist — это не только кошелек, а еще и браузер DApp для Ethereum-блокчейна. Он позволяет выкладывать и использовать смарт контракты, а также пользоваться DApp-приложениями.
Для наглядности работы с блокчейном рекомендуем использовать пару клиентов на разных компьютерах: можно будет увидеть, что создаваемые данные доступны не только локально — но это не обязательно.
UPD для Windows: Для пользователей Windows следующие пункты несколько осложнены. Требуется установить еще и Geth — клиент командной строки. Перед запуском Mist надо будет выполнить в командной строке
geth --rinkeby
(вместо флага --rinkeby можно использовать --testnet если нужна сеть Ropsten, либо вообще опустить флаг, если нужна главная сеть). Из пользовательского интерфейса уже не получится изменить сеть или начать майнить. Если вы собираетесь подключаться и пробовать майнить на Ropsten — запустите geth такой командой
geth --testnet console 2>nul
В geth-консоли можно будет выполнить
miner.setEtherbase("<адрес вашего кошелька>")
после чего запустить
miner.start(4)
(4 — количество потоков, можно выставить сколько хотите). Имейте в виду, что лучше дождаться окончания синхронизации перед тем, как начинать майнить
1. Во время запуска Mist предлагает выбрать сеть — Main network или Test network. Выбираем Test network.
Для выполнения любых операций на блокчейне требуется валюта этой сети, в данном случае ether. В Main network эфир стоит реальных денег, а в Test network — ничего не стоит и его легче получить. Кроме того, перед запуском к вам на компьютер скачиваются все данные сети, для testnet Ropsten на момент написания статьи это меньше 7 GB, для testnet Rinkeby — 800 MB, для реальной сети — больше 40 GB. Поэтому для начала выбираем Testnet. В реальной сети эфир можно получить купив его на бирже за реальные деньги (на момент написания статьи это около $300), либо намайнить, но для этого требуются довольно большие мощности и затраты времени. В тестовых сетях источники варьируются: это либо майнинг для Ropsten (получение из других источников Ropsten у нас не заработало), либо получение через такие источники как www.rinkeby.io ->Crypto_Faucet для Rinkeby. Майнинг в Testnet занимает значительно меньше времени, чем в реальной сети, например на ноутбучном процессоре i5 6200u мы получали 5 эфиров в зависимости от везения за пару-тройку часов. Скорость майнинга в этом случае была около 50 KH/s (50 KH — 50 килохэшей, или 50 000 хешей в секунду), вы сможете ее увидеть у себя и прикинуть сколько времени потребуется лично вам. Кстати намайнив несколько эфиров на одном клиенте можно будет без проблем передать часть на другой, например если тот майнит медленнее. Стоит упомянуть, что в дальнейшем мы будем использовать только Ropsten, которая является Proof-of-Work сетью, поэтому в ней и используется майнинг. В версиях Mist после 0.9 эта сеть больше не является сетью по умолчанию, поэтому если хотите использовать ее — сначала запустите Mist, нажав Launch Application, затем в пункте меню Develop->Network выберите нужную сеть. В целом надо отметить, что Rinkeby более удобен, так как не требует майнинга, быстрее и легче, поэтому вы не много потеряете используя его. Однако Ropsten более приближен к реальной сети и позволяет почувствовать ее особенности.
2. Итак, запущен Mist, предлагает задать пароль для своего аккаунта. Логин не нужен, так как для идентификации используется файл приватного ключа.
Приватный ключ хранится на линуксе в папке ~/.ethereum/testnet/keystore/ для Ropsten, ~/.ethereum/rinkeby/keystore/ — для Rinkeby. Обратите внимание, что для разных сетей создаются отдельные ключи и если вы собираетесь использовать Ropsten, как и мы, то потребуется создать еще один аккаунт. Имя состоит из даты и времени создания и адреса. Под адресом понимается шестнадцатеричная строка в 20 байтов вида 0xe03269461f7672494fb0dbbe89c00614601b5d24. В названии файла начальный 0x опущен. Адрес используется для идентификации вашего аккаунта в блокчейне, на него можно отправлять ether с других аккаунтов.
3. Как уже говорилось, требуется синхронизация локальной базы, на это для testnet Ropsten может уйти пару часов и больше, но необходимо дождаться завершения процесса. Иначе есть вероятность получить рассинхронизированную базу.
По крайней мере в нашем случае была ситуация, что при запущенном майнинге эфир начал набираться чересчур быстрыми темпами, но при этом его невозможно было использовать — все операции не были видны другим участникам сети. Проблема выяснилась следующим образом — в Mist в левом нижнем углу отображается номер последнего блока (либо сколько блоков остается до окончания синхронизации, в этом случае все нормально и нужно лишь дождаться окончания процесса). Номер последнего блока в локальной копии можно сравнить с реальным значением для данного блокчейна например на ropsten.etherscan.io можно узнать последние номера блоков для сети Ropsten. Если ваше значение намного отличается в меньшую сторону — возможно ваша база не синхронизирована. Итак, что делать если синхронизация в mist дошла до конца, но номер блока неправильный? Мы решали эту проблему удалением данных и скачиванием их заново. Данные на Линуксе для сети Ropsten лежат в папке ~/.ethereum/testnet, нам помогло удаление всего из подпапки chaindata. После чего запустили mist и уже на этот раз терпеливо дождались окончания синхронизации.
4. После окончания синхронизации можно выбрать пункт меню Develop->Start mining. Это необходимо для того, чтобы получить хоть немного эфира. Это актуально только для сети Ropsten. Если хотите использовать сеть Rinkeby — зайдите на www.rinkeby.io, вкладка Crypto Faucet, и следуйте приведенным инструкциям.
Эфир нужен для любых операций по изменению данных, им оплачивается так называемый gas — абстрактная единица измерения, которая служит для оценки требующейся работы по выполнению транзакции. Она нужна для независимости этой оценки от текущей рыночной стоимости эфира. При отправке транзакции можно задать сколько эфира вы платите за каждую единицу газа и максимальное количество газа, которое вы готовы оплатить. Чем больше вы выделяете — тем более приоритетна ваша транзакция для потенциальных майнеров. Ведь по сути плата за gas — это оплата работы майнеров по выполнению вашей транзакции и включению ее в очередной блок. Поэтому при майнинге кроме фиксированной платы за найденный блок — на момент написания это 5 эфиров, — майнер также получает плату за транзакции, как правило это несколько сотых эфира. Количество газа за транзакцию зависит от вычислительной сложности операций над данными. Пример того как расходуется и оценивается газ мы приведем в следующей статье.
Простейший Smart Contract
Как только у вас на аккаунте будет какое-то количество эфира — можно начинать эксперименты со смарт контрактами. Язык, на котором пишутся контракты — Solidity, — напоминает С++ и JavaScript. Есть и другие языки, но Solidity самый популярный, активно поддерживаемый и хорошо документированный, поэтому рекомендуем использовать именно его. Рассмотрим простой контракт, единственная цель которого — хранить и обеспечивать возможность менять единственную строку.
Код контракта:
pragma solidity ^0.4.10;
contract StringHolder {
string savedString;
function setString( string newString ) {
savedString = newString;
}
function getString() constant returns( string ) {
return savedString;
}
}
Строка
pragma solidity ^0.4.10
означает, что минимальный требуемый компилятор для данного контракта — 0.4.10, а символ ^ запрещает использование компилятора начиная с 0.5.0. Это актуально, так как Solidity развивающийся язык и несмотря на желание разработчиков сохранять совместимость — это не всегда возможно.Имя контракта задается после ключевого слова
contract
. В теле контракта описываются все хранящиеся данные, в данном случае это поле savedString
типа string
. Манипуляции с данными осуществляются через сеттеры и геттеры. В данном случае функция setString( string newString )
присваивает в переменную контракта новое значение для строки. Функция getString() constant returns( string )
возвращает значение строки (тип возвращаемого значения задается как returns(<тип>)
). Стоит особо отметить ключевое слово constant
— оно гарантирует, что никакие из данных не будут изменены при выполнении функции. Если данные не меняются — то не нужно платить за газ. Поэтому геттеры выполняются моментально и бесплатно. Сеттеры требуют оплаты и выполняются не моментально (только в результате включения транзакции в очередной блок блокчейна).Для начальных экспериментов с контрактами очень удобна Remix IDE. Достаточно скопировать приведенный код контракта и вставить его в окошко для кода. В правой панели нажать Create — создастся контракт без публикации в блокчейн. Увидите следующее.
Синим отмечаются геттеры (getString), красным — сеттеры (setString). Показано сколько расходуется газа.
Для задания строки в поле setString не забудьте поставить кавычки, иначе получите ошибку
Проверив, что get и set работают как надо можно деплоить контракт в настоящий блокчейн. Для этого переключаемся обратно в Mist, заходим в Contracts и нажимаем Deploy New Contract. Копируем код в поле Solidity Contract Source Code и справа видим выпадающий список Pick a contract. Выбираем StringHolder, единственный пункт в данном случае. Выбираем размер оплаты, от которого будет зависеть время выполнения деплоя, нажимаем Deploy, в окне отобразится примерная стоимость, вводим пароль от аккаунта и нажимаем Send Transaction. В кошельке появится новая транзакция с прогрессом “x of 12 Confirmations” (x из 12 подтверждений). Первое подтверждение будет означать, что транзакция включена майнером в блок, последующие — что создано соответствующее количество блоков после блока с нашей транзакцией. Это дает большую гарантию, что блок с нашей транзакцией не будет отменен. Но для того чтобы контракт стал активным достаточно одного подтверждения. После подтверждения заходим в Contracts > String Holder. В mist отображается интерфейс контракта: слева геттеры (Read from contract), справа сеттеры (Write to contract) в виде выпадающего списка. Работает так же, как в Remix IDE, только задание строки — это уже настоящая транзакция, которая так же, как создание контракта, будет требовать подтверждения паролем и будет ожидать 12 подтверждений от майнеров.
Как другим пользователям увидеть этот контракт? Контракт определяется двумя составляющими: адрес и интерфейс ABI. Все это можно узнать на странице контракта в Mist, по кнопкам “Copy address” и “Show Interface”. Адрес — это такое же 20-байтное шестнадцатеричное число, например в нашем случае это 0x65cA73D13a2cc1dB6B92fd04eb4EBE4cEB70c5eC. А интерфейс — JSON-текст, для нашего смарт контракта он выглядит следующим образом:
[ {
"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"
} ]
Интерфейс генерируется автоматически из кода контракта и не должен меняться после деплоя, адрес контракта возвращается после деплоя и указывает на бинарный код контракта. Mist сохраняет эти данные и предоставляет интерфейс для их получения только если деплой выполнялся через него. Кстати список выполненных/выполняемых транзакций Mist тоже хранит локально и только если они совершались через его интерфейс.
Клиент, желающий использовать контракт, должен получить эти данные, и в случае с Mist выбрать Contracts->Watch Contract. Название можно выбирать любое, оно нужно лишь для удобства. Нажали ОК — контракт появился в списке, можно заходить в него и изменять строку уже с другого клиента. При этом после выполнения транзакции (получения хотя бы одного подтверждения) строка изменится у всех клиентов.
Этот пример описывает настоящее распределенное приложение, где каждый клиент скачивает на компьютер весь блокчейн, что не очень удобно в реальности, хоть и обеспечивает отсутствие посредников (на самом деле Mist тоже посредник, хоть и надежный). В реальности приходится идти на компромисс: например узел блокчейна разворачивается у третьей стороны, пользователю предоставляется веб-интерфейс. Или в браузер устанавливается специальный плагин (такой как Metamask), который использует ключ пользователя для подписи транзакций. В любом случае возникает проблема доверия к посреднику. Надеемся в будущем эта проблема решится или за счет реализации протокола легкого клиента, которому не требуются данные целиком, или каким-то еще способом. А пока приходится работать с тем что есть.
В следующей статье мы подробнее рассмотрим как это работает.
Комментарии (23)
dron_k
23.08.2017 23:48Подскажите какие есть наиболее популярные приложения/контракты, хотелось бы понять как они используются на практике.
rubyruby Автор
24.08.2017 00:40+1Пока сложно говорить о каком-то широком применении на практике для рядового пользователя. Некоторые крупные компании внедряют его в качестве эксперимента, например Альфа-Банк и S7 (статья на хабре). Если из того, что можно самому пощупать — то сейчас мы разбираемся с 0xProject — протокол для обмена ethereum токенов. Вот здесь репозиторий с их смарт контрактами и описанием того, что они делают. Так же доступен код dapp на базе этого протокола на reactjs, соответственно можно увидеть целостную картину приложения.
equand
24.08.2017 10:32Никак, без широкого распространения эфириума по любым компьютерам это нереально. К сожалению, решение все внутри коробки мне кажется не взлетит. Хотя я болею за эфир, но ставить еще софтину и качать 200гб, чтобы запустить довольно медленный процесс я не собираюсь. Никакая киллер-фича меня не заставит это делать, всегда будут миддлмены для этого. Поэтому принцип контракта теряется. Тем более ошибки в них бывают, а решение их довольно сложное, т.к. доступ лимитирован. Есть иное решение, но я пока не сформировал его полностью, принцип его в отсутствии какого-либо софта, а криптовалюты используются как легко-проверяемый реестр транзакций.
sys_int64
24.08.2017 09:34+1чтобы писать контракты удобнее использовать truffle и testrpc.
rubyruby Автор
24.08.2017 10:47+1Конечно, но не хотелось перегружать вводную статью. Для сколько-нибудь объемного проекта что-то вроде truffle точно необходимо, а для простейшего примера для тех, кто только начал изучение — описанный путь на наш взгляд самый бесхитростный и понятный.
debounce
24.08.2017 13:17Скажите, если у меня нет 200гб(как тут говорят) и мне нужна только тестовая сеть, как быть, клиент все равно закачает обе?
rubyruby Автор
24.08.2017 13:22Если выбрать тестовую сеть Rinkeby, которая сейчас по-умолчанию в Mist, то данных скачается в районе всего 1.5 гигабайт. Сети друг от друга не зависят, можно скачивать только нужную
debounce
24.08.2017 14:21+1Это приложение — клиент к кошельку писали где-то в аду. Почему ничего не работает?
У меня бесконечно крутится поверх всех окон прогресс, который можно закрыть только Сняв задачу. Запускаю кошелек, выбираю тестовую сеть и получаю ошибку
Аккаунт создавал до этого уже сто раз и все равно он просит создать заново. Ввожу опять пароли — CREATING и так до бесконечности(я проверял)
Если сделать что-то магическое нажимая много раз skip-next-back, то появится кнопка Launch appliction! Можно попасть в кошель, где будет написано: осталось 4 млн блоков, прогресс — 0% — я так понимаю он закачивает основную сеть? В меню выбора сетей основная сеть — галочка и невозможность отменить.rubyruby Автор
24.08.2017 14:41Похоже для Windows приложение какое-то особенное. Попробуйте скачать и установить Geth — консольный клиент для Windows отсюда. Потом в командной строке выполнить geth --rinkeby и уже после этого запустить Mist.
debounce
24.08.2017 14:55Спасибо! В консоли идет закачка и это отображается в кошельке — прогресс идет. Сделал аккаунт в кошельке — у него есть адрес и он main account(etherbase) —
1) что это за акк — это для тестовой сети или подходит для всех сетей?
2) на этот адрес можно намайнить тестовых денег(получение через такие источники как www.rinkeby.io ->Crypto_Faucet для Rinkeby)?rubyruby Автор
24.08.2017 15:121) Аккаунт для каждой сети свой, и сети между собой никак не контактируют. Чисто теоретически может быть даже адрес в разных сетях одинаковый, хоть и вероятность практически нулевая, и эти аккаунты не будут иметь ничего общего.
2) Да, на этот адрес можно получать по несколько эфиров в день оттуда, создав от своего аккаунта в гите гист, добавив туда свой адрес, и вставив ссылку на этот гист в поле на сайте rinkeby.io. Только это не майнинг на самом деле, просто переводится валюта
Спасибо кстати за вопрос по мисту на винде, эту информацию надо бы добавить в статью
debounce
24.08.2017 15:50Спасибо за статью. Получилось опубликовать контракт, дождаться подтверждений, а потом на «странице» контракта вызвать setString() и опубликовать изменение.
Я публиковал два раза — два вызова setString(), а вижу(getString() ) только последнее значение. Где и у кого Контракт хранит переменные?
rubyruby Автор
24.08.2017 16:14Все правильно, контракт хранит текущее состояние, а не историю
debounce
24.08.2017 16:57Получается, можно определить массив, а при каждом исполнение контракта в него добавлять значения. Т.е эти данные тоже хранятся в блокчейне?
Как написать игру? Суть простая: на контракт посылаем любое кол-во денег, обратно получаем ноль или джекпот. Соответственно, контракт где-то аккумулирует средства и иногда бывает щедрым.
-Контракт может иметь своей «кошелек», чтобы распоряжаться счетом?
-Возможен ли в Solidity рандом?rubyruby Автор
24.08.2017 17:33Если нужно накапливать какую-то информацию, то да, можно определить массив и он будет храниться на блокчейне.
Сам контракт может хранить эфир, смотрите в сторону ключевого слова payable, его надо добавлять к методам, тогда во время вызова можно будет указать количество эфира, которое перейдет вместе с транзакцией.
Рандом в самом блокчейне невозможен, нужно использовать внешний генератор, смотрите Oraclize, он не только для рандома, а вообще для получения внешних данных.
debounce
24.08.2017 19:15Запилил контракт, который должен возвращать половину при вызове платной функции.
0x4523Ae66D2fb365c09c80890b3442ebB4757E0DB
pragma solidity ^0.4.10; contract Game { uint summ; function bid() payable { uint amount = msg.value; address bidder = msg.sender; summ += amount; bidder.transfer(amount/2); } function getSumm() constant returns( uint ) { return summ; } }
Все работает, кроме bidder.transfer() — функция деньги принимает, summ плюсует, а вот на вывод не работает.
Или что-то не так с msg.sender? Я понимаю это как адрес вызывающего платную функцию или это owner — владелец контракта?debounce
24.08.2017 19:30И еще что-то непонятное: вызываю платную функцию с суммой 2 ETH, на счет контракта попадает только 1 ETH. Половина ушла куда-то) Block explorer по адресу контракта показывает только IN операции, вывода не было. Может взаимозачет какой и майнеры такие операции объединяют?
rubyruby Автор
24.08.2017 20:52Вроде нормальный код, bidder не получает эфир назад? На ropsten.etherscan.io для контракта есть вкладка Internal Transactions, в которой перечислены отправки из него. Почему-то ее нет для Rinkeby. Тут нужно как-нибудь по-другому проверять
debounce
24.08.2017 21:07смотрю здесь www.rinkeby.io
Тут какой-то взаимозачет происходит. Отсылаю с кошелька 10 ETH, реально баланс уменьшается на 5, но в транзакциях видно OUT 10 ETH. И в контракте такая же картинка IN 10 ETH, но реально баланс контракта увеличился на 5 ETH. С виду все правильно сработало, просто в block explorer ожидал увидеть
кошелек OUT 10 ETH
контракт IN 10 ETH
контракт OUT 5 ETH
кошелек IN 5 ETH
но походу майнер оптимизировал транзакции с четырех шагов до двух
mistik_max
Отличная, полезная статья, респектую автору! Узнал для себя некоторые новые вещи о блокчейне и эфириуме…