У криптоматов, криптообменников, мерчантов и всех других, кто принимают биткоин, стоит задача обрабатывать платежи как можно быстрее. С учетом среднего подтверждения биткоин блока в 10 минут, возникает вопрос: можно ли засчитать получение платежа, если транзакция на депозит находится в мемпуле и еще не подтверждена? Иными словами, насколько реалистично заменить или отменить транзакцию, которая уже находится в мемпуле?

TL;DR: я развернул свою ноду Bitcoin Core v24.0, которая поддерживает full-RBF и попытался заменить транзакцию — у меня ничего не вышло.

Больше новостей про продакт-менеджмент, технологии и предпринимательство в крипте в моем телеграм канале: @grin_channel

Что такое RBF и почему это стало актуальным

RBF (replace by fee) — замена неподтвержденной транзакции новой транзакцией с более высокой комиссией сети. Под заменой имеется в виду, что новая транзакция использует тот же инпут, что и исходная.

Физический смысл RBF в том, что майнеру (создателю блока) выгодней взять транзакцию с более высокой комиссией. А так как у транзакций одинаковый инпут, то вторая транзакция становится невалидной.

Еще в 2016 год вышло обновление ноды Bitcoin Core v0.12.0, которое включило в себя поддержку Opt-in RBF. Это функция, которая позволяет создавать транзакции со специальным флагом, сигнализирующим о том, что транзакция может быть заменена. Технически это выглядит так: у каждой транзакции есть свой порядковый номер nSequence, определяющий очередность взятия транзакций в блок. И, чтобы сделать транзакцию потенциально незаменимой, нужно было указать максимально возможный порядковый номер — 0xffffffff. Например, блокчеин эксплорер mempool.space об этом указывает явно:

Источник: https://mempool.space/
Источник: https://mempool.space/

По правилам протокола Bitcoin, все повторные транзакции с тем же инпутом должны быть отклонены и не распространяться нодами дальше по сети. Это правило называется first seen rule. В крайнем случае, так было раньше.

В декабре 2022 году вышло обновление ноды Bitcoin Core 24.0.1, которое включает в себя full-RBF. Это функция, которая позволяет нодам обрабатывать повторные транзакции и считать их легитимными. По дефолту в ноде эта функция выключена, поэтому в настройках ноды для ее активации необходимо задать параметр mempoolfullrbf=1

На февраль 2023 года почти 30% всех нод Bitcoin перешли на версию 24.0.1:

Источник: https://bitnodes.io/dashboard/1y/
Источник: https://bitnodes.io/dashboard/1y/

Согласно исследованию, из всех нод версии 24.0.1 минимум 17% активировали full-RBF.

Пошаговая инструкция по замене транзакций

Я решил развернуть ноду Bitcoin v24.0.1 и попробовать заменить транзакцию. Ниже поэтапный план действий.

Системные требования Bitcoin Core с bitcoin.org:

  • Disk space. 350 GB.

  • Download. 500 MB/day (15 GB/month)

  • Upload. 5 GB/day (150 GB/month)

  • Memory (RAM) 1 GB.

  • System. Desktop. Laptop. Some ARM chipsets >1 GHz.

  • Operating system. Windows 7/8.x/10. Mac OS X.

У меня по факту заняло около 480 GB, поэтому лучше заложить дискового пространства побольше.

Шаг 1. Скачиваем ноду Bitcoin Core v24.0.1

Скачиваем пропатченную ноду Bitcoin Core v24.0.1. Нода специально подключается к пирам, которые поддерживают full-RBF и способствуют распространению повторных транзакций по сети:

git clone -b full-rbf-v24.0.1 https://github.com/petertodd/bitcoin.git

Детальней об этой ноде можно прочитать здесь. Либо можно скачать оригинальную ноду и затем в настройках указать пиры, у которых стоит full-RBF. Об этом будет еще описано ниже.

Шаг 2. Компилируем исходники и запускаем ноду

Вначале устанавливаем все необходимые зависимости.

Linux:

sudo apt-get install automake autotools-dev bsdmainutils build-essential ccache clang gcc git libboost-dev libboost-filesystem-dev libboost-system-dev libboost-test-dev libevent-dev libminiupnpc-dev libnatpmp-dev libqt5gui5 libqt5core5a libqt5dbus5 libsqlite3-dev libtool libzmq3-dev pkg-config python3 qttools5-dev qttools5-dev-tools qtwayland5 systemtap-sdt-dev

Mac OS:

brew install automake boost ccache git libevent libnatpmp libtool llvm miniupnpc pkg-config python qrencode qt@5 sqlite zeromq

Дальше компилируем ноду Bitcoin:

./autogen.sh
./configure
make

Если вы хотите запустить компиляцию сразу на нескольких ядрах, то для команды make необходимо задать соответствующие параметры:

Linux

make -j "$(($(nproc) + 1))"

Mac OS

