Перейти к содержанию

Bitcoin для веб-разработчика: транзакции, кошелёк и платежи

Представим учебную библиотеку: читатель выбирает набор материалов, а сайт создаёт счёт. Чтобы добавить Bitcoin, недостаточно нарисовать адрес и показать сообщение «Оплачено». Нужно понять, что именно проверяет сервер, когда транзакция считается обнаруженной и почему полученный результат может измениться. Начнём с устройства платежа, затем перенесём его в локальную сеть и свяжем с обычным веб-приложением.

Для чтения достаточно знать HTTP, JSON и работу с командной строкой. Все суммы в наших собственных данных будут выражены целым числом сатоши. Учебные операции предназначены для regtest; действующий сайт ProfessorWeb и реальные кошельки в примере не участвуют.

Что кошелёк действительно тратит

В привычной базе данных у пользователя может быть одна строка с балансом. В Bitcoin исходная модель другая: существуют выходы предыдущих транзакций, которые ещё не потрачены. Такой выход называют UTXO — unspent transaction output. Его однозначно задают идентификатор транзакции и номер выхода: пара txid и vout.

Пусть кошелёк контролирует выход на 80 000 сатоши. За набор материалов нужно заплатить 30 000, а комиссия выбранной транзакции составляет 700. Кошелёк использует весь старый выход и создаёт новые: 30 000 продавцу и 49 300 себе. Эти 49 300 — сдача. Нельзя «уменьшить старый UTXO на тридцать тысяч»: он расходуется полностью, а остаток становится другим выходом.

Часть операции Сумма, сатоши Назначение
Вход 80 000 Ссылка на ранее полученный выход
Выход 0 30 000 Платёж библиотеке
Выход 1 49 300 Сдача плательщику
Разница 700 Комиссия

Для обычной транзакции действует соотношение: сумма входов равна сумме выходов плюс комиссии. Но сервер магазина не обязан разбирать все чужие входы, чтобы принять платёж. Его задача — найти выходы, относящиеся к своему счёту, и проверить их состояние. Общая сумма транзакции может включать сдачу и платежи другим получателям.

Адрес задаёт способ получения платежа, а приватный ключ позволяет удовлетворить условия траты соответствующего выхода. Подпись подтверждает разрешённое действие с конкретной транзакцией; она не раскрывает ключ. При этом адрес нельзя считать удостоверением личности. Сервис знает, какой счёт он связал с адресом, но один этот факт ничего не говорит о паспорте отправителя или владельце браузера.

Комиссия зависит от транзакции

Размер перевода и стоимость его обработки — разные величины. Для оценки комиссии важны виртуальный размер транзакции и ставка в сатоши на виртуальный байт. Перевод с несколькими входами обычно сложнее перевода с одним входом. Поэтому «процент от покупки» плохо описывает сетевую комиссию.

На странице счёта отдельно показывают требуемую сумму получателю и сведения о комиссии кошелька. Продавец не должен молча вычитать сетевую комиссию из стоимости товара. Если счёт требует 30 000 сатоши, выход на 29 300 не становится полным платежом лишь потому, что отправитель потратил ещё 700 на обработку.

Сдача также влияет на приватность. Если повторно использовать один адрес для всех покупателей, сторонний наблюдатель получает удобную точку для сопоставления операций. Для последовательного курса мы будем выдавать отдельный адрес счёту и сохранять его связь с заказом на сервере. Это уменьшает неоднозначность учёта, но не делает платежи анонимными.

Выбираем среду для учебной работы

У Bitcoin есть основная сеть и отдельные среды разработки. В regtest узел создаёт локальную цепочку, а разработчик сам определяет момент появления блока. В публичной тестовой среде уже приходится учитывать доступность других узлов, получение тестовых монет и ожидание блоков. Для разбора состояний заказа удобнее начать с полностью контролируемого regtest.

Ниже — сценарий для читателя, у которого установлен Bitcoin Core. Он использует отдельный каталог, а не существующее хранилище кошелька. RPC и P2P ограничены локальной машиной. Команды приведены как учебная последовательность; фактические ответы зависят от установленной сборки.

mkdir -p ./bitcoin-library-lab

