December 11

Темпография. Spending unconfirmed UTXO - механизм расходования неподтверждённых UTXO в сети Bitcoin

Темпография и Биткоин

При чём тут темпография?

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

Итак, давайте разбираться…

Создание транзакций с неподтверждёнными выходами

Начну с главного: что означает spending unconfirmed UTXO?

Коротко - это формирование транзакции, которая использует выход (UTXO), полученный из другой транзакции, которая ещё не подтверждена (не вошла в блокчейн).

Проще говоря, тратим средства, которые находятся в неподтверждённой транзакции.

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

Как создать такую транзакцию на практике?

Bitcoin Core (и большинство кошельков подобного типа) поддерживает расходование своих же неподтверждённых выходов. По умолчанию кошелёк Bitcoin Core считает неподтверждённую «сдачу» (change) надёжной и позволяет тратить её дальше.

Однако поступающие извне неподтверждённые платежи обычно недоступны для трат (кошелёк ждёт хотя бы 1 подтверждение для входящих средств, но чаще - больше).

Пример с bitcoin-cli:

  • Допустим, у вас есть 1 BTC на балансе.
  • Вы выполняете команду отправки: bitcoin-cli sendtoaddress <адрес1> 0.5
  • Это создаёт транзакцию T1, которая тратит ваш вход и выдаёт 0.5 BTC на <адрес1> (например, получателю) и сдачу ~0.5 BTC на новый ваш адрес.
  • Транзакция T1 пока не подтверждена. Теперь сразу сделаем ещё одну отправку:
    bitcoin-cli sendtoaddress <адрес2> 0.4

Получаем следующую схему: если других доступных UTXO нет, кошелёк возьмёт входом именно сдачу (0.5 BTC) из T1. В результате формируется транзакция T2, которая тратит выход из T1, хотя T1 ещё в мемпуле.

Таким образом, T2 - child-транзакция, зависящая от неподтверждённого parent (T1). Оба платежа транслируются в сеть почти одновременно. Кошелёк разрешает такое, поскольку сдача - собственный выход.

Пример ещё один можно привести через через raw transaction (RT). В частности, мы можем создать цепочку вручную - через RPC. Но для начала - давайте поясню про RT:

Raw-транзакция в Биткоине - это буквально “сырой” формат транзакции, представленный в виде последовательности байтов (обычно в hex-строке). Это нерасшифрованная, двоично сериализованная транзакция, такой вид она имеет внутри протокола и при передаче по сети.

Она содержит все поля транзакции:

  • version — версия формата
  • inputs (vin) — какие UTXO расходуются
  • outputs (vout) — кому и сколько BTC отправляется
  • scriptSig / scriptPubKey — скрипты подписи и блокировки
  • witness — данные SegWit-подписей (если есть)
  • locktime — задержка исполнения
  • количество входов/выходов, флаги и т. д.

Raw-транзакция может быть:

  • неподписанной — для оффлайн/аппаратного подписания
  • подписанной — готовой к отправке в мемпул

Её используют для:

  • ручного создания транзакций
  • проверки, анализа, декодирования
  • оффлайн-подписи (air-gapped)
  • отладки кошельков и нод

Пример выглядит так:
0100000001c3a4...00000000

То есть raw-транзакция - это низкоуровневое представление транзакции в том виде, как его понимают узлы Биткоина. (Можете потренироваться на практике: blockchain.com/ru/explorer/assets/btc/decode-transaction).

Теперь давайте предположим, у нас есть TXID неподтверждённой транзакции T1 и индекс выходa, который хотим потратить. Последовательность действий такая:

  • С помощью RPC createrawtransaction создаём болванку новой транзакции, указывая вход: TXID родителя и номер vout, а также один или несколько выходов.
  • Например: bitcoin-cli createrawtransaction '[{"txid":"<TXID_T1>","vout":<N>}]' \ '{"<адрес_получателя>":0.4, "<адрес_сдачи>":0.0999}'
  • Здесь расходуем выход T1 и отправляем часть (0.4 BTC) на адрес получателя, а остальное (например, 0.0999 BTC) - себе на сдачу (учтём при этом комиссию в разнице).
  • Подписываем транзакцию через signrawtransactionwithwallet (или signrawtransactionwithkey, если ключи вне кошелька).

Что дальше?

Отправляем подписанный hex в сеть командой sendrawtransaction. Если узел видит родительскую T1 в мемпуле, он примет и ретранслирует child-транзакцию T2.

Обратите внимание! При ручном составлении нужно правильно указать флаг replaceable (sequence < 0xFFFFFFFE) для входа, если планируется использовать RBF, но для просто цепочки из неподтверждённых это не обязательно.

(Опять же - поясню: RBF (Replace-By-Fee) - механизм в Биткоине, позволяющий заменить уже отправленную, но ещё не подтверждённую транзакцию новой версией с более высокой комиссией, чтобы ускорить включение в блок).

Также, если родительская транзакция неизвестна узлу (не в мемпуле и не в блокчейне), sendrawtransaction вернёт ошибку bad-txns-inputs-missingorspent - тогда сначала нужно обеспечить присутствие родителя (например, отправив его hex или дождавшись, когда он распространится).