make -j "$(($(sysctl -n hw.physicalcpu) + 1))"

У меня на 8 GB RAM и 4 ядрах компиляция заняла около 15 минут. Если у вас на этом этапе возникли проблемы, то можете посмотреть раздел Troubleshooting здесь.

Шаг 3. Создаем конфигурационный файл bitcoin.conf

Файл необходимо создать в корневой папке ./bitcoin. Есть даже онлайн генератор конфигов, мне во время эксперимента понадобились следующие параметры:

mempoolfullrbf=1 #Включает full-RBF функционал
txindex=1 #Индексирует внешние транзакции, которые не принадлежат кошельку ноды
datadir=<dir_path> #Если необходимо сменить директорию, куда нода будет загружать все данные

Остановлюсь подробней на параметре txindex=1. Если не включить этот параметр, то нода не сможет обращаться к входным и выходным параметрам транзакций, которые не относятся к кошельку ноды. Это может понадобиться, когда вы пытаетесь отправить повторную транзакцию с использованием того же входа, который по факту является ссылкой на выход внешней транзакции. Этот параметр потребует дополнительно порядка 20 GB дискового пространства под хранение индексированных данных.

Если у вас оригинальная нода, и вы хотите добавить пиры с включенным full-RBF, то в конфигурационный файл добавьте еще следующие параметры:

addnode=full-rbf1.btc.petertodd.net
addnode=full-rbf2.btc.petertodd.net
addnode=full-rbf3.btc.petertodd.net
addnode=full-rbf4.btc.petertodd.net

Список пиров с включенным full-RBF взят здесь.

Шаг 4. Запускаем ноду и создаем кошелек

Запускаем ноду при помощи команды ./bitcoind -daemon. Если вы не хотите запускать ноду в фоновом режиме, то запускайте без параметра -daemon

Дальше нода должна начать синхронизацию, у меня весь процесс занял порядка трое суток.

Обращаться к ноде можно при помощи bitcoin-cli. Например, посмотреть сколько уже скачено блоков можно следующей командой: ./bitcoin-cli getblockcount. Со всеми командами можно ознакомиться здесь.

После полной синхронизации необходимо создать кошелек. Это можно сделать разными способами (импортировать, сгенерировать), например, создать командой createwallet:

./bitcoin-cli createwallet "testwallet"

Шаг 5. Подготавливаем скрипт

Я использовал python-скрипты отсюда.

Для начала стоит убедиться, что вы можете обращаться к локальной Bitcoin ноде через python-bitcoinlib:

python3 /lib/python-bitcoinlib/examples/ssl-rpc-connection.py

Дальше можно взять за основу doublespend.pyи сделать следующее:

  1. Отправить первую транзакцию с network fee значительно меньшим, чем у текущих транзакций в мемпуле. Так, транзакция «застрянет» в мемпуле и не попадет в блок;

  2. Подождать минуту, пока транзакция распространяется по сети;

  3. Отправить вторую транзакцию с network fee в разы превышающим текущих транзакций в мемпуле. Так, у майнера будет мотивация взять именно повторную транзакцию в блок.

Что в итоге получилось

Я проводил эксперимент несколько раз — результат был одинаков. Рассмотрим на примере одной из итераций:

Время создания

Network fee

Транзакция №1

00:00

0.0000113 BTC (0.26$)

Транзакция №2

00:19

0.004068 BTC (±94$)

Первую транзакцию я отправил с network fee = 0.26$. Это значительно ниже рыночных условий. Несмотря на то, что вторая транзакция имела fee в 94$, майнеры (создатели блоков) все равно ее не брали. То есть replace-by-fee не сработал.

Примечательно, как блокчеин эксплореры реагировали на повторные транзакции. В основном все отображали только первую транзакцию (например, mempool.space или blockchair). Эксплорер Blockcypher отображает повторные транзакции и предупреждает о double-spend:

Источник: https://live.blockcypher.com/
Источник: https://live.blockcypher.com/

Я предполагаю, что майнеры не могут взять повторную транзакцию в блок, так как этот блок будет отвергнут большинством участников сети. И пока Bitcoin Core v24.0.1 с включенным full-RBF стоит у меньшинства участников, замена транзакций работать не будет.

Если вам понравилась статья, то подписывайтесь на мой телеграм канал, где я рассказываю про продакт-менеджмент, технологии и предпринимательство в крипте: @grin_channel