bitcoind -regtest -datadir="$PWD/bitcoin-library-lab" \
  -server -listen=0 -rpcbind=127.0.0.1 -rpcallowip=127.0.0.1 -daemon

bitcoin-cli -regtest -datadir="$PWD/bitcoin-library-lab" getblockchaininfo
bitcoin-cli -regtest -datadir="$PWD/bitcoin-library-lab" createwallet "library"
bitcoin-cli -regtest -datadir="$PWD/bitcoin-library-lab" createwallet "reader"

В ответе getblockchaininfo проверяем поле chain: ожидается regtest. Эта проверка важнее самого факта успешного запуска. Два созданных кошелька играют разные роли: библиотека принимает перевод, читатель отправляет его. Наличие нескольких кошельков требует явно выбирать нужный через -rpcwallet, иначе команда может относиться не к той роли или завершиться ошибкой.

Чтобы получить учебные монеты, сначала создадим адрес кошелька читателя, затем укажем его при генерации блоков:

bitcoin-cli -regtest -datadir="$PWD/bitcoin-library-lab" \
  -rpcwallet=reader getnewaddress

# Вместо READERS_ADDRESS подставляется адрес из предыдущего ответа.
bitcoin-cli -regtest -datadir="$PWD/bitcoin-library-lab" \
  generatetoaddress 101 "READERS_ADDRESS"

bitcoin-cli -regtest -datadir="$PWD/bitcoin-library-lab" \
  -rpcwallet=reader getbalance

Первоначальная награда за блок не становится сразу доступной для обычной траты. Генерация 101 блока позволяет получить созревшую награду самого раннего блока. Эти монеты существуют только в учебной цепочке. Отдельная команда help generatetoaddress поможет проверить интерфейс установленной версии, прежде чем продолжать лабораторию.

Теперь библиотека получает новый адрес для конкретного счёта. Читатель отправляет на него 0.00030000 BTC — это 30 000 сатоши. Команда кошелька вернёт идентификатор отправленной транзакции, а не доказательство завершённой покупки. После создания очередного блока состояние можно запросить повторно. Если транзакция действительно вошла в него, число подтверждений увеличится.

Счёт принадлежит серверу

В нашей библиотеке браузер отправляет запрос «Создать счёт для набора 17». Сервер сам узнаёт стоимость набора, выдаёт адрес, сохраняет время создания и формирует идентификатор заказа. Значения суммы и оплаченности, присланные браузером, не становятся доверенными данными.

Вот собственная учебная запись счёта. Строка address здесь обозначает поле, которое сервер заполняет реальным regtest-адресом; это не готовый адрес для отправки.

{
  "invoiceId": "library-17-001",
  "network": "regtest",
  "address": "ADDRESS_FROM_LIBRARY_WALLET",
  "expectedSats": "30000",
  "requiredConfirmations": 2,
  "status": "awaiting_payment"
}

Сумма передаётся строкой с целым числом. Это сохраняет точность при обмене с JavaScript и допускает последующее преобразование в BigInt. Для показанных значений обычное число тоже помещается в безопасный диапазон, но единый контракт данных избавляет от неожиданной смены представления в будущем.

Сервер опрашивает узел или обрабатывает уведомления, затем сверяет актуальное состояние с узлом. При использовании кошелькового RPC важно помнить: gettransaction описывает транзакцию, известную выбранному кошельку. Он не заменяет произвольный глобальный поиск всех переводов. Сумма верхнего уровня и массив details также имеют разные назначения. Для счёта нас интересуют подходящие входящие записи и соответствующие выходы.

Если JSON-RPC возвращает десятичную сумму BTC как число, нельзя сначала потерять точность в двоичном Number, а потом считать, что умножение на сто миллионов восстановило исходное значение. В полноценном адаптере требуется сохранять десятичное представление при разборе ответа и явно переводить его в сатоши. В собственной модели уже храним сатоши, поэтому дальнейшие сравнения выполняются без дробной арифметики.

Описываем состояния вместо одного флажка

Для веб-интерфейса полезнее несколько состояний, чем paid: true. Разберём их на том же счёте.