Что ещё важно учесть? Конечно же - настройки и флаги, влияющие на создание цепочек:

  • spendzeroconfchange – флаг конфигурации Bitcoin Core, разрешающий или запрещающий тратить неподтверждённую сдачу. По умолчанию spendzeroconfchange=1, то есть кошелёк тратит свою неподтверждённую сдачу. Если установить 0, кошелёк будет ждать подтверждения сдачи прежде чем пустить её в расход (вообще-то это более грамотный и безопасный подход).
  • walletrejectlongchains – настройка кошелька, препятствующая созданию слишком длинных цепочек. По умолчанию кошелёк откажется создавать новую транзакцию, если она образует цепочку из >25 неподтверждённых транзакций, предвидя, что сеть всё равно её не пропустит. В таком случае RPC вернёт ошибку (например, Insufficient funds или too-long-mempool-chain). Этот лимит можно отключить (поставить 0), но тогда есть риск создать транзакцию, которую ваш именно узел примет, а остальная сеть - проигнорирует.
  • testmempoolaccept – RPC, позволяющий протестировать, примет ли ваш узел транзакцию (или пакет транзакций) в мемпул. Начиная с Bitcoin Core 22.0, он поддерживает пакетную проверку нескольких связанных транзакций сразу. Это бывает полезно, если вы сформировали child-транзакцию до отправки в сеть, потому что можно одним вызовом проверить parent и child как пакет на соответствие политикам mempool.

Итого, создание цепочки parent → child не требует специальных опций – достаточно сослаться на неподтверждённый UTXO.

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

Ограничения и риски при трате неподтверждённых выходов

Лимит глубины цепочки неподтверждённых транзакций

Собственно - выше о нём уже написал, но давайте чуть подробней.

В сети Bitcoin действует стандартное ограничение на длину последовательности зависимых транзакций. Bitcoin Core-узлы по умолчанию не принимают в мемпул транзакцию, если у неё более 25 “предков” (unconfirmed ancestors) или более 25 “потомков” (unconfirmed descendants).

Другими словами, цепочка не может быть длиннее 25 транзакций подряд. Этот параметр задаётся настройками -limitancestorcount и -limitdescendantcount. Остальное - в зоне экстремальных экспериментов.

Значения по умолчанию – 25 для каждого (включая саму транзакцию). Если попробовать отправить 26-ю транзакцию, ссылаясь на выход, у которого уже 25 уровней неподтверждённых предков, узлы вернут ошибку too-long-mempool-chain.

Пример:

  • T1 – родительская транзакция без подтверждений;
  • T2 тратит выход T1;
  • T3 тратит выход T2 и т.д.
  • Вот здесь максимальная глубина такой цепочки ограничена 25 транзакциями.

Кроме количества, накладываются ограничения на общий размер и “массу” (vsize) цепочки. По умолчанию суммарный объём всех транзакций-предков или всех потомков не должен превышать ~101 000 виртуальных байт (≈100 кB). Это задаётся опциями -limitancestorsize и -limitdescendantsize (по умолчанию 101 кB каждая).

Таким образом, если даже 25 транзакций очень велики (например, каждая близка к максимальному размеру), цепь может не пройти по лимиту размера, хотя не превысила бы количество.

Почему введены подобные ограничения?

Главная причина - защита от DoS-нагрузки на узлы. Длинные цепочки транзакций требуют больше ресурсов для хранения и проверки зависимостей в мемпуле. Собственно, напомню, что базово биткоин - это именно антиспам внутри чейна.

Лимиты на 25 взаимосвязанных транзакций были добавлены ещё в Bitcoin Core v0.12 (2015 год), т.е. 10 лет назад, в качестве меры предосторожности. Это порог политики, а не консенсуса: майнер или узел волен увеличить его локально, но тогда его транзакции не будут ретранслироваться большинством узлов с дефолтными настройками.

Например, можно запустить узел с -limitancestorcount=50, но остальные (с дефолтными 25) просто не передадут дальше 26-ю транзакцию из цепочки. По этой причине практически все участники сети придерживаются единых значений – 25/25 и 101 kB, чтобы сохранять предсказуемое поведение мемпула.

Важно!

Правило 25 является общепринятым для Bitcoin и поддерживается в Bitcoin Core, btcd и др. реализациях. Но вот форках были изменения: например, Bitcoin Cash Node увеличил лимиты до 50, а Bitcoin SV поднял до 1000 и вовсе планирует убрать ограничение для поддержки бизнес-кейсов с 0-conf. Но в сети Bitcoin такая радикальная отмена опасна: длинные цепочки без ограничений могут стать вектором атаки на узлы.

Ограничения ancestor/descendant

Коротко - поясню:

  • Ancestor (предок) - это любая неподтверждённая транзакция, от которой зависит данная. Например, для T3 предками будут T2 и T1. В контексте мемпула подсчитываются все поколения предков.
  • Descendant (потомок) - любая последующая транзакция, которая тратит выход данной (непосредственно или через цепочку). Например, для T1 потомками являются T2, T3 и т.д..

Лимит 25 применяется и к тому, и к другому направлению: ни одна транзакция в мемпуле не должна иметь >25 предков и не должна иметь >25 потомков. Причём при добавлении новой транзакции проверяется оба условия: как на неё саму (сколько у неё предков), так и на её самого далёкого предка (сколько потомков у корня цепочки станет, если добавить новую). Ещё точнее - это условие должно проверяться.

Таким образом предотвращается как слишком длинная последовательность, так и ситуация, когда один parent разветвляется в множество потомков.

Стандартный же сценарий обычно линеен (каждый последующий тратит сдачу предыдущего), но если у одной неподтверждённой транзакции несколько выходов и каждый потрачен, то все эти дети считаются потомками первого. Лимит 25 распространяется и на такую “широкую” конфигурацию.

Риски длинных цепочек