Комментарии (10)


  1. DustCn
    00.00.0000 00:00
    +1

    А зачем в $(sysctl -n hw.physicalcpu) + 1 добавляется еденица? Или на маке при одном ЦПУ там ноль?


    1. zepars Автор
      00.00.0000 00:00
      -1

      В команде “make -j”, параметр определяет количество джоб для параллельного вычисления. Верхнее ограничение необязательно определяется количеством доступных ядер или потоков процессора. Максимально допустимое число джоб зависит еще от количества доступной памяти, сколько каждая джоба памяти потребляет, I/O задержки и тд. 

      Так как сложно выделить метрику, сколько реально джоб оптимальней всего запустить, то обычно привязываются на количество ядер. Но можно поставить количество джоб чуть выше и это может ускорить процесс.

      Вот тут об этом детальней обсуждают: https://unix.stackexchange.com/questions/208568/how-to-determine-the-maximum-number-to-pass-to-make-j-option


      1. DustCn
        00.00.0000 00:00
        +2

        Так я поэтому и спросил, зачем берется больше на один процесс, который будет бегать по ядрам, трешить кэш и всем мешать?


        1. zepars Автор
          00.00.0000 00:00

          Я не знаю детально как работают джобы под капотом, поэтому сложно что-то сказать. Но я сделал пару итераций, чтобы сравнить результаты:

          time make -j "$(($(sysctl -n hw.physicalcpu)))"
          >make -j "$(($(sysctl -n hw.physicalcpu)))"  2066.41s user 136.78s system 739% cpu 4:58.08 total
          
          time make -j "$(($(sysctl -n hw.physicalcpu)))"
          >make -j "$(($(sysctl -n hw.physicalcpu)))"  2040.59s user 135.83s system 734% cpu 4:56.25 total
          
          time make -j "$(($(sysctl -n hw.physicalcpu) + 1))"
          >make -j "$(($(sysctl -n hw.physicalcpu) + 1))"  1939.44s user 131.89s system 730% cpu 4:43.57 total
          
          time make -j "$(($(sysctl -n hw.physicalcpu) + 1))"
          >make -j "$(($(sysctl -n hw.physicalcpu) + 1))"  2026.15s user 138.11s system 716% cpu 5:01.90 total

          Разброс получается слишком большой, чтобы можно было дать статистически значимый результат)


    1. alexeishch
      00.00.0000 00:00

      Про мак не скажу, не использую, но в линуксе добавление единички поверх количества логических(а nprocесли у тебя HyperThreading возвращает количество ядер умноженное на два) процессоров чаще всего помогает чтобы быстро работало на виртуальных машинах в хостинге.

      Собственно у тебя есть два три варианта как распараллелить компиляцию:
      1. Минимальное по CPU: количество джоб равно количеству процессоров - меньше делать не стоит по причине того что все что меньше будет работать медленее
      2. Максимальное по CPU: количество джоб равно количеству процессоров умноженному на 2 - больше делать не стоит по причине того что ты точно не знаешь какие из задач компилятора требуют максимальной нагрузки на процессор, а какие на подсистему ввода вывода и большее количество джоб уже будет радикально снижать производительность.
      3. Оптимальное по памяти: количество джоб равно количество RAM в гигабайтах деленное на 2 или 4 в зависимости от наличия HT.

      На своём компьютере ты должен в зависимости от конфигурации самостоятельно подобрать это число (ну если ты используешь линукс постоянно). Оно у тебя будет больше того что в пункте 1, но меньше чем в пункте 2. Если у тебя число 3 меньше чем 1 - то на компьютер нужно добавить памяти.

      Вот в советах всегда советуют нижнюю планку + 1 просто потому что с большой долей вероятности это будет оптимальный вариант на незнакомой конфигурации, а также для облака. На своём компе который ты постоянно используешь ты конечно же должен уже знать точное число )

      P.S. Судя по всему для мака будет всегда использоваться количество физических процессоров, в то время как для линукса - логических. Это наводит на мысль что люди перепечатывают инструкции не думая )


  1. Eugene_Rymarev
    00.00.0000 00:00

    @zepars, в итоге получается, когда нод full-RBF будет больше половины, то такие транзы будут добавляться в блокчейн?


    1. zepars Автор
      00.00.0000 00:00
      +1

      Да. Причем помимо Bitcoin Core с включенным full-RBF, есть еще неофициальные ноды, которые тоже могут поддерживать эту функцию (например, Bitcoin Knots). На текущий момент подключенных к сети кастомных нод – 7.3%


      1. Eugene_Rymarev
        00.00.0000 00:00

        И в этом случае биткоиноматы станут уязвимыми? Или в них можно будет добавить защиту на проверку этого параметра замены транзакции?


        1. zepars Автор
          00.00.0000 00:00

          В этом случае (хотя и сейчас для перестраховки надежней так делать) надо дождаться, когда майнер создаст блок и добавит туда транзакцию. Появление нового блока в среднем занимает 10 минут, а может продлиться и вовсе до 40, поэтому некоторые бизнесы не дожидаются этого и сразу засчитывают транзакцию на этапе попадания ее в мемпул.


  1. ebt
    00.00.0000 00:00

    Относительно или безотносительно описанного функционала, может быть, кто-нибудь может порекомендовать веб-GUI к Bitcoin Core? С QT как-то исторически не сложилось.