Состояние Что известно Что показывает сайт
Ожидается платёж Подходящих выходов пока нет Адрес, сумма и срок счёта
Обнаружен Узел видит подходящий перевод Найдена транзакция; подтверждений ещё недостаточно
Частичная оплата Получено меньше требуемого Полученная сумма и недостающая часть
Подтверждается Сумма достаточна, политика ещё не выполнена Текущее число подтверждений
Принят Выполнены правила суммы и подтверждений Доступ к набору материалов
Конфликт или переоценка Наблюдение изменилось Статус пересчитывается; действие поддержки

Два подтверждения здесь — условие нашей лаборатории, а не универсальная гарантия для любой стоимости покупки. Реальная политика зависит от назначения сервиса, суммы, допустимого риска и способа наблюдения. В учебном интерфейсе порог делаем частью записи счёта, чтобы изменение правил было видно.

Платёж может прийти после срока счёта или превысить требуемую сумму. Эти случаи требуют заранее выбранного поведения. Сервис может перевести заказ на ручное рассмотрение, но не должен незаметно потерять перевод. Аналогично повторное уведомление о той же транзакции не выдаёт второй доступ: обработка должна быть идемпотентной.

Когда прежний результат меняется

Транзакция в mempool ещё не закреплена блоком. Она может исчезнуть из наблюдаемого mempool или быть заменена. После включения в блок остаётся возможность изменения ветки цепочки. Поэтому число подтверждений, наличие блока и конфликтующие операции относятся к текущему наблюдению, а не к вечной характеристике заказа.

В обработчике удобно хранить ключ отдельного полученного выхода — txid:vout. Он позволяет отличить две записи одной транзакции и не учитывать один выход дважды. Вместе с ним сохраняются номер и хеш наблюдаемого блока. Если блок больше не относится к принятой ветке, сервер пересчитывает связанное состояние, а не продолжает безусловно считать выход подтверждённым.

Важно разделять сетевую оценку и уже совершённое действие бизнеса. Если доступ однажды выдан, автоматическое изменение числа подтверждений не должно самовольно создавать новый заказ или запускать возврат. Для этого нужна отдельная политика сервиса и журнал решения. Блокчейн даёт данные о переводе; применение этих данных к покупке остаётся задачей приложения.

Узел не становится публичным API сайта

Браузер обращается к небольшому API библиотеки, например GET /api/invoices/library-17-001. Сервер возвращает разрешённые поля счёта и текущий статус. Доступ к административным RPC-методам Bitcoin Core, файлам кошелька и cookie аутентификации остаётся на стороне сервера. Произвольное имя RPC-метода из запроса пользователя не пересылается узлу.

Такое разделение позволяет ограничить частоту запросов, проверить владельца заказа и не раскрывать внутреннюю структуру кошелька. Уведомление о переводе тоже не является достаточным основанием для выдачи доступа: его содержимое проверяется по собственному узлу. При временной недоступности узла интерфейс показывает «Проверка временно недоступна», сохраняя последнее наблюдение отдельно от свежего результата.

Полезная ручная проверка будущей лаборатории — последовательно создать обычный перевод, частичный перевод, повторное уведомление, поздний платёж и изменение подтверждений. Ожидаемый результат каждого сценария определяется таблицей состояний. При подготовке этого текста узел, операции и проверки не запускались; таблица задаёт проверяемые требования для следующего шага.

Дескрипторы и подготовка транзакции

Кошельку нужно понимать не только отдельные адреса, но и правила создания следующих адресов и обнаружения принадлежащих ему выходов. Дескриптор описывает соответствующий набор условий и ключевого материала. Публичный дескриптор может помогать наблюдать платежи без возможности подписывать траты. Однако отсутствие приватного ключа не делает его безобидным для публикации: он способен раскрыть связь многих адресов и операций.

Watch-only кошелёк удобно изучать отдельно от подписывающего. Сервер библиотеки может наблюдать входящие средства, а полномочия на расходование находиться в другой среде. Такой способ уменьшает объём доступных серверу действий, но не заменяет защиту самого сервера и проверки выводимого адреса. Если атакующий подменит адрес на странице счёта, защищённое подписание расходных операций не поможет покупателю.