Допустим, вы сформировали цепочку из многих 0-conf транзакций. С какими проблемами можно столкнуться на практике?

Во-первых, растущие комиссии и блокировка цепочки. Пока транзакции не подтверждены, ситуация в сети может измениться. Если выросли комиссии, а ранние транзакции в вашей цепочке имели низкий fee-rate, они могут надолго застрять.

Беда в том, что подтолкнуть ускорение одной транзакции в середине цепочки недостаточно – майнеру придётся включать всю её цепочку предков. Значит, чтобы ускорить, скажем, 5-ю из 10 транзакций, придётся повысить комиссию и ей, и всем предкам вплоть до первой, иначе они не подтвердятся своевременно. Это крайне неудобно.

Обычно проблему такого рода решают либо ожиданием подтверждения хотя бы части цепочки, либо применением CPFP от самого последнего звена (об этом ниже). Использовать RBF для какой-то одной транзакции тоже не выйдет: замена parent-транзакции через RBF аннулирует все её потомки (их входы пропадут). Таким образом, цепочка неподтверждённых несовместима с RBF - попытка заменить одну из ранних транзакций потребует заново проводить и все последующие.

Опять же - поясню ряд моментов:

CPFP (Child-Pays-For-Parent)- способ ускорить зависшую биткоин-транзакцию, при котором получатель или владелец UTXO создаёт новую транзакцию-“дочку” с высокой комиссией, чтобы майнеру стало выгодно включить и её, и “родителя”.

Как это работает?

  1. Есть неподтверждённая транзакция-родитель с низкой комиссией.
  2. Она создаёт UTXO у получателя.
  3. Получатель тратит этот UTXO в новой транзакции-child и ставит высокую комиссию.
  4. Майнеры видят пакет (parent + child) и включают обе, т.к. суммарная fee привлекательная.

Зачем это нужно?

Случаев несколько, но вот основные:

  • Родитель завис, но отправитель недоступен / не умеет делать RBF;
  • Получатель хочет быстрее получить подтверждение;
  • Применяется это также и при депозитах на биржи, сервисах, Escrow, LN-каналах и т.д.

Что важно знать?

  • Работает, только если родительская транзакция создаёт доступный для траты (spendable) UTXO.
  • Майнеры ориентируются на общую стоимость (fee-rate) пакета, а не только child.
  • Ограничена политиками мемпула - например лимитом 25 транзакций в цепочке.
  • Не требует opt-in, в отличие от RBF (opt-in - механизм, при котором участник добровольно включает определённую функцию, вместо того чтобы она действовала автоматически).

Коротко отличие

  • RBF - отправитель заменяет транзакцию более дорогой.
  • CPFP - получатель платит комиссию через новую транзакцию.

Поехали дальше…

Во-вторых, политика мемпула и выбивание транзакций. Если сеть перегружена и мемпул заполняется до предела, узлы начинают эвикцию (вытеснение: эвикция (eviction) в Биткоине - удаление неподтверждённой транзакции из мемпула узла, обычно из-за нехватки места или слишком низкой комиссии) транзакций с самыми низкими комиссиями. Этот процесс учитывает зависимости: узел смотрит не только на fee конкретной транзакции, но и на средний fee с её потомками(descendant feerate).

Например, у вас первая транзакция очень дешевая, а вторая (её потомок) – дорогая. Узел видит, что за первой тянется выгодный потомок, и может не выкидывать их, потому что в сумме они перспективны для блока. Но если вся цепочка низкооплачиваемая, без ярко выраженного дорогого потомка, узел при очистке удалит родителя и вместе с ним автоматически всех его потомков.

Давайте - для объективности - дам доп. описание вне своей оценки: “если из-за переполнения мемпула удаляется одна из ваших ранних транзакций, вся последующая цепочка перестанет приниматься узлами (с дефолтной политикой) до появления package relay или снижения нагрузки. Проще говоря, выпадение одного звена “обрушивает” всю незакреплённую цепь. Те узлы, что выкинули транзакцию, не будут знать о её потомках, и откажутся принимать их (ведь их входы неизвестны). Вам придётся либо заново рассылать цепочку, либо ждать, пока самая первая транзакция подтвердится уже другим образом. Риск eviction возрастает при длительном отсутствии подтверждений и малом fee. Кстати, по умолчанию транзакции в любом случае удаляются из мемпула через 14 дней (параметр mempoolexpiry=336 часов) – это ещё один сценарий, при котором длинная цепочка может пропасть, если ни один блок её не включил. Поэтому критически важно либо давать достаточную комиссию, либо пользоваться механизмами ускорения”.

Как видите - и здесь мнение схожее. А почему? Потому что такова сложившаяся архитектура, а не чья-то прихоть.

Ограничения по ширине (ancestor limit)

Как сказал выше, не только глубина, но и количество боковых ответвлений ограничено.

Например, вы провели одну неподтверждённую транзакцию, у которой 2 выхода, и потратили оба выхода в двух новых транзакциях. Теперь у первой транзакции два потомка. Каждый из них, в свою очередь, может породить ещё потомков и т.д., но общее число всех потомков первого не должно превышать 25. Если пытаться распараллелить цепочку (например, создать много выходов и быстро их расходовать), - упрётесь в лимит descendant-count. К тому же, такая, широкая, цепь затрудняет управление комиссиями: все дочерние транзакции независимо конкурируют за место в блоке. Часть может подтвердиться, часть – зависнуть.

Поэтому чаще цепочки делают линейными!

