Перейти к основному содержимому
Блокчейн61ГЛАВА 61Путь транзакции: почему «успешно» не значит «записано»proposalendorseordervalidatestatus 200 ≠ записано

Путь транзакции: почему «успешно» не значит «записано»

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

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

Есть одна ошибка, на которой спотыкаются все, кто пришёл в 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 устроен именно так и откуда берётся его скорость.

Четыре стадии​

ЧЕТЫРЕ СТАДИИ: ИСПОЛНЕНИЕ ИДЁТ ДО ОЧЕРЕДИ, А НЕ ПОСЛЕ1Предложениеклиент → пирывызов уходит каждому пиру, чья подпись нужна2Подтверждениепиры исполняюткаждый считает у себя и подписывает набор изменений3Упорядочиваниеслужба очередисделки строятся в ряд и режутся на блоки4Проверка и записькаждый пирполитика и конфликты; негодное помечается недействительным
Исполнение идёт ДО очереди — в этом главное отличие от Ethereum

В 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…"}]
ПИР ПОДПИСЫВАЕТ НЕ РЕЗУЛЬТАТ, А НАБОР ИЗМЕНЕНИЙПрочитаноkey: z-1version: блок 14, сделка 0не значение, а его номер версииЗаписаноkey: z-1value: {...три оценки...}готовое новое значениена четвёртой стадии пир сверяет: версия прочитанного ключа всё ещё та же?совпала — запись применяется; не совпала — сделка помечается недействительнойпоэтому вызовы можно исполнять заранее и параллельно: порядок проверяется потом
В наборе чтения — номер версии, а не значение; в наборе записи — готовое значение

Обратите внимание на несимметричность:

  • в наборе чтения записано не само значение, а его номер версии: «ключ z-1, версия из блока 14, сделка 0»;
  • в наборе записи — готовое новое значение целиком.

Версия — это адрес места, откуда значение взяли. На четвёртой стадии пир сверяет: версия прочитанного ключа всё ещё та же? Если да — запись применяется. Если ключ успели поменять — сделка недействительна.

Интересный факт

Отсюда берётся скорость Fabric. Вызовы можно исполнять заранее, параллельно и на разных машинах: никто не ждёт своей очереди. Порядок проверяется потом, дешёвым сравнением номеров версий.

Отвечено вопросов: 0 из 3
Чем порядок стадий в Fabric отличается от Ethereum?
Что лежит в наборе чтения?
Откуда берётся скорость Fabric?
Отвечено верно: 0 из 3

Первая причина отказа: не хватило подписей​

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

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]
СДЕЛКА В БЛОКЕ ЕСТЬ, А ИЗМЕНЕНИЯ НЕТENDORSEMENT_POLICY_FAILUREподписей меньше, чем требует политикаклиент при этом получил status:200 — ему ответил единственный опрошенный пирMVCC_READ_CONFLICTключ успели изменить между исполнением и записьюдве одновременные правки одного ключа: первая по порядку побеждает, вторая отпадает«успешный» ответ клиенту не значит «записано»: проверять надо состояние, а не код ответа
Две причины, по которым сделка лежит в блоке недействительной

Что произошло. Оба пира исполнили обе сделки, пока в состоянии лежала одна и та же версия asset1. В наборе чтения у обеих оказался один и тот же номер версии. Потом упорядочиватель поставил их в ряд внутри одного блока. Первая применилась и сменила версию ключа. Для второй ожидаемая версия уже не совпала — отказ.

Важно

Название MVCC_READ_CONFLICT пришло из баз данных: так называется управление одновременным доступом через версии. Механизм тот же самый, что в PostgreSQL, только версия хранится не в строке таблицы, а в наборе чтения транзакции.

Что с этим делать на практике. Это не ошибка, а нормальное поведение при состязании за один ключ. Лечится тремя способами:

  • повторить вызов — самый частый, потому что при повторе будет уже новая версия;
  • разнести данные по ключам — не один документ «все оценки группы», а по документу на студента;
  • не читать то, что не нужно — лишний getState кладёт лишний ключ в набор чтения и создаёт конфликты на пустом месте.
Отвечено вопросов: 0 из 3
Вызов вернул status:200, а записи нет. Что это значит?
Что означает MVCC_READ_CONFLICT?
Как уменьшить число конфликтов по версии?
Отвечено верно: 0 из 3

Как узнать правду вовремя​

Раз ответ на стадии 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. Ждать отката.

Недействительная сделка остаётся в блоке навсегда. Никто её оттуда не удалит и не «откатит» — это и есть неизменяемость. Повторять нужно новой сделкой.

Отвечено вопросов: 0 из 3
Где искать причину, если вызов сказал «успешно», а записи нет?
Что делает флаг --waitForEvent?
Недействительную сделку из блока можно удалить?
Отвечено верно: 0 из 3

Шпаргалка по главе​

Что нужноКак
Дождаться записидобавить --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 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоHyperledger Fabric – Transaction FlowNPTEL-NOC IITM
Видео7 Steps of a HyperLedger Fabric Blockchain TransactionErnesto
ВидеоHyperledger Fabric Consensus: Transaction Lifecycle Explained | Module 1.4KryptoMindz Technologies
ВидеоMultiversion Concurrency Control (MVCC) Explained in Simple TermsCameron McKenzie
ВидеоFailure and Recovery | Hyperledger Fabric Blockchain | Endorsement | VSCC | MVCC | Ledger FileLearning Blockchain

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.