Я переписал весь PHP-стек для блокчейна с нуля: 15 пакетов без единой внешней зависимости
Почему я перестал доверять заброшенным крипто-библиотекам приватные ключи — и что из этого вышло
Если вы когда-нибудь прикручивали блокчейн к 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-gmp,ext-sodium,ext-curl);composite комбинируют leaf'ы (подписант EIP-1559 тянет secp256k1 + Keccak + RLP);
meta-пакеты (
eth-php,ton-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:
Packagist: https://packagist.org/packages/amashukov/
Если вы строите крипто-платежи на PHP и хотите владеть своей supply chain, а не арендовать её у заброшенного репозитория, — код открыт, читайте, тестируйте, ломайте. Issues и PR приветствуются.