Зависимость от комиссий и роль CPFP

Комиссии напрямую влияют на судьбу неподтверждённой цепочки. Майнеры при выборе транзакций в блок опираются на отношение комиссии к весу (feerate). Однако для цепочек действует правило: нельзя включить потомка без его предка.

Поэтому реализован механизм child-pays-for-parent: майнеры оценивают пакет целиком. Они вычисляют совокупную плату и совокупный вес родителя + всех его потомков, которые добавлены в блок, и на основе этого считают средний fee-rate пакета.

Если, например, parent имеет очень низкую комиссию, но child повышает общую сумму комиссии, то в паре они могут стать выгодными (об этом уже написал выше, но случай важный, поэтому лучше акцентирую внимание ещё раз).

Высокооплачиваемый потомок как бы подтягивает дешёвого предка – именно на этом основан CPFP. Если потомок платит достаточно, то и parent, и child вероятно попадут вместе (в один блок). В противном случае майнер может поступить иначе: допустим, у parent fee высокий, а child низкий – тогда майнеру выгодно включить parent, не включая child (потомок останется ждать или будет выброшен).

Таким образом, чтобы всю цепочку включили разом, обычно каждая следующая транзакция не должна иметь fee-rate ниже, чем у предков. Лучше даже повышать его – так увеличивается descendant feerate и узлы охотнее держат цепочку в мемпуле, а майнеры, видя высокий ancestor feerate, берут весь пакет.

На практике это означает следующее: если вы вынуждены строить цепочку 0-conf, старайтесь на каждом шаге не занижать комиссию относительно предыдущих звеньев.

Если же какая-то транзакция в середине цепи застряла по причине низкого fee, существует стратегия: создать для неё потомка с очень высокой комиссией (CPFP). Да, именно так!

Допустим, транзакция A не подтверждается. Вы берёте один из её выходов (например, сдачу) и тратите его транзакцией B с очень большим fee-rate. Транзакция B пойдёт в мемпул только вместе с A (ведь вход B невалиден без A). Но благодаря огромной комиссии B, пара A+B будет рассматриваться майнерами как одна группа с повышенным средним тарифом.

Это мотивирует майнера включить обе. CPFP - полезный метод, когда не можете заменить транзакцию A через RBF (например, это чужой перевод в ваш адрес, либо вы не устанавливали флаг replaceable).

Важно заметить вот ещё что: CPFP эффективно “проталкивает” всю цепочку предков, но для успеха суммарная плата должна быть конкурентной с остальными транзакциями.

Вероятность выпадения из мемпула

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

Ещё раз напомню, т.к. это крайне важно: стандартный узел хранит транзакцию максимум 2 недели без подтверждения.

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

В целом, чем дольше цепочка остаётся неподтверждённой, тем выше риск, что какая-то её часть потеряется и придётся вручную восстанавливать транзакции.

Поэтому рекомендуется либо регулярно ребродкастить (повторно отправить (расшарить) уже созданную, но ещё не подтверждённую транзакцию другим узлам сети) важные транзакции, либо применять RBF/CPFP до того, как они “протухнут” в памяти узлов.

Отдельно считаю нужно упомянуть исключение из правил 25 – так называемый CPFP carve-out. Поясню:

Bitcoin Core с версии 0.19 ввёл особое послабление для двухуровневых цепочек: если транзакция имеет только одного неподтверждённого предка, то ей разрешается добавить одного потомка сверх лимитов.

Эта политика специально сделана для сценариев типа Lightning, где обе стороны канала должны иметь возможность добавить по одной транзакции с комиссией.

Carve-out позволяет иметь 26 транзакций в цепочке (25 обычных + 1 “особый” потомок на каждого родителя), но только при условии, что ни у родителя, ни у потомка нет других зависимых транзакций.

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

Поведение узлов и майнеров при работе с зависимыми транзакциями

Управление зависимостями в мемпуле

Узлы Bitcoin Core хранят невключённые транзакции в памяти (мемпуле) и отслеживают связь между ними. Если узел получает транзакцию, ссылающуюся на выход, которого у него нет (т.е. родитель неизвестен и не в мемпуле), такая транзакция считается orphan (осиротевшей, если буквально).

Орфан-транзакции не помещаются в основной мемпул, а хранятся отдельно во временном orphan-пуле.

Орфан-пул ограничен по размеру (по умолчанию порядка 100 транзакций) и служит для случаев, когда транзакции приходят не по порядку. Узел может запросить у пира недостающего родителя - по его хэшу. Если родитель вскоре не приходит и лимит пула превышен, самые старые orphans отбрасываются.

Благодаря протоколу реле, в реальности ситуации, когда child приходит раньше parent, редки – начиная с версии 0.13.0 транзакции рассылаются батчами в порядке зависимости, что резко сократило объём orphan-пулов.

Обычно сначала узлы узнают о родительской транзакции, потом о дочерней, поэтому обе попадают в мемпул корректно.

Когда обе транзакции (parent и child) находятся в мемпуле узла, он знает об их зависимости. Bitcoin Core вычисляет для каждой транзакции списки ее ancestors и descendants, а также суммарные метрики: общий вес потомков, общий вес предков, суммарную комиссию потомков и предков и т.д.

Эти данные хранятся как метаданные мемпула и обновляются при каждом добавлении или удалении транзакции. Благодаря этому узел быстро определяет, не нарушит ли добавление новой транзакции ограничения (25/101kb).