PSBT — формат частично подписанной Bitcoin-транзакции — позволяет разделить подготовку, дополнение данных и подписи разных участников. Подготовившая сторона выбирает выходы и параметры, подписывающая проверяет получателей, суммы, сдачу и условия. Готовый пакет не означает, что транзакция уже включена в блок, и не доказывает правильность реквизитов.

Для лаборатории полезно показать два окна: описание предполагаемой оплаты и описание подготовленной транзакции. Если адрес получателя или комиссия отличаются, подпись не должна проходить лишь потому, что файл имеет правильный формат. После подписания и публикации всё равно продолжается обычное наблюдение за сетевым состоянием.

Script, Schnorr и Taproot

Выход транзакции задаёт условия будущей траты. Bitcoin Script помогает выразить эти условия, а данные тратящей транзакции должны им соответствовать. Такой контракт отличается от модели аккаунтов EVM: мы не вызываем один глобальный объект с постоянно меняющимся полем баланса.

SegWit и Taproot меняют способы представления и проверки определённых видов трат. Для Taproot важно различать путь по ключу и раскрытие подходящего пути скрипта. Изучение Schnorr объясняет используемые свойства подписей, но из этого нельзя вывести, что любая Taproot-транзакция скрывает участников или становится полностью анонимной.

В веб-приложении сначала определяют поддержку выбранного формата адреса кошельками, способ построения и проверки транзакции и сохранение данных наблюдения. Реализацию криптографической примитивы не стоит подменять короткой собственной функцией ради урока: для практической работы нужен рассмотренный инструмент, а ручная арифметика служит объяснению механизма.

Lightning и другой жизненный цикл счёта

Lightning использует каналы и платежи, состояние которых нельзя описать лишь количеством блоковых подтверждений каждой покупки. Платёжный запрос, срок действия, доступный маршрут и ликвидность становятся самостоятельными условиями. Достаточный общий баланс узла ещё не означает, что любой нужный платёж пройдёт по выбранному маршруту.

Для библиотеки это новый адаптер оплаты. Он создаёт собственный счёт и сохраняет идентификатор, требуемую сумму и ожидаемый результат. Интерфейс различает созданный, истёкший и рассчитанный счёт. При повторном уведомлении доступ выдаётся один раз, а потеря соединения с узлом не превращается в доказательство неоплаты.

Оплата в цепочке Оплата Lightning
Наблюдаем выходы транзакций Наблюдаем состояние конкретного счёта
Политика использует подтверждения Политика использует результат расчёта счёта
Комиссия зависит от структуры транзакции Успех и стоимость связаны с маршрутом и условиями платежа
Возможны замена и изменение ветки Возможны истечение, отсутствие маршрута и повторное наблюдение

Подготовку каналов и восстановление узла рассматриваем отдельно от публичной страницы оплаты. Пользовательский API не должен давать браузеру административные полномочия Lightning-узла. Сначала нужно объяснить локальную модель и независимые состояния; подключение реальной инфраструктуры является следующим этапом.

Как расширять этот маршрут

После базового интерфейса можно разобрать дескрипторные кошельки, выбор монет и PSBT. PSBT позволяет разделить подготовку транзакции и подписание, но сам формат не гарантирует правильность получателя: подписывающая сторона всё равно должна понимать операцию. Taproot и Schnorr изучаем после Script и устройства выходов, иначе новые названия лишь заслоняют знакомую модель траты.

Lightning — отдельная последовательность уроков. Канал, платёжный запрос, ликвидность и состояние расчёта не сводятся к обычному числу подтверждений Bitcoin-транзакции. Для веб-сервиса понадобится другая модель счёта, хотя правила серверного учёта, повторной обработки и недоверия к данным браузера сохраняются. Поэтому первую библиотеку доводим до ясного результата в regtest, а затем добавляем новый способ оплаты как самостоятельный адаптер.

Интерфейс RPC и поля наблюдения сверяются по документации Bitcoin Core и описанию gettransaction. Границы доступа к узлу разобраны в документе проекта о JSON-RPC. Общий порядок чтения и будущие лаборатории собраны в маршруте Bitcoin, Solidity и Web3.