Почему я перестал доверять заброшенным крипто-библиотекам приватные ключи — и что из этого вышло

Если вы когда-нибудь прикручивали блокчейн к PHP-бэкенду, вы знаете это тихое чувство тревоги при взгляде на composer.json.

web3p/web3.php? Заброшен несколько лет назад, EIP-1559 так и не выучил. Популярные пакеты Keccak и эллиптической криптографии? Один мейнтейнер, обновления раз в год. Поддержка TON? В лучшем случае частичная — где-то недописанный кошелёк, где-то нет канонического BOC-энкодера.

А теперь вспомните, чего эти библиотеки касаются: код, который выводит адреса, подписывает транзакции и работает с приватными ключами. Rug-pull, заброшенный мейнтейнер или один скомпрометированный релиз — и вы в одном composer update от слитого кошелька.

Для пет-проекта — ладно. Для платёжной системы, которая подписывает реальную стоимость on-chain, такая supply chain неприемлема.

Поэтому я сделал нерациональную вещь: переписал весь стек. 15 пакетов, чистый PHP, ноль внешних runtime-зависимостей — только PHP-расширения (ext-*) и PSR-интерфейсы. Каждая строчка криптокода своя, аудируемая, и привязана к эталонным тестовым векторам.

Ниже — почему и как, включая три прод-бага, которые и оправдали всю затею.


«PHP для крипты? Серьёзно?» — да, и вот честная ниша

Сразу отвечу на главный вопрос Хабра. Это не замена viem/ethers по масштабу. PHP + крипта — это конкретная ниша: финтех, платежи, on-ramp, merchant-инструменты, iGaming, Telegram Mini Apps. Если у вас уже есть PHP/Symfony-бэкенд и нужно подписывать и сеттлить реальную стоимость на TON или EVM — у вас два пути: поднимать Node-сайдкар ради подписи, либо иметь проверенный PHP-стек. Это про второй путь.


Архитектура: пирамида зависимостей, а не монолит

Стек послойный — каждый кусок полезен и тестируется отдельно:

leaf:      keccak-php  secp256k1-php  rlp-php  ton-cell-php  ton-crypto-php  http-client-php
composite: abi-encoder-php   eip1559-tx-signer-php   ton-wallet-php
RPC:       eth-rpc-client-php          toncenter-client-php
meta:      eth-php  ◄────────────────►  ton-php
bundle:    blockchain-context-bundle  (Symfony 7)
  • leaf-пакеты не зависят ни от чего, кроме PHP-расширения (ext-gmpext-sodiumext-curl);

  • composite комбинируют leaf'ы (подписант EIP-1559 тянет secp256k1 + Keccak + RLP);

  • meta-пакеты (eth-phpton-php) — установка всего SDK одной строкой;

  • Symfony 7 бандл автовайрит всё для пользователей фреймворка.

composer require amashukov/ton-php
composer require amashukov/eth-php