Если нарушит – транзакцию не примут в мемпул и не будут ретранслировать (вы получите ошибку). Зависимости также влияют на очистку мемпула: как отмечалось, при эвикции узел смотрит на средний descendant fee, чтобы решить, удалять транзакцию или нет.

Если удаляет – заодно вычищает и всех ее потомков, поскольку они уже “висят в воздухе” без предка.

В итоге, мемпул каждого узла всегда поддерживает валидный набор: либо транзакция и все её предки присутствуют, либо если какого-то предка нет (выпал/не пришёл) – потомка тоже не будет.

Как майнеры выбирают цепочки при построении блока?

Майнер (или, точнее, алгоритм составления блока в полном узле с функцией майнинга) оперирует тем же мемпулом и информацией о зависимостях. Блок нельзя сгенерировать с транзакцией, входы которой не созданы ранее в том же блоке или предыдущих блоках.

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

В Bitcoin Core реализация майнинга сортирует транзакции по эффективности (fee per weight), учитывая при этом ancestor feerate. Это означает, что если транзакция имеет неподтверждённых предков, для оценки её приоритета рассматривается средний fee-rate пакета: она + её предки.

Майнер сравнивает, выгоднее ли включить весь пакет целиком вместо других кандидатов. Например:

  • Если у цепочки parent+child суммарный средний fee-rate высок (child поднял его), она может быть взята полностью. Child с высоким fee “протягивает” родителя – классический случай CPFP, который рассмотрен выше.
  • Если же у потомка комиссия низкая, он ухудшает средний показатель относительно одного родителя. В таком случае майнер может включить только родительскую транзакцию, а child пропустить. Поскольку parent уже подтвердится, потомок потеряет своего предка в мемпуле и, скорее всего, будет выкинут, если никто не применит к нему ещё более сильный CPFP.

Алгоритм упрощённо можно представить так: узел-майнер берёт из мемпула транзакции с максимальным modified fee rate – это либо одиночные транзакции без детей, либо целые семейства, где учтены комиссии потомков.

Затем он добавляет их в блок, причём если добавление какого-то кандидата требует сначала добавить его предков – предки тоже включаются (даже если у них лично низкая комиссия, они “оплачены” потомками).

В результате правильно составленный блок может включать целую цепочку связанных транзакций. Часто так и бывает: если вы сделали цепочку из 2–3 транзакций с повышающимся fee, велика вероятность, что майнер поместит их все вместе в один блок, чтобы собрать суммарную комиссию пакета.

Стоит упомянуть, что разные реализации узлов могут иметь нюансы в подборе транзакций, но большинство (Bitcoin Core, btcd и др.) придерживаются схожей логики, оптимизированной под совокупную выгоду. Таким образом, для майнера цепочка транзакций – пакет, оцениваемый целиком.

Майнер не будет включать потомка без родителя, и наоборот, может включить “убыточного” родителя только если потомок перекрывает его низкую комиссию.

Различия между реализациями узлов

Давайте ещё раз: поскольку правила про неподтверждённые цепочки- политика, а не жёсткий консенсус, разные узлы теоретически могут иметь разные настройки. Ключевое слово - теоретически!

Однако на практике Bitcoin Core задаёт стандарт, и отклонения редки. Узел btcd (альтернативная реализация на Go) по умолчанию совместим с Core-политиками, включая лимит 25.

Если бы btcd или иной клиент решил принять, скажем, 30 звеньев в цепи, это привело бы к тому, что последние 5 транзакций он не сможет разослать большинству пиров – толку от этого мало. Поэтому даже нестандартные узлы обычно сохраняют тот же лимит по умолчанию.

Основные расхождения могут быть в поддержке RBF или деталях пакетной реле – например, не все реализации сразу внедряли поддержку package relay (пакетной отправки транзакций) или правила CPFP-carve-out. Но эти моменты тонкие и, как правило, не мешают базовой совместимости.

В контексте майнинга почти все крупные майнеры используют Bitcoin Core или его производные для сборки блоков, поэтому алгоритм выбора транзакций де-факто одинаков. Различия можно увидеть в форках: так, Bitcoin Unlimited (клиент BCH) экспериментировал с снятием ограничений на unconfirmed chain и реализовал собственный механизм ретрансляции длинных цепочек между узлами. Но это относится к другому протоколу (BCH).

В сети же Bitcoin участники стараются придерживаться единых ограничений ради стабильности сети. Если какой-то узел будет слишком вольно обращаться с неподтверждёнными транзакциями, результатом станет либо изоляция его мемпула, либо даже потенциальные уязвимости (например, для двойных трат).

Поэтому различий между Bitcoin Core, btcd и другими в этом вопросе минимально мало – все соблюдают лимиты ~25/101kb и алгоритмы CPFP/RBF, установленные Core-разработчиками.

Примеры практического применения цепочек 0-conf

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

Child-Pays-for-Parent (CPFP) для ускорения транзакций

CPFP – инструмент, позволяющий повысить шансы подтверждения “застрявшей” транзакции. Предположим, вы отправили платёж с слишком низкой комиссией, и он долго не входит в блок. Если вы контролируете один из выходов этой транзакции (например, сдачу на свой адрес), вы можете создать child-транзакцию с расходованием этого выхода и установить очень высокую комиссию для неё.

Новый transaction оплатит своего родителя, поскольку майнер увидит их как комплект: родитель с низким fee + ребёнок с высоким fee. Средний fee-rate двух транзакций станет достаточным, и оба включатся в блок.

CPFP особенно полезен, когда исходная транзакция не помечена как RBF (заменить её напрямую нельзя).

