Децентрализованные технологии развиваются всё быстрее, капитализации проектов растут, рынок наполняется новыми вакансиями. Нет сомнений, что эта сфера уже оказывает сильное влияние на мир. Об этом, конечно, можно долго и интересно рассуждать, но моя статья о другом. В фокусе статьи две вполне себе конкретные вещи: on-chain программы Solana и алгоритм цифровой подписи ed25519. К чьему-то сожалению здесь не будет ничего об уязвимостях, потому что мне не хватает компетенций в таких вопросах. Зато я расскажу о программной модели Solana, которая позволяет строить децентрализованные приложения, а также о том, какое место в ней занимает алгоритм цифровой подписи ed25519 и как он математически работает. Solana - это молодой быстрорастущий проект, который представляет из себя блокчейн и платформу для децентрализованного исполнения программ. Одной из главных целей Solana является достижение высокой скорости работы сети, сопоставимой скорости централизованных сетей. Родоначальником идей, лежащих в основе проекта Solana, а также текущим CEO Solana Foundation является Анатолий Яковенко, который ранее достаточно долго работал в Qualcomm. Служебный токен сети носит одноименное название (кратко SOL), а его минимальная единица - lamport (ру. лэмпорт), равный SOL. Solana использует Proof-of-Stake в качестве алгоритма консенсуса, а также алгоритм Proof-of-History для быстрого и безопасного определения порядка транзакций, который, к слову, для Solana и был придуман. Одной из функциональных единиц Solana являются аккаунты. Формально аккаунт - это пара из 32-байтной последовательности, называемой адресом, и соответствующей структуры данных, существующей по этому адресу. Упрощенно аккаунт можно представить следующим образом: Среди полей аккаунта: Баланс в лэмпортах lamports Вектор байтов data, в котором можно хранить любую полезную (или бесполезную) информацию Владелец owner - 32-байтный адрес on-chain программы, владеющей аккаунтом (об этом далее) Флаг executable, равный true, если по данному адресу существует on-chain программа Поле rent_epoch не очень интересно в рамках статьи. Замечу, что тип Pubkey я намеренно назвал 32-байтным адресом. Физический смысл этих 32 байтов будет раскрыт позже, а пока буду называть это просто адресом. On-chain программа Solana - программа, исполняемая распределенно. Это означает, что при вызове она исполняется несколькими серверами (валидаторами) сети, и истинный результат исполнения определяется путём принятия консенсуса. В других сетях (например Ethereum и Near) эти программы называются смарт-контрактами. On-chain программы Solana принято писать на языке Rust (хотя можно на C), они компилируются в динамические библиотеки для виртуальной машины BPF. Каждая on-chain программа имеет точку входа в виде некоторой функции: Среди аргументов этой функции: program_id - адрес этой программы accounts - массив аккаунтов, с которыми (и только с которыми) программа может работать в рамках данного вызова instruction_data - массив байтов, в котором программе может быть передана любая информация Вызов программы происходит следующим образом. Предположим, программа уже написана и загружена в сеть. Любой клиент, зная адрес программы, может собрать список аккаунтов, которые хочет передать ей, сформировать инструкцию и с помощью запроса к одному из RPC серверов сети Solana "вызвать" программу с данными аргументами. Теперь посмотрим на структуру AccountInfo, которая появилась в вышеприведённом коде:    Много непонятных символов для читателей, незнакомых с Rust. Это механизмы языка для подсчета ссылок, контроля "прогулки" ссылок по стеку вызовов и прочее. Не будем на этом останавливаться, а рассмотрим три поля, которых не было в структуре Account: key - адрес этого аккаунта is_signer - флаг, означающий факт подписания транзакции, вызывающей эту программу, приватным ключом данного аккаунта is_writable - флаг, разрешающий программе менять поля этого аккаунта is_signer и is_writable определяются на стороне клиента перед вызовом программы. Теперь приведу правила, на основе которых в Solana можно гибко строить различную бизнес-логику: Если аккаунт is_writable, и программа, которой он передан при вызове, является owner'ом этого аккаунта, она может уменьшать поле lamports и менять массив data (увеличивать поле lamports может любая программа) Поле is_signer программа может проверить, чтобы понять, вызвал ли её физический владелец данного аккаунта Кажется, становится сложнее. Теперь стоит вернуться к Pubkey, который я до текущего момента называл просто адресом, и прояснить ситуацию. Оказывается, что в Solana адреса программ и аккаунтов зачастую являются публичными ключами из алгоритма цифровой подписи ed25519. Таким образом владельца приватного ключа (будь то человек, хранящий приватный ключ у себя на компьютере, или робот, генерирующий ключи автоматически) можно в некотором смысле считать физическим владельцем аккаунта. В реальности любой клиент может вызвать любую программу, подложив ей в аргументы любые аккаунты. Но факт подписи позволяет строить безопасную логику. Теперь разберём алгоритм ed25519. Я приведу его шаги и покажу корректность проверки подписи в режиме "следите за руками", без доказательств некоторых утверждений. Понимание алгоритма позволит нам на кончиках пальцев почувствовать  ещё один механизм Solana, позволяющий строить ещё более гибкую логику. Алгоритм ed25519, кроме прочего, красив тем, что его реализацию для конкретной аппаратуры можно сделать очень быстрой. Вот ряд его свойств, приятных с точки зрения аппаратуры Разработчики алгоритма заточили программное обеспечение под x86_64 набор инструкций и получили высокую скорость проверки подписи на некоторых процессорах Intel. Проверка набора из 64 подписей позволила разработчикам экономить время за счёт сохранения промежуточных результатов в регистрах и их переиспользования на следующих итерациях. Также было выяснено, что код / данные укладываются в iL1 / dL1 кэш процессора, что также оказывает положительный эффект на скорость проверки набора подписей и предотвращает войну за кэши (cache contention) при многопоточной проверке. Ввиду того, что весь алгоритм тонко спроектирован под аппаратуру, реализованная в ПО подпись также оказалась очень быстрой в терминах числа тактов процессора. Вычисление публичного ключа из приватного также оказалось довольно быстрым. Тем не менее, значительную задержку даёт системный вызов Linux, возвращающий случайное число. С точки зрения обращений в память ПО спроектировано таким образом, чтобы паттерн обращений в память был полностью предсказуем, а секретные данные не могли утечь через кэш.  Паттерн условных переходов также полностью предсказуем, ПО защищено от утечек секретных данных, вызванных ошибками предсказателя переходов Подпись в сжатом виде имеет длину 64 байта Публичный (в сжатом виде) и приватный ключи имеют длину по 32 байта каждый Аппаратура - штука интересная, и говорить о ней можно долго. Я не буду на этом останавливаться, но оставлю полезные ссылки в конце. Теперь поговорим немного о математике. Алгоритм цифровой подписи ed25519 определяется выбором 7 параметров: Целое число  Криптографическая хэш-функция H, дающая на выходе - битное число Простое число , задающее поле Галуа  Число , не являющееся квадратом в этом поле (это также называют квадратичный невычет) Точка , где - это группа точек эллиптической кривой над  Простое такое, что (нейтральный элемент группы) Способ - битного кодирования элементов  Данная эллиптическая кривая называется скрученной кривой Эдвардса (Twisted Edwards Curve). Групповой закон на ней выглядит следующим образом: Далее определим некоторые числа из , как “отрицательные”. Для выбранного способа кодирования будем считать  отрицательным, если кодирование  лексикографически больше кодирования . Например для  в полепри little-endian кодировании отрицательными будут считаться . Таким образом для кодирования точки на кривой достаточно  бит:  бит для кодирования  и один бит для знака , а сам  можно вычислить (в поле , разумеется): Сгенерируем случайным образом -битное число , которое будет являться приватным ключом. Возьмём от него хэш , где  - биты вычисленного хэша. Вычислим число  следующим образом: Публичным ключом будем считать кодирование точки (далее кодирования точек буду обозначать подчёркиванием). Ввиду групповой структуры эллиптической кривой точка  также лежит на этой кривой. Для подписи сообщения : Вычислим число  Найдём точку  Найдём число  Сигнатурой будем считать - битную строку  Для верификации подписи проверяющая сторона должна: Восстановить точку  Восстановить точку  Восстановить целое число  Проверить равенство  Выясним корректность такой проверки. Домножим  на : Пусть , тогда для некоторого целого  справедливо: В терминах  исходное выражение примет следующий вид: Но и  выбраны так, что - нейтральный элемент, а значит Таким образом видно, что равенство выполнено для любого сообщения, подписанного истинным приватным ключом. Параметры, которые разработчики алгоритма рекомендуют к использованию, выглядят следующим образом:   SHA-512 Кодирование чисел из  - простой little-endian   ,  положительно в терминах битового кодирования Таким вот (не)хитрым образом работает алгоритм ed25519.  Теперь важно отметить, что из публичного ключа всегда восстанавливается точка на кривой. Но в Solana переменные типа Pubkey не всегда содержат кодирование точки на кривой. Здесь мы подошли к ещё одному механизму безопасности Solana, который называется Program Derived Addresses (PDA). Факт того, что какая-то точка не лежит на кривой, с точки зрения ed25519 означает, что не существует приватного ключа такого, что подпись сообщения этим ключом проходила бы проверку относительно публичного ключа . В Solana это научились использовать для того, чтобы наделить on-chain программы возможностью якобы ставить подпись на транзакцию. Сперва важно отметить, что on-chain программа может быть вызвана не только каким-то off-chain клиентом. Она также может быть вызвана другой on-chain программой . При этом может передать в только те аккаунты, которе есть среди переданных в . Представим, что в был передан аккаунт , поле is_signer которого равно false. Если этот аккаунт обладает особым адресом, то может передать его в c is_signer: true. Особый адрес вычисляется следующим образом: Выберем какой-то сид seed (массив байтов), который будет всем известен Найдём такое 8-битное число bump_seed, что SHA256(p1_id, seed, bump_seed) возвращает 32-байтное число, которое не может быть декодировано в точкуна кривой Под p1_id здесь я имею в виду адрес программы . Для полученного адреса не существует приватного ключа, а также он зависит от адреса программы. Рантайм может проверить, что адрес аккаунта с полем is_signer: false действительно вычисляется из адреса вызывающей программы и указанных сидов, и передать вызываемой программе этот аккаунт с полем is_signer: true. Таким образом в Solana программа может быть в некотором смысле физическим владельцем аккаунта. Но осталась пара вопросов: Почему существуют такие 32-байтные последовательности, из которых нельзя восстановить ? Как найти bump_seed? 32-байтная последовательность не всегда может быть декодирована в точку ввиду того, что выражение для , которое я приводил выше, вычислимо не для каждого , так как не для каждого числа в поле порядка  существует квадратный корень. Элементы поля, для которых существует квадратный корень, называются квадратичными вычетами. Для проверки того, что нельзя восстановить из 32-байтной последовательности, можно, например, воспользоваться критерием Эйлера: Вот код функции, которая используется в Solana для восстановления точки: Она вернёт None, если не удастся извлечь корень. Ниже код для извлечения корня. Функция возвращает байт, по которому можно определить, удалось ли найти ненулевой корень, а также сам результат. Тут используются некоторые трюки для ускорения вычислений, не будем на них останавливаться. Главное, что мы поняли основные идеи того, как именно механизм работает в Solana. Осталось только понять, как найти bump_seed. Какого-то умного алгоритма нет, но он и не нужен. Можно просто в цикле от 255 до 0 перебирать сиды, генерируя новый адрес и проверяя, лежит ли он на кривой. В большинстве случаев поиск останавливается на 255 или 254. Теперь у нас есть целостное понимание того, как работает PDA и зачем это нужно. В целом я затронул большую часть от всей программной модели Solana. Перейду к заключению В заключение хочется отметить, что Solana - технически очень интересный и сложный проект. Среди самых известных децентрализованных приложений, реализованных на Solana, можно выделить Metaplex - платформу для минта и продажи NFT, а также Serum - первую децентрализованную биржу, торговля на которой производится с помощью ордебука (сейчас они позиционируют себя, как Serum Core - инструментарий для разработки DeFi приложений на Solana). Оба проекта имеют открытый исходный код, который богат примерами для начинающих разработчиков. На этом у меня всё. Мне удалось затронуть достаточно разные темы, каждая из которых по-своему интересна. Если говорить о Solana, то следующие источники смогут помочь разобраться с ней: Официальная документация Solana Репозиторий example-helloworld - то, с чего стоит начать Репозиторий Solana Program Library - коллекция on-chain программ, написанных разработчиками Solana Репозитории Metaplex и Agonostic orderbook (on-chain ордербук от Serum) Мои первые попытки сделать что-то на Solana. Хотя я был молод и глуп, для начала тоже может быть полезно По части криптографии, которую я затронул, могу порекомендовать следующие материалы: Нигель Смарт "Криптография" - книга, в которой есть все основы Доступно о криптографии на эллиптических кривых - длинная, но легкочитаемая статья, поможет начинающим быстро понять суть происходящего Статья от разработчика из Near о реализации ed25519. В ней можно посмотреть одну из эффективных реализаций И, конечно же, оригинальная статья о ed25519 со всеми подробностями и сравнениями с другими алгоритмами Также я достаточно мимолётно упомянул об аппаратной "дружелюбности" ed25519 и о его стойкости к утечками через кэши или миспредикты предсказателя переходов. О том, как работает аппаратное обеспечение компьютеров, и почему есть такие уязвимости, можно узнать в следующих книгах: Дэвид М. Харрис и Сара Л. Харрис "Цифровая схемотехника и архитектура компьютера" David A. Patterson, John L. Hennessy "Computer Organization and Design: Hardware / Software Interface" David A. Patterson, John L. Hennessy "Computer Architecture: A Quantitative Approach" А по языку Rust на мой взгляд лучшие источники - это: Rust by examples - примеры от Rust community The Rust Programming Language - книга, покрывающая (наверное) все механизмы языка (неофициально растбук) Rust in Action - несколько проектов на Rust под руководством авторов книги Jim Blandy, Jason Orendorff, Leonora F .S. Tindall "Programming Rust" - полный учебник, аналог растбука