В vendor/ из этого стека попадает только другой amashukov/*-код и PHP-расширения. Никаких транзитивных крипто-зависимостей, которые вы не проверяли.


Подводный камень №1 — мнемоника TON это не BIP-39

Эта тихо выдаёт пользователю чужой кошелёк.

Почти каждая PHP-библиотека мнемоник реализует BIP-39. TON BIP-39 не использует. У него своя схема: PBKDF2-HMAC-SHA512, 100 000 итераций, фиксированная соль "TON default seed", плюс отдельный раунд проверки «это вообще валидная TON-мнемоника» перед деривацией ключа.

Скормите TON-сид-фразу BIP-39-библиотеке — получите валидный на вид keypair, но для совершенно другого адреса. Ваше детектирование депозитов будет слушать адрес, на который пользователь никогда не отправит.

amashukov/ton-crypto-php реализует настоящую схему TON (под капотом Ed25519 через libsodium), так что адрес, который вы выводите, — это адрес, который выводит @ton/crypto. Сверено по эталонным векторам.


Подводный камень №2 — канонический BOC и обрыв на 255 байтах

TON сериализует всё в ячейки (TLB-формат «Bag of Cells»). Чуть-чуть не так разложите байты — сообщение отвергнет сеть, или, хуже, примет, но распарсит не в то, что вы имели в виду.

Ловушка: в заголовке BOC кодируется offset_byte_size. Для маленьких payload'ов это один байт. Как только суммарный размер сериализации переваливает за 255 байт, он обязан авто-промоутиться до двух байт. Пропустите этот промоушен — и всё, что выше порога, корраптится.

amashukov/ton-cell-php реализует канонический энкодер (флаг has_crc32c, little-endian CRC32C-хвост, авто-промоушен offset_byte_size 1→2) и доказывает это единственным значащим способом — byte-for-byte parity-тестами против @ton/core v15. Тот же вход — идентичные байты. Этот parity-сьют и есть audit trail.


Подводный камень №3 — комиссии EIP-1559 и врущий ноль от Erigon

Два EVM-бага, оба регулярно уезжают в прод в наивном коде.

Формула комиссии. Транзакциям EIP-1559 нужны maxFeePerGas и maxPriorityFeePerGas. Устоявшаяся в экосистеме формула — maxFee = baseFee * 2 + tip. Но реальная боль в краевом случае: под нагрузкой часть RPC-ответов возвращается без поля baseFeePerGas вообще. Умножаете null — либо падаете, либо подписываете транзакцию с мусорным fee-cap, которая никогда не майнится. amashukov/eth-rpc-client-php явно гардит отсутствующее поле.

0x от Erigon. Спросите у Erigon-ноды (а они стоят за многими RPC-прокси) баланс свежего аккаунта — и он может вернуть голую строку '0x' вместо корректного '0x0'. Наивный hexdec() превращает это в мусор или ошибку. Клиент нормализует.

Поверх сырого eth_*-зеркала пакет даёт фасад JsonRpcProvider в стиле ethers.js v6 — знакомые имена методов, bigint-safe через GMP — и ABI-энкодер, сверенный byte-for-byte с ethers.js v6. Что ethers выдаёт как calldata, то выдаёт и этот пакет.

(Бонусные грабли бесплатно: никогда не делайте ltrim($signature, '0x'). Подпись, у которой компонент r начинается с нулевого байта, тихо помангается. Срезайте ровно префикс, а не ведущие нули.)


Качество: часть, которая делает это нанимаемым

Ноль зависимостей убедителен только если код доказуемо корректен. На каждом из 15 пакетов:

  • PHPStan level 9 — строжайший уровень, без baseline, без @phpstan-ignore-лазеек;

  • PER-CS через php-cs-fixer;

  • Rector на собственном наборе правил (amashukov/rector-php-rules), который банит привычную гниль: заглушенные ошибки статанализа, доступ к суперглобалам, env-проверки в доменном коде;

  • GitHub Actions CI — зелёный на каждый push, бейджи в каждом README;

  • parity-тесты против эталонных JS/TS-реализаций (@ton/core, ethers.js) везде, где есть каноническая сериализация.

Последний пункт — и есть весь смысл. «Доверьтесь мне» для крипты не работает. «Вот тест, который выдаёт те же байты, что и эталонная реализация» — работает.

use Amashukov\Keccak\Keccak;

$selector = substr(Keccak::hash('transfer(address,uint256)', 256), 0, 8);
// → a9059cbb  (совпадает с ethers.js id())

Честно про ограничения

  • Это 0.x — публичный API может меняться до тега 1.0; я жду внешних потребителей, чтобы пропинить shape.

  • Аудитория узкая (см. секцию про нишу) — это не «убийца ethers», это инструмент для PHP-команд, которым нужна крипта здесь и сейчас.

  • Maintenance-долг реален: toncenter версионируется, quirks Alchemy/Erigon дрейфуют, finality-правила меняются после форков. Я держу это в проде, поэтому баги ловятся в бою, а не в issue-трекере через полгода.


Посмотреть и сломать

Все 15 пакетов под лицензией MIT:

Если вы строите крипто-платежи на PHP и хотите владеть своей supply chain, а не арендовать её у заброшенного репозитория, — код открыт, читайте, тестируйте, ломайте. Issues и PR приветствуются.