Многие кошельки позволяют вручную сделать CPFP: для этого находят TXID неподтверждённой транзакции, берут её выход и отправляют его (например, сами себе) с требуемой комиссией.

В Bitcoin Core кошельке автоматического бампа через CPFP нет, но пользователь может выполнить sendtoaddress, указав адрес и сумму так, чтобы кошелёк выбрал неподтверждённую сдачу. Главное – убедиться, что spendzeroconfchange включён.

Кстати, некоторые сторонние кошельки прямо предлагают функцию CPFP (называемую иногда сhild pays for parent или accelerate transaction), но по сути они делают то же: генерируют транзакцию, тратящую выход застрявшего платежа.

Lightning Network и anchor outputs

В LN микроплатежи происходят вне блокчейна (оффчейн), однако для открытия и закрытия каналов используются onchain транзакции. Особенность тут такова, что транзакция закрытия канала (commitment transaction) может быть подписана задолго до её фактического выхода в сеть.

Например, вы и партнёр открыли LN-канал, и у вас зафиксирована commit-транзакция, которая будет использована, если канал закрывается односторонне. За время жизни канала fee в сети могут сильно измениться, и заранее указанная комиссия commit-транзакции может оказаться слишком мала. Если такой коммит попадёт в мемпул с недостаточной комиссией, его могут не подтвердить до истечения важных временных блокировок (что чревато потерей средств).

Решение этой проблемы – как раз и кроется в anchor outputs (якорные выходы) в транзакциях коммита LN.

Anchor output – небольшой специальный выход (например, 330 сатоши) в транзакции, который любой из сторон канала может потратить. Он сконструирован так, чтобы его расходование требовало минимального времени (через OP_CHECKSEQUENCEVERIFY=1, т.е. всего 1 блок задержки) и было не ограничено по размеру комиссии.

Смысл в том, что если commit-транзакция имеет слишком низкий fee, сторона может создать child-транзакцию, тратящую anchor output и добавляющую комиссию (по типу CPFP). Этот child привяжется к коммиту и повысит общий fee-rate пакета, заставив майнеров включить их.

Политика Bitcoin Core специально поддерживает этот сценарий через CPFP carve-out – даже если commit-транзакция уже достигла лимита потомков или имеет минимальную комиссию, узел разрешит к ней присоединить одного потомка (якорь по сути) вне обычных лимитов. (Поясню: в этом контексте carve-out - исключение из мемпул-политики Bitcoin Core, созданное специально для CPFP: обычно действует лимит: максимум 25 потомков/предков в одной зависимости транзакций, но для каналов Lightning (commitment tx) это мешало делать CPFP-ускорение, поэтому Bitcoin Core ввёл carve-out - буквально “вырезание из правила”).

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

Применение anchor outputs стало de facto стандартом (опция option_anchors) в современных реализациях Lightning (c 2020–2021 годов). По умолчанию новые каналы открываются с якорями, а старый механизм (commitment с фиксированной комиссией или RBF-try) постепенно уходит.

Таким способом LN обеспечивает безопасность на случай переполненного мемпула: даже если базовая транзакция имеет крайне низкий fee, её можно будет довести до майнеров посредством CPFP.

Отмечу вот ещё что: до внедрения пакетного ретрансля (package relay) остаётся момент: commit-транзакция с fee ниже минимального relay-фи (1 сат/вБ) может вовсе не попадать в мемпул узлов.

Поэтому текущие реализации LN стараются хотя бы минимально достаточную комиссию назначить коммитам, чтобы они не были полностью отвергнуты сетью.

В будущем же предполагается поддержка package relay, позволяющая релейтить связку commit+anchor сразу, даже если по отдельности commit не проходит минимальный порог.

Платёжные каналы и микроплатежи на основе 0-conf

Ещё до развития Lightning предлагались схемы микроплатежей на основе цепочек транзакций.

Классический пример – односторонний платежный канал:

  • Алисa посылает Бобу транзакцию с блокировкой (например, 2-of-2 мультиподписью), после чего они обмениваются последовательностью подписанных транзакций, расходующих эту “закреплённую” монету с увеличивающимся балансом Боба.
  • В итоге последняя транзакция транслируется и закрывает канал.
  • В такой схеме промежуточные состояния существуют как подписанные, но не транслируются, чтобы не конфликтовать друг с другом. Это значит, что в мемпуле не висит цепочка – по сути, либо канал закрыт, либо ещё нет.
  • Поэтому прямого создания длинной цепочки нет (что и хорошо). Но в любой момент одна сторона могла прервать канал и опубликовать последнюю транзакцию.

Что важно в рамках статьи: были приложения, пытавшиеся реализовать микроплатежи без ожидания подтверждений, именно последовательно тратя неподтверждённые UTXO.

Например, некоторые сервисы мгновенных ставок или онлайн-игры принимали платежи 0-conf и сразу же отправляли выигрыш в новой транзакции, не дожидаясь подтверждения депозита. Это и порождало цепочку: депозит пользователя (не подтверждённый), затем сразу вывод выигрыша.

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

Помимо этого, риск 0-conf (двойных трат) делал такие решения уязвимыми: злоумышленник мог попытаться одновременно разослать double-spend транзакции, чтобы обмануть систему, принимающую платеж без подтверждения. (Опять же - поясню: 0-conf (zero-confirmation) - транзакция, которая ещё не вошла в блок, но уже появилась в сети (мемпуле) и видна узлам).

