Сетевой протокол Ethereum с нуля. Часть первая. Соединение
В этой статье мы разберемся в основных концепциях работы сети Ethereum и напишем Python-скрипт для ее пингования.
Ethereum — это криптовалюта, где код может исполняться посредством блокчейна. Эта возможность позволяет создавать «умные контракты», которые будут выполняться автоматически. Давайте разберемся, как работают «умные контракты» и протокол Ethereum в целом.
Предполагается, что у читателя есть базовое понимание Python, Git и сетевых концепций, таких как TCP и UDP.
Концепция криптовалюты
Под криптовалютой подразумевается механизм, который использует децентрализованное хранение и передачу информации. В централизованных системах всегда есть доверенная третья сторона, которая отслеживает все учетные данные и занимается обработкой транзакций. Без посредника сторонам может быть трудно доказать, что то, о чем они говорят, действительно принадлежит им. Такую систему использует большинство банков мира.
Криптовалюты решают проблему децентрализованного регулирования, так как каждый элемент сети ведет учет всех проходящих в ней транзакций. Для поддержания консенсуса после проведения транзакции данные о ней передаются в сеть вместе с математической задачей, которую решают узлы сети и распространяют далее. Это обновление, протекающее по сети, и является доказательством того, что транзакция была проведена успешно и имеет место быть.
Настройка среды разработки
Все нижеприведенные действия производились на Amazon Linux и должны быть осуществимы на OS X и большинстве дистрибудивов Linux.
Intel , Нижний Новгород, можно удалённо , По итогам собеседования
Давайте создадим виртуальную среду для этого проекта:
Виртуальная среда не позволит произойти конфликту модулей Python.
Для активации виртуального окружения запустите команду:
Это приведет к изменению некоторых переменных среды. Теперь Python будет использовать пакеты только из виртуальной среды, pip будет ставить пакеты только туда.
Вы можете убедиться, что все сработало, проверив путь к среде:
Для того, чтобы не запускать виртуальную среду каждый раз при входе в систему, можете добавить следующие строки в
Это довольно удобно использовать при работе над проектом.
Примечание Используемая версия Python 2.7.12 не гарантирует, что все будет работать с другим версиями.
Версию можно проверить командой:
Последнее, что нужно сделать — создать пакетный скелет с библиотекой pipiecutter:
Будем использовать minimal skeleton , который позволяет производить публикацию в pip и выполнять тестирование:
Вам потребуется ответить на некоторые вопросы для настройки проекта. Назовем проект pyethtutorial . После этого вы можете настроить Git для его отслеживания.
Также давайте установим пакет nose . Он понадобится для тестирования:
Теперь давайте проверим, что все работает. В pyethtutorial/tests есть один тест, который поможет нам в этом убедиться:
Для запуска всех тестов используйте команду nosetests в каталоге проекта:
Все работает. Идем дальше.
Реализация
Нам нужно выяснить, как общаться с другими узлами сети.
Вот отрывок из документации по протоколу Ethereum:
Одноранговая связь между узлами, на которых запущенны клиенты Ethereum, выполняется с использованием протокола devp2p.
Узлы devp2p обмениваются сообщениями с использованием RLPx — транспортного протокола, использующего шифрование. Одноранговые узлы могут предлагать и принимать соединения на любых TCP-портах, однако по умолчанию порт, на котором может быть установлено соединение, будет 30303.
Узлы devp2p находят соседние узлы с помощью протокола обнаружения DHT.
Таким образом по умолчанию мы отправляем пакеты через порт 30303, используя протокол RLPx. Протокол devp2p имеет два разных режима: основной, который использует TCP, и режим обнаружения, который использует UDP. UDP работает таким образом: вы подключаетесь к определенным серверам, называемыми «узлами начальной загрузки» (для BitTorrent это router.bittorrent.com и router.utorrent.com), которые предоставляют вам небольшой список одноранговых узлов для подключения. После получения списка узлов вы можете подключиться к ним. Сервера в свою очередь будут делиться своими списками узлов с вами. Это будет продолжаться до тех пор, пока у вас не будет полного списка узлов в сети.
Звучит достаточно просто, но давайте сделаем это еще проще. В спецификации RLPx есть раздел «Обнаружение узла». В нем описано, как сообщения отправляются через UPD порт 30303, задавая следующую структуру пакетов:
И различные типы пакетов:
Типы сообщений представлены C-подобными структурами данных. Самое простое, что мы можем сделать сейчас — это реализовать пакет PingNode , который состоит из объекта version , двух объектов EndPoint и timestamp . Объекты EndPoint состоят из IP-адреса и двух целых чисел, представляющих порты UDP и TCP соответственно.
Чтобы отправить пакеты на аппаратный интерфейс, они кодируются по стандарту RLP. В документации говорится:
Функция кодирования RLP принимает элемент. Элемент определяется следующим образом:
Строка (то есть массив байтов) является элементом.
Список элементов — это элемент.
Кодирование RLP определяется следующим образом:
Для одного байта, значение которого находится в диапазоне [0x00, 0x7f], этот байт является его собственной RLP-кодировкой.
В противном случае, если длина строки составляет 0-55 байт, кодировка RLP состоит из одного байта со значением 0x80 плюс длина строки, за которой следует строка. Таким образом, диапазон первого байта [0x80, 0xb7].
Если длина строки больше 55 байтов, то RLP-кодировка состоит из одного байта со значением 0xb7 плюс длина в строки в двоичной форме, за которой следует длина строки, за которой следует строка. Например, строка длиной 1024 будет кодироваться как \ xb9 \ x04 \ x00, за которой следует строка. Таким образом, диапазон первого байта равен [0xb8, 0xbf].
Если общая полезная нагрузка списка (т. е. длина всех элементов) составляет 0-55 байт, то RLP-кодирование состоит из одного байта со значением 0xc0 плюс длина списка, за которым следует последовательность RLP-кодировок каждого элемента. Таким образом, диапазон первого байта [0xc0, 0xf7].
Если общая полезная нагрузка списка составляет более 55 байтов, то RLP-кодирование состоит из одного байта со значением 0xf7 плюс длина полезной нагрузки в двоичной форме, за которой следует длина полезной нагрузки, за которой следует последовательность RLP-кодировок объектов. Таким образом, диапазон первого байта [0xf8, 0xff].
Прежде чем что-либо можно будет закодировать в RLP, нужно преобразовать структуру в «элемент»: либо строку, либо список элементов (определение является рекурсивным). Как сказано в документации, RLP просто кодирует «структуру» и оставляет интерпретацию байтов содержимого протоколу более высокого порядка.
Начнем реализацию самого протокола. Будем использовать библиотеку rlp для RLP-кодирования и декодирования. Для установки используем pip install rlp .
У нас есть все необходимое для отправки пакета PingNode . Далее мы создадим PingNode , упакуем его и отправим. Чтобы упаковать данные, мы начнем с RLP-кодирования структуры, потом добавим байт, чтобы обозначить тип структуры, добавим криптографическую подпись и, наконец, добавим хеш для проверки целостности пакета. Приступим.
Первый класс — это класс EndPoint . Ожидается, что порты будут целыми числами и адрес будет находиться в формате 127.0.0.1. Адрес передается в библиотеку ipaddress , поэтому мы можем использовать его служебные функции, например, преобразование представления с точками в двоичный формат, что и происходит в методе pack . Для установки этого пакета используйте pip install ipaddress . Метод pack подготавливает объект, который будет использоваться rlp.encode , преобразуя его в список строк. Для портов на странице спецификации RLP сказано: «Целые числа Ethereum должны быть представлены в бинарной форме», а спецификация Endpoint перечисляет их типы данных как uint16_t или беззнаковые 16-битные целые числа. Таким образом, используется метод struck.pack с строкой формата >H , что означает «big-endian unsigned 16-bit integer».
Следующий класс — это PingNode . Вместо того, чтобы задавать значения позже, введем исходные байтовые значения для packet_type и version . Для метода pack мы можем использовать исходное значение версии, так как оно уже находится в байтах. Для адресов будем использовать struct.pack со строкой формата >I . Также добавим 60 к отметке времени, чтобы дать дополнительные 60 секунд и пакет успел прибыть в пункт назначения. (В документации сказано, что пакеты с устаревшей временной меткой не обрабатываются.)
Последний класс — PingServer . Этот класс открывает сокеты, подписывает, хеширует сообщения и отправляет их на другие серверы. Конструктор принимает объект EndPoint . Далее при создании сервера загружается секретный ключ, который мы должны инициализировать.
Ethreum использует систему асимметричного шифрования, основанную на эллиптических кривых secp256k1 . Для реализации понадобится библиотека secp256k1-py . Установим pip install secp256k1 .
Чтобы сгенерировать ключ, вызовем конструктор Private Key с None в качестве аргумента, а затем запишем вывод функции serialized() в файл:
Помещаем файл в каталог с проектом. Не забудьте добавить его в .gitignore , если вы используете Git, чтобы случайно его не опубликовать.
Метод wrap_packet кодирует пакет: hash || signature || packet-type || packet-data
Первое, что нужно сделать, — добавить тип пакета в RLP-код пакетных данных. Затем хэшированная полезная нагрузка подписывается с помощью функции ecdsa_sign_recoverable и ключа. Параметр raw установлен в значение True , потому что мы сами сделали хеширование (иначе функция использовала бы собственную хеш-функцию). Затем мы обрабатываем подпись и добавляем ее перед полезной нагрузкой. Наконец, вся полезная нагрузка хэшируется, и этот хэш добавляется в пакет. Теперь пакет готов к отправке.
Возможно, вы заметили, что мы еще не определили функцию keccak256 . Ethereum использует нестандартный алгоритм sha3 , называемый keccak-256 . Библиотека pysha3 реализует ее. Используйте pip install pysha3 для установки.
В pyethtutorial/crypto.py мы определяем keccak256 :
Вернемся к PingServer . Следующая функция udp_listen обрабатывает входящие передачи. Она создает объект сокета, который привязывается к UDP-порту сервера. Затем определяется функция receive_ping , которая принимает входящие данные, выводит их и возвращает объект Thread , который будет запускать get_ping , поэтому мы можем отправлять пинги одновременно с приемом входящих данных.
Последний метод ping создает объект PingNode , формирует сообщение с помощью wrap_packet и отправляет его с использованием UDP.
Теперь мы можем настроить скрипт, который будет отправлять некоторые пакеты.
Запустим код и получим следующий вывод:
Мы успешно запинговали самих себя.
Пытаемся пинговать соседний узел
Хорошими кандидатами для приема наших сообщений будут те самые узлы начальной загрузки. В документации сказано:
Чтобы начать работу, geth использует узлы начальной загрузки, адреса которых записаны в исходном коде.
Geth — это Ethereum-клиент, реализованный на Go. В этом репозитории файл /bootnodes.go содержит списки узлов начальной загрузки в специальном формате:
Ниже перечислены основные узлы сети:
Для примера используется узел US-WEST, но вы можете использовать любой из этого списка. Например, ближайший к вам.
Сейчас send_ping.py выглядит так:
Давайте проверим, что выйдет:
Ждет ответа, но его нет. Разберемся, что пошло не так.
Решение
Оказывается, что Ethereum использует адрес возврата из заголовка UPD, а не тот, который мы ему передаем в PingNode .
53042 — это порт из заголовка UDP. Сокет отправляет пакет с этим заголовком, потому что он не привязан к какому-либо порту заранее. Ниже в комментариях отмечены проблемы с PingServer :
Проблема в том, что udp_listen и ping используют разные сокеты (созданные на строках 3 и 15), а тот, который используется ping , не привязан к порту 30303, поэтому он использует произвольный порт.
Чтобы исправить это, нужно переопределить порт в методе __init__ на сервере. Окончательный результат выглядит примерно так:
Сокет инициализирован в __init__ и указан в udp_listen и ping .
Теперь попробуем запустить send_ping.py :
Мы получили сообщение от узла начальной загрузки! Отлично.
Источник
Ethereum + Python = Brownie
Салют, дорогой криптоэнтузиаст!
Сегодня речь пойдёт о Brownie — аналоге фреймворка Truffle, который часто используется для разработки умных контрактов на Solidity, их тестирования и развёртывания (о чём можно почитать в цикле соответствующих статей здесь).
Так зачем же нужен ещё один фреймворк и в чём его ключевое отличие от Truffle?
- Во-первых, в них используются разные языки — в то время, как Truffle опирается на JS, не все знают этот язык и не всем его комфортно использовать; в brownie же в используется Python 3.
- Во-вторых, brownie за счёт интеграций различного софта делает разработку удобнее: тут и менеджер пакетов ethpm для умных контрактов, и ganache для развёртывания локальной тестовой цепочки, и тесты через pytest, и все версии компиляторов solc, и даже биндинги к MythX — инструменту для автоматического поиска ошибок в умных контрактах,- иначе говоря brownie предлагает инструменты для всего цикла разработки. Конечно Truffle тоже позволяет использовать все эти инструменты, однако они не встроены во фреймворк и их приходится устанавливать дополнительно.
- В-третьих, brownie позволяет работать не только с умными контрактами на Solidity, но и на vyper — типизированном python-based для разработки умных контрактов.
Таким образом, если вы предпочитаете работать с пайтоном и хотите упростить себе разработку умных контрактов, то однозначно стоит попробовать brownie.
Что же ещё умеет brownie?
Как сказано в самом репозитории brownie — это фреймворк разработки полного цикла умных контрактов для Ethereum-based платформ, поддерживающий:
- Несколько языков программирования умных контрактов: Solidity и Vyper.
- Сборку контрактов.
- Интерактивное взаимодействие с контрактами.
- Тестирование умных контрактов с помощью pytest.
- Скрипты для взаимодействия с умными контрактами.
- Работу с шаблонами умных контрактов.
Рассмотрим все эти возможности и организацию проекта на brownie подробнее, для чего установим brownie себе на машину. Сделать это проще всего можно с помощью pip:
pip install eth-brownie
Теперь brownie доступен как консольная утилита.
Проект brownie
Проект brownie представляет из себя определённую структуру директорий и конфигурационный файл brownie-config.yaml . Создать проект можно либо с помощью команды brownie init
либо можно создать проект на основе шаблона с помощью команды brownie bake template_name
Далее я рассмотрю второй вариант на основе шаблона для ERC-20 токена (шаблон token).
Рассмотрим структуру проекта:
Стоит отметить, что помимо перечисленных директорий brownie имеет конфигурационный файл, который находится в корне проекта и называется brownie-config.yaml — в нём можно указать опции компилятора, данные для подключения к ноде или параметры тестирования.
Команды brownie
brownie даже в базовой комплектации имеет множество команд, но я рассмотрю четыре из них, которые значительно чаще прочих используются в производственном цикле: compile , console , test и run .
brownie compile
Данная команда используется для компиляции умных контрактов, которые расположены в директории проекта contracts или её поддиректориях. Если необходимо, чтобы часть контрактов не компилировалась как самостоятельные единицы, то к названию файла или директории стоит приписать слева символ нижнего подчёркивания «_»,- в таком случае компилятор brownie будет их игнорировать (это полезно при подключении библиотек к проекту).
Собранные контракты помещаются в ./build/contracts/ в виде одноимённых json-файлов, которые содержат ABI контрактов, их байт-код и дополнительную мета-информацию.
При компиляции brownie запоминает, какие контракты были скомпилированы, а какие ещё нет и компилирует только их. Но если нужно перекомпилировать все контракты, то можно сделать это добавив флаг -all .
Параметры компиляции, такие как версия компилятора или оптимизация кода, задаются в файле brownie-config.yaml
brownie test
Этой командой запускаются тесты в проекте с использованием pytest, однако стоит заметить, что команда не возвращает никакое значение, поэтому есть трудности с интеграцией тестов в CI/CD.
Тесты в проекте хранятся в директории tests/
brownie run
С помощью данной команды осуществляется запуск скриптов из директории scripts. Однако передача параметров в них поддерживается только с версии brownie 2.x, но даже без них удобно использовать данный функционал для интеграции с CI/CD (например для деплоя контрактов).
brownie console
Запускает интерактивный режим brownie: по сути он является интерпретатором установленной версии питона, но с заранее импортированными пространствами имён для проекта. Например для проекта токена мы имеем следующие переменные сразу после запуска:
Информацию о их назначении можно найти в документации brownie, однако подавляющая часть имён имеет то же предназначение, что и в web3py.
Работа с майнетом/тестнетами
Во всех примерах выше brownie поднимал ganache (локальное тестовое окружение Ethereum) и работал с ним, однако есть возможность работы с произвольной выбранной сетью (в том числе с приватным тестнетом и даже с Quorum!). Для этого используется параметр —network network_name при выполнении команд console и run, где network_name должна быть описана в brownie-config.yaml. По умолчанию там уже заданы майнеты ETH и ETC, а также локальный и публичные тестнеты. Однако можно добавлять свои сети в том числе добавляя в их свойства свои собственные параметры, если yaml позволяет это сделать. Обычно я при работе создаю дополнительно сети develop, test и master (по названиям веток в гите), а их блокчейны разворачиваю в Azure с помощью специальной службы.
Подводя итог можно сказать, что brownie на текущий момент уже достаточно зрелое Enterpise-ready решение для разработки под Ethereum и способен удовлетворить практически все возникающие в её процессе потребности. Питонистам и не только однозначно стоит попробовать сделать на нём свой следующий проект.
Источник