Публикация, нормализация, проверка транзакций и их трансляция на узлах ADAMANT
Функции
Transaction.prototype.verify проверяет транзакцию, полученную из других блоков, которая может включать старые транзакции. Transactions.prototype.publish проверяет входящие транзакции от пользователей, подтверждая свежие неподтверждённые транзакции. verify также вызывается после получения новой транзакции через API, после методов publish и objectNormalize. publish вызывается только при отправке транзакций через публичное API.
Обработка транзакций
Когда узел получает новую транзакцию от клиентского приложения через API как первый узел, увидевший её, Logic->Transaction.prototype.publish создаёт новую транзакцию из полученных данных, проверяет свойства транзакции клиента (включая timestampMs), добавляет идентификатор транзакции и обрабатывает неподтверждённую транзакцию обычным образом. Если узел получает новую транзакцию от другого пира на текущей высоте, он проверяет транзакцию и удаляет пир, если она недействительна, затем обрабатывает неподтверждённую транзакцию. При получении транзакции от другого пира при подъёме или проверке блокчейна с высоты 0 узел проверяет транзакцию, удаляет пир при недействительности и обрабатывает неподтверждённую транзакцию, проверяя несколько транзакций одновременно. Проверка включает вызов узлом objectNormalize() для валидации схемы полученной транзакции.
Процесс обработки неподтверждённой транзакции начинается с logic.transaction.process(), который проверяет идентификатор транзакции и нормализует идентификатор отправителя. Далее транзакция нормализуется: logic.transaction.objectNormalize() проверяет объект транзакции по схеме и удаляет ненужные свойства, включая timestampMs до активации spaceship. Наконец, вызывается logic.transaction.verify() для проверки всех свойств, таких как timestamp, timestampMs и signature.
Удаление пира означает исключение его из списка пирам до повторного обнаружения; узлы не блокируют пиры, поэтому статус BANNED никогда не применяется. Узел транслирует транзакцию другим узлам после обработки неподтверждённой транзакции. Помимо objectNormalize(), узел имеет normalize(), который используется только для эндпоинта POST /transactions/normalize, который должен быть объявлен устаревшим. У узла есть и apply(), и applyUnconfirmed(), потому что методы “apply” изменяют балансы счётов отправителя и получателя, тогда как applyUnconfirmed() изменяет неподтверждённый баланс; то же самое относится и к методам “undo”. Когда узел формирует новый блок, транзакции берутся из уже проверенного списка. В настоящее время publish() проверяет, не находится ли временная метка транзакции в будущем или более чем constants.maxTransactionAgeSec секунд в прошлом. Нет смысла добавлять те же проверки в publish, что и в verify, потому что последний должен вызываться после publish. Кроме того, проверки из publish нельзя включить в verify, поскольку они зависят от текущего времени, что делает невозможным проверку, является ли старая транзакция не старше 5 секунд. Клиентское приложение получит тексты ошибок, такие как The difference between timestamp and timestampMs is greater than ${maxTimestampMsDelta}ms. или Invalid transaction timestamp. Timestamp is not in the int32 range, если они находятся не в publish(), а в verify().
Примечания о временных метках
Поле timestamp транзакции — это время эпохи ADAMANT в секундах, а не Unix-время. Поле timestampMs транзакции должно быть временем эпохи ADAMANT в миллисекундах, а не Unix-временем; Unix-миллисекунды можно вычислить как constants.epochTime.getTime() + timestampMs. Поле timestampMs не является частью байтов транзакции, подписей, идентификаторов транзакций или хэшей, даже после активации spaceship. После spaceship консенсусная проверка должна требовать, чтобы timestampMs находился в той же секунде эпохи ADAMANT, что и timestamp: 0 <= timestampMs - timestamp * 1000 < 1000. Это более строгое правило — одна и та же секунда — является преднамеренным: timestampMs должен уточнять ту же самую секунду эпохи ADAMANT, что хранится в timestamp, и клиенты должны вычислять timestamp = Math.floor(timestampMs / 1000). Использование Math.round() или Math.ceil() около границы секунды создаёт несогласованную пару и должно отклоняться. Анализ текущих клиентов и источников показал, что adamant-api-jsclient, PWA, adamant-console (через [email protected]), iOS и примеры в документации все используют Math.floor или эквивалентное усечение, и ни один путь генерации временных меток исходящих транзакций ADM не использует round или ceil. До spaceship консенсусная нормализация должна удалять timestampMs, чтобы поведение до активации оставалось совместимым со старыми узлами. Несколько транзакций от одного отправителя в пределах одной секунды разрешены, если только это разные транзакции; timestampMs помогает клиентам сортировать быстрые чат-сообщения, но не должен становиться nonce или частью подписи.
Проверки на этапе приёма
Метод publish() содержит проверки приёма для публичного API для только что отправленных транзакций, которые могут зависеть от текущих часов узла. Если транзакция проходит publish(), она всё равно обрабатывается verify(), который намеренно не имеет проверок по текущему времени и должен сохранять только детерминированные проверки, такие как схема, идентификатор, подписи, балансы и правило совпадения секунды для timestampMs. Проверки по текущему времени из publish() не должны переноситься в verify(), потому что verify() используется для повторного воспроизведения, синхронизации и исторических транзакций. Существующая проверка на будущее — слотовая: при 5-секундных слотах клиент с часами, опережающими примерно на 400 мс, вызывает Transaction timestamp is in the future только тогда, когда округлённая до секунд временная метка переходит в следующий слот. maxTransactionFutureMs — это неконсенсусная допустимая погрешность приёма через публичное API, применяемая только в publish(), а не при повторном воспроизведении, проверке пиров/блоков или логике активации консенсуса. Предназначенное условие сохраняет старую политику приёма по слотам, смягчая граничный случай, отклоняя только тогда, когда transactionSlotNumber > currentSlotNumber и время транзакции опережает текущее время узла более чем на maxTransactionFutureMs. Для этого расчёта используйте timestampMs, если клиент его предоставил; в противном случае используйте timestamp * 1000. Небольшая неконсенсусная льгота в publish() (например, 500 мс для следующего слота) может улучшить UX для немного опережающих часов клиентов, не меняя проверку при повторном воспроизведении, хотя клиентским приложениям по-прежнему следует сохранять небольшой резерв временной метки для совместимости со старыми узлами. Транзакция, чья timestamp опережает текущий блок на одну секунду, всё ещё может быть включена в этот блок без нарушения блокчейна, поскольку timestamp транзакции — это подписанная метаданные транзакции, а не источник валидности слота или блока, и консенсус не требует tx.timestamp <= block.timestamp. При повторном воспроизведении узлы не сравнивают временные метки транзакций с локальным системным временем, поэтому льгота на этапе приёма не может вызвать расхождение при повторном воспроизведении.
Эндпоинт статуса
Эндпоинт /api/node/status может возвращать дополнительные поля времени, такие как nodeTimestampMs (миллисекунды эпохи ADAMANT) и unixTimestampMs без влияния на консенсус. nodeTimestamp остаётся в секундах эпохи ADAMANT.
Высоты активации
Высоты активации консенсуса должны быть настраиваемыми для тестовой сети и цепочек на основе ADAMANT, сохраняя значения по умолчанию для основной сети в коде или конфигурации. Историческая проверка fairSystemActivateBlock: 4359464 использовала height > fairSystemActivateBlock, поэтому эквивалентная первая активная высота для именованной активации fairSystem — 4 359 465. Активация spaceship должна ограничивать только поведение, чувствительное к консенсусу, например, приём, нормализацию, хранение и проверку timestampMs во время обработки блоков, пирам и повторного воспроизведения. Проверки publish(), относящиеся к часам, являются политикой приёма и должны оставаться вне активации консенсуса.