По этим причинам прямые on-chain микроплатежи с доверием к 0-conf сегодня используются редко. Вместо них перешли на Lightning Network, где микроплатежи происходят off-chain, либо на батчинг (см. ниже).

Instant channel (мгновенные каналы) в Lightning

Некоторые инновации (например, Turbo Channels) позволяют начать использовать LN-канал до подтверждения транзакции открытия. Это достигается договорённостью: если одна сторона сразу пушит баланс другой стороне в новом канале, та может тратить его внутри Lightning, хотя funding-транзакция ещё не подтверждена.

Такая схема несколько рискованна и требует доверия к тому, кто открывает канал (или страховки фиатом, как в этом примере). По сути тут тоже образуется ситуация, что off-chain средства используются до on-chain подтверждения.

В случае непрохождения транзакции открытия, канал аннулируется; потери компенсируются за счёт депозитов (в описанном примере – Боб просто возвращает долг Алисе вне сети).

Чтобы минимизировать риск, в протоколе предлагается сделать funding-транзакцию non-RBF и с двумя выходами (один – возврат средств открывающему канал, второй – сам канал). Открывающий сможет применить CPFP к возвратному выходу, если транзакция подвиснет, тем самым протолкнув и сам канал.

Хотя такие instant channels ещё не стали массовыми, этот пример показывает на мой взгляд, как комбинация цепочек 0-conf и механизмов fee-bump может расширить возможности Lightning.

Экосистемы с регулярным использованием unconfirmed chaining

Если обобщить, то будет понятно, что в основном сюда относятся решения второго уровня типа Lightning, где без коротких 0-conf цепочек не обойтись (anchor outputs).

Но также некоторые биржи и платежные процессоры могут кредитовать средства пользователям по первому неподтверждённому поступлению (условно “показывая” баланс до подтверждения) – далее пользователь может делать транзакции. Если в итоге депозит не подтвердится (двойная трата), сервис отменит последующие операции. Это рискованная практика, поэтому крупные игроки требуют подтверждений.

Исторически, высокочастотные приложения (например, азартные игры на раннем этапе Bitcoin) активно использовали 0-conf транзакции. Известно, что популярная игра Satoshi Dice до введения строгих правил успевала отправлять выигрыши раньше, чем ставка подтверждалась, образуя цепочку. После ужесточения политики (лимита 25 и распространения RBF) такие сервисы либо изменили логику, либо мигрировали на альткоины с другими параметрами.

В сообществе Bitcoin Cash, напротив, стремление поддерживать как можно больше 0-conf операций привело к увеличению лимитов и разработке специфических узлов (Bitcoin Unlimited) для ретрансляции длинных цепочек. Но я к этому форку отношусь холодно и особо в архитектуру не вникаю. Да и вам не советую.

Можно сказать, что в Bitcoin-сети постоянное интенсивное использование unconfirmed chaining – редкость из-за упомянутых ограничений. Большинство сценариев либо переходят на off-chain (LN) для множества быстрых платежей, либо агрегируют платежи.

Тем не менее, понимание механизма расходования неподтверждённых UTXO важно даже для рядовых пользователей – ведь каждый раз, когда вы отправляете следующие транзакции не дождавшись подтверждения предыдущих, вы вступаете в эту игру с мемпулом и майнерами.

Практические рекомендации по использованию неподтверждённых UTXO

Пока опишу их немного, т.к., возможно, сделаю 2-ю часть.

Первое. Минимизируйте длину цепочек, когда это возможно

Старайтесь не генерировать длинных последовательностей транзакций без подтверждения. Лимит 25 - это не так уж мало: 25 шагов, после которых придётся ждать блок. В большинстве случаев бизнес-логики можно обойтись куда меньшим. Если у вас серия платежей, рассмотрите возможность объединения их в один батч.

Вместо десяти последовательных переводов, например, сделайте один с десятью выходами. Батчинг существенно эффективнее: вы платите одну комиссию за десяток выплат, уменьшаете нагрузку на блокчейн и не рискуете застрять из-за unconfirmed chain. (К этому же я пришёл и в EVM-сетях).

Как отмечают разработчики: “batching не только экономит комиссии, но и упрощает управление UTXO, снижает количество “сдачи” и позволяет при необходимости применять RBF/CPFP к всей батч-транзакции, если она задержалась”. В отличие от цепочки, у батча нет проблемы ломания последовательности, т.е. вы всегда можете заменить или ускорить единственную транзакцию.

Второе. Избегайте одновременной зависимости от RBF и CPFP

Если планируете возможность Replace-By-Fee, лучше не строить на этой же транзакции цепочку потомков. Как описал выше, включение RBF-флага (что сейчас делается по умолчанию для большинства кошельков) конфликтует с моделью CPFP-цепочки.

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

Некоторые кошельки (включая Bitcoin Core) могут автоматически отказывать тратить неподтверждённую сдачу, помеченную RBF, поскольку потенциально последующий CPFP-ребёнок и так будет конфликтовать при замене.

В общем случае: или RBF, или CPFP. Если исходная транзакция - ваша и вы можете её заменить, делайте RBF (так надёжнее). Если не можете (или не хотите) - тогда CPFP, но без RBF.

Третье. Следите за комиссиями с самого начала

Лучше предотвратить проблему, чем потом городить цепочку для её решения. Если транзакция чувствительна ко времени - поставьте адекватную комиссию сразу. Не надейтесь прокатить 1 sat/vB в часы пик с расчётом “а потом догоню CPFP”, потому что это может быть слишком поздно (для вас).

