Путь транзакции: почему «успешно» не значит «записано»
Зачем это нужно
Есть одна ошибка, на которой спотыкаются все, кто пришёл в Fabric из Ethereum. Выглядит она так:
INFO [chaincodeCmd] chaincodeInvokeOrQuery -> Chaincode invoke successful. result: status:200
Успех. Проверяем состояние:
Error: endorsement failure during query. response: status:500 message:"The asset asset10 does not exist"
Записи нет. При этом высота цепочки выросла — сделка попала в блок. Она там лежит, помеченная недействительной.
Разобраться в этом можно только одним способом: пройти весь путь транзакции по стадиям. Заодно станет понятно, почему Fabric устроен именно так и откуда берётся его скорость.
Четыре стадии
В Ethereum порядок такой: транзакция попадает в очередь, потом в блок, и только потом узлы её исполняют. Исполнение идёт после упорядочивания, поэтому все узлы считают одно и то же в одном и том же порядке.
В Fabric наоборот. Сначала пиры исполняют вызов и подписывают результат, и только потом подписанное отправляется в очередь. Такая схема называется «исполнить — упорядочить — проверить».
Стадия 1. Предложение. Клиент отправляет вызов каждому пиру, чья подпись нужна по политике. Это ещё не транзакция, а запрос «посчитай и подпиши».
Стадия 2. Подтверждение. Каждый пир исполняет функцию у себя, в песочнице, ничего не записывая. На выходе — набор изменений и подпись под ним. Клиент собирает ответы и сверяет их побайтово (то самое место, где ловится недетерминизм из главы про чейнкод).
Стадия 3. Упорядочивание. Собранная транзакция уходит упорядочивателю. Он не исполняет код и не смотрит, что там внутри: его дело — поставить сделку в общую очередь и нарезать блоки.
Стадия 4. Проверка и запись. Блок приходит каждому пиру. Пир проверяет две вещи: хватает ли подписей по политике и не изменились ли за это время прочитанные ключи. Что не прошло — помечается недействительным. Блок при этом не меняется: негодная сделка остаётся в нём навсегда, просто с отметкой.
Стадия 4 — ответ на загадку из начала главы. Клиенту ответил пир на стадии 2, а отказ произошёл на стадии 4, когда клиент уже ушёл. Никто ему об этом не сообщит, если он сам не подпишется на событие.
Что именно подписывает пир
Пир подписывает не «новое состояние мира», а набор изменений: что прочитано и что записано. Достанем настоящую сделку из блока.
peer channel fetch newest last.pb -c pgk --tls --cafile "$ORDERER_CA"
configtxlator proto_decode --input last.pb --type common.Block --output last.json
Внутри, в ветке ns_rwset:
прочитано: [{"key":"z-1","version":{"block_num":"14","tx_num":"0"}}]
записано: [{"is_delete":false,"key":"z-1","value":"eyJpZCI6InotMSIsImZpbyI6…"}]
Обратите внимание на несимметричность:
- в наборе чтения записано не само значение, а его номер версии: «ключ
z-1, версия из блока 14, сделка 0»; - в наборе записи — готовое новое значение целиком.
Версия — это адрес места, откуда значение взяли. На четвёртой стадии пир сверяет: версия прочитанного ключа всё ещё та же? Если да — запись применяется. Если ключ успели поменять — сделка недействительна.
Отсюда берётся скорость Fabric. Вызовы можно исполнять заранее, параллельно и на разных машинах: никто не ждёт своей очереди. Порядок проверяется потом, дешёвым сравнением номеров версий.
Первая причина отказа: не хватило подписей
Вернёмся к загадке. Вот вызов, отправленный одному пиру, хотя политика канала требует большинство из двух организаций:
peer chaincode invoke ... -C pgk -n basic \
--peerAddresses localhost:7051 --tlsRootCertFiles "$PEER0_ORG1_CA" \
-c '{"function":"CreateAsset","Args":["asset10","white","3","Oleg","100"]}'
INFO [chaincodeCmd] chaincodeInvokeOrQuery -> Chaincode invoke successful. result: status:200
Единственный опрошенный пир посчитал вызов, ничего плохого не увидел и ответил успехом. Клиент собрал сделку с одной подписью и отправил её в очередь. Дальше — журнал пира:
WARN [validation] preprocessProtoBlock -> Channel [pgk]: Block [7] Transaction index [0]
TxId [a3ab06f2…] marked as invalid by committer. Reason code [ENDORSEMENT_POLICY_FAILURE]
Высота цепочки выросла, состояние не изменилось. Ровно то, с чего начиналась глава.
Практический вывод: status:200 от peer chaincode invoke означает только «пир посчитал и подписал». Проверять надо состояние — или запускать команду с флагом --waitForEvent, тогда клиент дождётся записи блока.
Вторая причина: ключ успели изменить
Вторая причина тоньше. Отправим два изменения одного и того же ключа одновременно:
inv '{"function":"UpdateAsset","Args":["asset1","blue","5","Anna","111"]}' &
inv '{"function":"UpdateAsset","Args":["asset1","blue","5","Boris","222"]}' &
wait
Оба вызова отвечают успехом:
INFO [chaincodeCmd] chaincodeInvokeOrQuery -> Chaincode invoke successful. result: status:200
INFO [chaincodeCmd] chaincodeInvokeOrQuery -> Chaincode invoke successful. result: status:200
Смотрим состояние:
{"AppraisedValue":222,"Color":"blue","ID":"asset1","Owner":"Boris","Size":5}
Прошло только одно изменение. Журнал объясняет, какое именно:
WARN [validation] validateAndPrepareBatch -> Block [8] Transaction index [1]
TxId [f0f17f3c…] marked as invalid by state validator. Reason code [MVCC_READ_CONFLICT]
Что произошло. Оба пира исполнили обе сделки, пока в состоянии лежала одна и та же версия asset1. В наборе чтения у обеих оказался один и тот же номер версии. Потом упорядочиватель поставил их в ряд внутри одного блока. Первая применилась и сменила версию ключа. Для второй ожидаемая версия уже не совпала — отказ.
Название MVCC_READ_CONFLICT пришло из баз данных: так называется управление одновременным доступом через версии. Механизм тот же самый, что в PostgreSQL, только версия хранится не в строке таблицы, а в наборе чтения транзакции.
Что с этим делать на практике. Это не ошибка, а нормальное поведение при состязании за один ключ. Лечится тремя способами:
- повторить вызов — самый частый, потому что при повторе будет уже новая версия;
- разнести данные по ключам — не один документ «все оценки группы», а по документу на студента;
- не читать то, что не нужно — лишний
getStateкладёт лишний ключ в набор чтения и создаёт конфликты на пустом месте.
Как узнать правду вовремя
Раз ответ на стадии 2 ничего не гарантирует, клиенту нужен способ дождаться стадии 4. Их два.
Флаг --waitForEvent у peer chaincode invoke:
peer chaincode invoke ... --waitForEvent -c '{"function":"Zavesti","Args":["z-1","Ковалик Олег","ИС-21"]}'
INFO [chaincodeCmd] ClientWait -> txid [8d102bdb…] committed with status (VALID) at localhost:7051
INFO [chaincodeCmd] chaincodeInvokeOrQuery -> Chaincode invoke successful. result: status:200
Появилась строка committed with status (VALID) — вот это и есть настоящее подтверждение записи.
Подписка на события блока — так делают в приложениях. Библиотека fabric-gateway в современных версиях ждёт записи по умолчанию: вызов submit возвращает управление только после того, как сделка попала в блок и прошла проверку.
В peer chaincode invoke без --waitForEvent возвращается ответ стадии 2 — так исторически сложилось, и менять это не стали ради обратной совместимости. Поэтому в учебных руководствах так часто встречается «команда сказала успех, а ничего не произошло».
Заглянем в блок
Соберём картину целиком — достанем блок с настоящей сделкой:
номер блока: 15 | транзакций: 1
хеш предыдущего: +miykifz3th0/2OROHtXRRV998Z5oF1LeVLh8jDEjwo=
канал: pgk | txId: a23931d66aebd7df98fba10714fed1a8f12f3453ededb0845ed876ee7b0fdf04
подписант MSP: Org1MSP
чейнкод: zachetka 1.0
прочитано: [{"key":"z-1","version":{"block_num":"14","tx_num":"0"}}]
подписей одобрения: 2
Здесь видно всё, о чём была глава: кто отправил (Org1MSP), какой контракт вызвали, что прочитали и с какой версией, сколько подписей собрано. Плюс хеш предыдущего блока — та самая цепочка, ради которой всё затевалось.
Типичные ошибки
Ошибка 1. Считать status:200 подтверждением записи.
Разобрана выше. Лечится --waitForEvent или проверкой состояния.
Ошибка 2. Опрашивать одного пира, когда политика требует двух.
Команда отработает, сделка попадёт в блок недействительной. В логе — ENDORSEMENT_POLICY_FAILURE. Лечится перечислением всех нужных пиров через повторяющийся --peerAddresses.
Ошибка 3. Все записи в один ключ.
Счётчик, общий журнал, «последний номер» — любая такая конструкция превращает параллельную работу в очередь из конфликтов. Разносите по ключам.
Ошибка 4. Искать причину отказа не там.
Клиент видит status:200, поэтому в его логах ничего нет. Причина всегда в журнале пира:
docker logs peer0.org1.example.com 2>&1 | grep "marked as invalid"
Ошибка 5. Ждать отката.
Недействительная сделка остаётся в блоке навсегда. Никто её оттуда не удалит и не «откатит» — это и есть неизменяемость. Повторять нужно новой сделкой.
Шпаргалка по главе
| Что нужно | Как |
|---|---|
| Дождаться записи | добавить --waitForEvent к peer chaincode invoke |
| Собрать подписи двух организаций | повторить --peerAddresses и --tlsRootCertFiles для каждого пира |
| Найти причину отказа | docker logs peer0.org1.example.com 2>&1 | grep "marked as invalid" |
| Посмотреть содержимое блока | peer channel fetch newest b.pb -c pgk + configtxlator proto_decode |
| Высота цепочки | peer channel getinfo -c pgk |
| Уменьшить конфликты | разносить данные по ключам, не читать лишнего |
Словарь главы
| Термин | Что это |
|---|---|
| Предложение | Запрос «посчитай и подпиши», отправленный пирам; ещё не транзакция |
| Подтверждение | Подпись пира под набором изменений |
| Набор чтения | Список прочитанных ключей с номерами их версий |
| Набор записи | Список новых значений, готовых к применению |
ENDORSEMENT_POLICY_FAILURE | Подписей меньше, чем требует политика |
MVCC_READ_CONFLICT | Прочитанный ключ изменился между исполнением и записью |
| Недействительная сделка | Лежит в блоке с отметкой; состояние не меняет и не удаляется |
--waitForEvent | Флаг, заставляющий клиента дождаться записи блока |
Цель: скорость ≥ 90 зн/мин, точность ≥ 88%
peer chaincode invoke -C pgk -n zachetka --waitForEvent
Что должен уметь после главы
- назвать четыре стадии и объяснить, почему исполнение идёт до упорядочивания;
- объяснить, что подписывает пир и почему в наборе чтения лежит номер версии;
- разобрать случай «успешно, но не записано» и найти причину в журнале пира;
- отличить
ENDORSEMENT_POLICY_FAILUREотMVCC_READ_CONFLICTи знать, что делать с каждым; - дождаться настоящего подтверждения записи;
- назвать три способа уменьшить число конфликтов по версии.
Проверка по главе
Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Поток транзакций — все четыре стадии с картинками.
- Чтение, запись и версии — устройство набора изменений и проверка версий.
- Политики подтверждения — как задать их для конкретного контракта.
- Fabric Gateway для приложений — клиентская библиотека, которая ждёт записи сама.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.