Если уж так вышло, что fee низкий, не медлите с CPFP. Добавьте child с большим fee раньше, чем транзакция выпадет из мемпула. Также стоит знать: если ваш parent уже выпал у части узлов, а вы пытаетесь транслировать child, ничего не выйдет – придётся сначала восстановить parent.

В отсутствие package relay вам, возможно, придётся связаться напрямую с майнерами или дождаться, пока mempool рассосётся (на практике - это, скорее, исключение). Поэтому действуйте проактивно: видите, что транзакция не проходит по скорости – либо RBF (если можно), либо создавайте “ребёнка” с доплатой.

Четвёртое. Не превышайте политики сети (лимиты по числу и размеру)

Настраивать у себя -limitancestorcount >25 имеет смысл разве что для частных случаев (например, тестирование). В обычной работе вам это не поможет – сеть не поддержит.

То же касается размера: огромные цепочки даже до 25, но весом по 100 kB каждая транзакция, на практике не встречаются.

Помните, что стандартные узлы отвергнут транзакцию, нарушающую лимиты 25/101kb, даже если ваш узел её создал. Особенно актуально это для сервисов: если вы программируете кошелёк, который автоматически тратит всё новые и новые неподтверждённые сдачи, внедрите счётчик “глубины цепочки” и ограничьте его.

Иначе пользователи столкнутся с неожиданными ошибками.

Например, известна проблема: пользователь пытается отправить монеты, а кошелёк выдаёт “Insufficient funds”, хотя баланс есть: это происходит, когда кошелёк уже набрал максимальную цепочку изменений и не может их продолжать тратить.

Пятое. Диверсифицируйте UTXO для параллельных отправлений

Да, я снова про диверсификацию :).

Если вам всё же нужно выполнить много транзакций в короткий срок (например, раздать 100 выплат), не делайте их цепочкой друг за другом с одного выхода. Лучше использовать несколько независимых входов.

Допустим, у вас есть 5 UTXO – вы можете одновременно инициировать 5 “цепочек” по 5 транзакций, и ни одна не упрётся в лимит, потому что они не связаны одним предком. Конечно, они и в мемпуле будут независимы.

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

Если же у вас один большой UTXO и нужно много переводов, рассмотрите batching или чередование с паузой на подтверждение.

Шестое. Используйте Lightning Network для частых микроплатежей

Если задача – множество мелких оплат в реальном времени, on-chain транзакции не подходят из-за ограничений. Lightning решает эту проблему: внутри канала вы можете совершать тысячи переводов без участия блокчейна. В итоге лишь открытие и закрытие канала будут on-chain (и то обычно 1-2 транзакции).

LN по сути-то специально разработана, чтобы снять нагрузку микроплатежей с основной сети, и сценарии, требующие 0-conf транзакций на каждый платёж, в идеале должны мигрировать на LN или схожие решения. Это и надёжнее (нет риска двойных трат для каждой оплаты, пока канал безопасен), и дешевле для массовых операций.

Седьмое. Будьте осторожны с доверием к 0-conf

По возможности, не считайте неподтверждённые поступления окончательными. Если вы продавец, лучше дождитесь 1 подтверждения (а я на деле всегда жду 6), прежде чем отгружать товар – иначе любая цепочка может в один момент аннулироваться (достаточно пропажи или блока с double-spend).

Если приходится принимать 0-conf (например, для очень быстрого сервиса, где ставка невелика) – ограничьте суммы, реализуйте мониторинг на двойную трату и понимайте, что цепочки увеличивают вероятность проблем.

В частности, длинная цепочка 0-conf - это цепочка возможностей для атакующего: он может попробовать провести double-spend на любом этапе.

При RBF в сети сейчас принято, что если транзакция не помечена replaceable, большинство узлов вторую конфликтующую не возьмёт. Но при достаточно длинной цепочке атаки с перекрытием возможны.

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

Восемь. Рассчитывайте на худшее (fail-safe)

Если у вас критичная цепочка (например, закрытие Lightning-канала с крупной суммой), всегда имейте план, что делать, если она не подтвердится.

В случае LN, протокол предусматривает штрафные транзакции, watchtower и пр. – это отдельная тема, но суть в том, что разработчики уровня L2 тщательно прорабатывают ситуацию подвисших транзакций.

В своём приложении вы тоже должны: например, если через X времени нет подтверждения, пробовать CPFP; если транзакция выпала – пытаться переслать снова (либо, наоборот, отказаться от операции).

Мемпул – ненадёжная область, транзакция либо войдёт в блок, либо может исчезнуть.

А значит? Никогда не предполагайте, что неподтверждённая транзакция гарантированно будет вечно “лететь” и в итоге дойдёт – закладывайте таймауты и возможность повторной отправки.

Вместо заключения

тратить неподтверждённые UTXO можно, и это порой весьма полезно (например, для ускорения комиссией или повышения удобства пользователя). Но делать это нужно понимая ограничивающие факторы. Bitcoin-сеть настроена так, чтобы 0-conf транзакции были возможны, но не слишком много и не слишком долго.

Следуя приведённым рекомендациям, сможете пользоваться этой фичей, не рискуя потерять транзакции и не создавая узким мест в работе вашего приложения. Как образно отметил один интересный чувак: “длинные цепочки – начинают ломаться на пределах политики, тогда как альтернативные подходы (батчи) просто становятся эффективнее при большем объёме”. Вот и всё :).

Пользуйтесь преимуществами Bitcoin разумно и учитывайте его внутреннюю механику при проектировании ваших транзакций.

А продолжу, но позже -

До!