2D Chain — Integration Test Report 2D Chain — Отчёт по интеграционным тестам

End-to-end tests across the full chain stack: transfers, atomic swaps, Tron protobuf dispatch, verifier replay and operations, anti-spam, and security audit Сквозные тесты по всему стеку чейна: переводы, атомарные свопы, диспетчеризация через Tron protobuf, верификатор, антиспам и аудит безопасности
51
TestsТесты
14
GroupsГруппы
51
PassingПройдено
0
FailingОшибки
~2.7s
RuntimeВремя
1. Funding Accounts 1. Начальное финансирование 1 test 1 тест
PASS minter funds Alice, Bob, and Charlie minter финансирует Alice, Bob и Charlie
The genesis minter account holds all initial USD-stable supply. This test verifies that the minter can distribute funds to three independent accounts in a single block. It exercises the full RPC round-trip: eth_sendRawTransaction for each transfer, one produce_block call to batch all three into block #1, then receipt and balance checks to confirm each account received exactly 10,000 USD-stable. Генезисный аккаунт minter держит весь начальный запас USD-stable. Тест проверяет, что minter может раздать средства трём независимым аккаунтам в рамках одного блока. Проходит полный цикл RPC: eth_sendRawTransaction для каждого перевода, один вызов produce_block для упаковки всех трёх в блок #1, затем проверка receipt и балансов — каждый аккаунт должен получить ровно 10 000 USD-stable.
Minter 10,000 USD-stable Alice
Minter 10,000 USD-stable Bob
Minter 10,000 USD-stable Charlie
Block #1 (3 transactions) Блок #1 (3 транзакции)
tx0: Minter→Alice tx1: Minter→Bob tx2: Minter→Charlie
receipt.status == 0x1 (x3) Alice == 10,000 Bob == 10,000 Charlie == 10,000
2. Basic Transfers 2. Базовые переводы 3 tests 3 теста
PASS Alice sends 500 USD-stable to Bob Alice отправляет 500 USD-stable Бобу
The simplest possible transfer between two funded accounts. Alice signs an EIP-155 legacy transaction with 500 USD-stable (encoded as 500 × 1018 on the wire, scaled down to 6 decimals by the executor). After block production, the test verifies the receipt landed in block #2 with status success, and that Alice's balance decreased while Bob's increased by exactly the transfer amount. Простейший перевод между двумя аккаунтами с балансом. Alice подписывает legacy-транзакцию EIP-155 на 500 USD-stable (on-wire значение 500 × 1018, executor приводит к 6 десятичным). После формирования блока тест проверяет, что receipt попал в блок #2 со статусом success, баланс Alice уменьшился, а баланс Bob вырос ровно на сумму перевода.
Alice 500 USD-stable Bob
receipt.status == 0x1 blockNumber == 0x2 Alice == 9,500 Bob == 10,500
PASS Bob forwards 200 USD-stable to Charlie Bob пересылает 200 USD-stable Чарли
Tests a two-hop chain of custody: Alice first sends 500 USD-stable to Bob, then in a subsequent block Bob forwards 200 USD-stable of his new balance to Charlie. This verifies that funds received in one block are immediately spendable in the next, and that nonce tracking works correctly when a previously-inactive account (Bob, nonce 0) makes its first outbound transaction. Проверка двухшагового маршрута: Alice сначала отправляет 500 USD-stable Бобу, затем в следующем блоке Bob пересылает 200 USD-stable из полученных средств Чарли. Тест подтверждает, что средства, полученные в одном блоке, сразу доступны для трат в следующем, а nonce корректно отслеживается при первой исходящей транзакции с ранее неактивного аккаунта (Bob, nonce 0).
Alice 500 USD-stable Bob // Bob 200 USD-stable Charlie
receipt.status == 0x1 blockNumber == 0x3 Bob == 10,300 Charlie == 10,200
PASS Charlie sends back to Alice (circular) Charlie отправляет обратно Alice (кольцевой маршрут)
Completes a circular flow: Minter → Alice → Bob → Charlie → Alice. After three sequential blocks of transfers, the test checks that all three accounts hold the expected balances. This catches any double-spend or balance underflow bugs that might appear only when funds traverse multiple accounts across multiple blocks. Замыкает кольцевой маршрут: Minter → Alice → Bob → Charlie → Alice. После трёх последовательных блоков с переводами тест проверяет, что балансы всех трёх аккаунтов соответствуют ожидаемым. Это ловит баги двойной траты или отрицательных балансов, которые проявляются только при прохождении средств через несколько аккаунтов и блоков.
Alice Bob // Bob Charlie // Charlie 100 USD-stable Alice
Alice == 9,600 Bob == 10,300 Charlie == 10,100
3. Multi-Transaction Blocks 3. Блоки с несколькими транзакциями 2 tests 2 теста
PASS three transfers packed in a single block три перевода упакованы в один блок
Three independent senders (Alice, Bob, Charlie) each submit a transfer before a single produce_block call. The block producer executes all three transactions sequentially within one SERIALIZABLE Postgres transaction. This tests that intra-block execution is atomic and correct: Bob can spend his initial 10,000 even though Alice's transfer to him hasn't been committed yet (it's executed earlier in the same block). Три независимых отправителя (Alice, Bob, Charlie) отправляют переводы до единственного вызова produce_block. Block producer выполняет все три транзакции последовательно внутри одной SERIALIZABLE-транзакции Postgres. Тест проверяет атомарность внутри блока: Bob может потратить свои начальные 10 000, хотя перевод от Alice к нему ещё не закоммичен (но уже выполнен ранее в том же блоке).
Alice 100 Bob + Bob 50 Charlie + Charlie 25 Alice
Block #2 (single block, 3 transactions) Блок #2 (один блок, 3 транзакции)
Alice→Bob 100 Bob→Charlie 50 Charlie→Alice 25
block.txs.length == 3 Alice == 9,925 Bob == 10,050 Charlie == 10,025
PASS sequential nonces from same sender in one block последовательные nonce от одного отправителя в одном блоке
Alice submits three transactions with incrementing nonces (0, 1, 2) to different recipients, all included in a single block. The executor processes them in order: after each transaction it increments Alice's nonce, so the next transaction's nonce check passes. This verifies that the intra-block nonce state machine works correctly and doesn't require a new block between each spend from the same sender. Alice отправляет три транзакции с инкрементными nonce (0, 1, 2) разным получателям, и все попадают в один блок. Executor обрабатывает их по порядку: после каждой транзакции nonce Alice увеличивается, поэтому проверка nonce следующей транзакции проходит. Тест подтверждает, что конечный автомат nonce внутри блока работает корректно и не требует нового блока между последовательными тратами одного отправителя.
Alice 10 Bob n=0
Alice 20 Charlie n=1
Alice 30 Bob n=2
all status == 0x1 Alice == 9,940 Bob == 10,040 Charlie == 10,020 Alice.nonce == 3
4. Edge Cases 4. Граничные случаи 6 tests 6 тестов
PASS insufficient balance — tx fails with status 0 недостаточный баланс — транзакция завершается со статусом 0
Alice attempts to transfer 999,999 USD-stable while only holding 10,000. The executor detects the insufficient balance during charge_and_execute and records the transaction as failed (status 0) without modifying any account balances. This is critical for preventing overdrafts: the chain must never create money by allowing a spend beyond the sender's balance. Alice пытается перевести 999 999 USD-stable при балансе 10 000. Executor обнаруживает нехватку средств на этапе charge_and_execute и фиксирует транзакцию как неуспешную (status 0), не изменяя балансы. Это критично для защиты от овердрафта: чейн не должен создавать деньги, разрешая трату сверх баланса отправителя.
Alice (10k) 999,999 USD-stable Bob
receipt.status == 0x0 Alice unchanged == 10,000 Bob unchanged == 10,000
PASS wrong nonce — tx fails with status 0 неверный nonce — транзакция завершается со статусом 0
Alice sends a transaction with nonce 42, but her on-chain nonce is 0. The executor rejects the transaction immediately during nonce validation. Nonce enforcement prevents replay attacks (resubmitting an old transaction) and ensures transactions are processed in the sender's intended order. A wrong nonce never touches balances. Alice отправляет транзакцию с nonce 42, хотя её текущий nonce на чейне равен 0. Executor отклоняет транзакцию сразу на этапе валидации nonce. Контроль nonce защищает от replay-атак (повторная отправка старой транзакции) и гарантирует обработку транзакций в порядке, задуманном отправителем. Неверный nonce не затрагивает балансы.
Alice nonce=42 (expected 0) Bob
receipt.status == 0x0 Alice unchanged == 10,000
PASS zero-value transfer succeeds перевод нулевой суммы проходит успешно
A transfer of 0 USD-stable is technically valid and should succeed. This is important because zero-value transactions are sometimes used to trigger nonce increments, interact with precompiles via calldata, or simply as a "ping" to confirm an address is active. The test verifies balances stay unchanged but the sender's nonce still increments. Перевод 0 USD-stable формально валиден и должен завершиться успешно. Это важно, потому что транзакции с нулевой суммой используют для инкремента nonce, взаимодействия с precompile через calldata или как пинг для подтверждения активности адреса. Тест проверяет, что балансы не изменились, но nonce отправителя увеличился.
Alice 0 USD-stable Bob
receipt.status == 0x1 Alice == 10,000 Bob == 10,000 Alice.nonce == 1
PASS transfer to new address creates account перевод на новый адрес создаёт аккаунт
When funds are sent to an address that has never transacted on-chain, the executor must auto-create the account record before crediting it. This test sends 50 USD-stable to a fresh address (0x0000...DEAD) and confirms the account is created with the correct balance. Without this implicit creation, transfers to new addresses would fail. Когда средства отправляются на адрес, который ещё не участвовал в транзакциях, executor должен автоматически создать запись аккаунта перед зачислением. Тест отправляет 50 USD-stable на новый адрес (0x0000...DEAD) и убеждается, что аккаунт создан с правильным балансом. Без неявного создания переводы на новые адреса завершались бы ошибкой.
Alice 50 USD-stable 0x0000...DEAD
receipt.status == 0x1 new account == 50
PASS duplicate tx submission — balance debited only once повторная отправка транзакции — баланс списывается один раз
The same signed transaction is submitted twice via eth_sendRawTransaction. Both calls return the same tx hash (it's deterministic: keccak256 of raw bytes). The pending_transactions table has a unique constraint on hash, and the RPC handler uses on_conflict: :nothing to silently ignore the duplicate. After block production, only one transaction appears in the block and Alice's balance is debited exactly once. Without this protection, a node operator or network relay could replay a user's transaction to drain their account. Одна и та же подписанная транзакция отправляется дважды через eth_sendRawTransaction. Оба вызова возвращают одинаковый tx hash (он детерминирован: keccak256 от raw-байтов). В таблице pending_transactions стоит unique constraint на hash, а RPC-обработчик использует on_conflict: :nothing для бесшумного игнорирования дубликата. После формирования блока транзакция появляется ровно один раз, баланс Alice списывается однократно. Без этой защиты оператор ноды или сетевой relay мог бы переиграть транзакцию пользователя и опустошить его аккаунт.
Alice 100 USD-stable Bob ×2
hash1 == hash2 Alice == 9,900 Bob == 10,100 nonce == 1
PASS cross-chain replay — tx signed for chain_id 1 rejected at RPC кросс-чейн replay — транзакция с chain_id 1 отклоняется на уровне RPC
A transaction is signed with Ethereum mainnet's chain_id (1) instead of 2D's chain_id (11565). The RPC handler extracts the chain_id from the EIP-155 v field and rejects the transaction before it enters the pending queue. Without this check, a transaction broadcast on Ethereum mainnet could be replayed on 2D if the sender holds the same private key on both chains — the EIP-155 signature scheme recovers the correct address regardless of which chain the tx was signed for. Транзакция подписана с chain_id Ethereum mainnet (1) вместо chain_id 2D (11565). RPC-обработчик извлекает chain_id из поля v по EIP-155 и отклоняет транзакцию до попадания в очередь pending. Без этой проверки транзакция из Ethereum mainnet могла бы быть переиграна на 2D, если отправитель владеет тем же приватным ключом на обеих сетях — EIP-155 восстанавливает корректный адрес независимо от того, для какой сети подписана транзакция.
Alice chain_id=1 (expected 11565) Bob
error.code == -32000 message =~ "wrong chain_id" Alice unchanged == 10,000 Bob unchanged == 10,000
5. Balance Conservation 5. Сохранение баланса 1 test 1 тест
PASS total supply unchanged after many transfers общий объём эмиссии не изменился после множества переводов
The most fundamental invariant of any monetary system: transfers must not create or destroy money. This test snapshots the total supply (SUM of all account balances) before and after five transfers spread across two blocks. Any bug in the debit/credit logic — rounding errors, off-by-one in decimal scaling, or double-credits — would cause the totals to diverge. The gasless fee model means no supply leaks to fee burns either. Фундаментальный инвариант любой денежной системы: переводы не должны создавать или уничтожать деньги. Тест фиксирует суммарный баланс всех аккаунтов до и после пяти переводов в двух блоках. Любой баг в логике списания/зачисления — ошибки округления, сдвиг на единицу при масштабировании десятичных, двойное зачисление — приведёт к расхождению сумм. Благодаря модели без комиссий утечка через сжигание fee тоже исключена.
Block #2 Блок #2
Alice→Bob 1,000 Bob→Charlie 500 Charlie→Alice 250
Block #3 Блок #3
Alice→Charlie 300 Bob→Alice 700
total_supply(before) == total_supply(after)
6. RPC Query Consistency 6. Консистентность RPC-запросов 4 tests 4 теста
PASS eth_getBalance matches DB after transfers eth_getBalance совпадает с базой после переводов
The chain stores balances in 6-decimal USD-stable internally, but the Ethereum RPC spec expects 18-decimal values (like wei). This test performs a transfer, then queries the balance via both the direct DB path and the eth_getBalance RPC, and verifies the RPC result equals the DB value multiplied by 1012. A mismatch here would mean wallets display wrong balances. Чейн хранит балансы в 6-десятичном формате USD-stable, но спецификация Ethereum RPC ожидает 18-десятичные значения (как wei). Тест выполняет перевод, затем запрашивает баланс напрямую из базы и через eth_getBalance RPC и проверяет, что результат RPC равен значению из БД, умноженному на 1012. Расхождение означало бы, что кошельки показывают неверные балансы.
Alice 777 USD-stable Bob
rpc(eth_getBalance) == db_balance × 10^12
PASS eth_getTransactionCount tracks nonce eth_getTransactionCount отслеживает nonce
Wallets use eth_getTransactionCount to determine the next nonce for a new transaction. After Alice sends two successful transactions, this test verifies the RPC returns 0x2, matching the expected on-chain nonce. If this value is stale or wrong, wallet-submitted transactions would be rejected for nonce mismatch. Кошельки вызывают eth_getTransactionCount, чтобы узнать следующий nonce для новой транзакции. После двух успешных транзакций Alice тест проверяет, что RPC возвращает 0x2 — ожидаемый nonce на чейне. Если значение устаревшее или ошибочное, транзакции от кошелька будут отклонены из-за несовпадения nonce.
eth_getTransactionCount == 0x2
PASS block contains correct tx_count блок содержит правильное количество транзакций
Verifies that eth_getBlockByNumber returns the right number of transactions. Two transfers from different senders are submitted and mined in one block; the RPC response must list exactly two transaction hashes. Block explorers and indexers depend on this count being accurate. Проверяет, что eth_getBlockByNumber возвращает верное количество транзакций. Два перевода от разных отправителей попадают в один блок; ответ RPC должен содержать ровно два хеша транзакций. Блок-эксплореры и индексаторы зависят от точности этого значения.
block.transactions.length == 2
PASS chain integrity across multiple blocks целостность цепочки блоков
Each block stores the hash of the previous block as its parent_hash, forming an immutable chain. This test produces four blocks (funding + three transfers), then walks the entire chain from genesis to tip and asserts that every child block's parent_hash matches its predecessor's hash. A broken link would mean the chain can't be verified and block reorganization detection would fail. Каждый блок хранит хеш предыдущего в поле parent_hash, формируя неизменяемую цепочку. Тест создаёт четыре блока (финансирование + три перевода), затем обходит всю цепочку от генезиса до последнего блока и проверяет, что parent_hash каждого дочернего блока совпадает с хешем предшественника. Разрыв связи означал бы невозможность верификации чейна и сломанное обнаружение реорганизаций.
Chain linkage Связность цепочки
B0 (genesis) B1 (fund) B2 (A→B) B3 (B→C) B4 (C→A)
blocks.length == 5 child.parent_hash == parent.hash (×4)
7. Tron Protobuf Transfers 7. Переводы через Tron protobuf 6 tests 6 тестов
PASS Alice sends 500 USD-stable to Bob via Tron wallet API Alice отправляет 500 USD-stable Бобу через Tron wallet API
The chain supports Tron-format transactions submitted via the /wallet/broadcasttransaction endpoint, matching the real TronGrid API shape so that TronLink wallets can connect natively. This test builds a Tron protobuf TransferContract, signs it with SHA-256 (Tron's hash algorithm, unlike Ethereum's keccak256), and submits it through the HTTP endpoint. After block production, it verifies that balances reflect the transfer just as they would for an ETH transaction. Чейн принимает транзакции в Tron-формате через эндпоинт /wallet/broadcasttransaction, повторяя формат TronGrid API, чтобы кошельки TronLink подключались без адаптеров. Тест собирает protobuf-структуру TransferContract, подписывает её через SHA-256 (алгоритм хеширования Tron, в отличие от keccak256 у Ethereum) и отправляет через HTTP. После формирования блока тест проверяет, что балансы изменились так же, как при обычной ETH-транзакции.
Alice 500 USD-stable Bob (tron_pb)
broadcast.result == true Alice == 9,500 Bob == 10,500
PASS mixed ETH and Tron transfers in the same block ETH и Tron переводы в одном блоке
The block producer must handle both transaction formats in a single block, dispatching each to its respective decoder (RLP for ETH, protobuf for Tron). This test submits one ETH and one Tron transfer, mines them together, and verifies both executed correctly. This proves the kind-based dispatch in BlockExecutor works and that the two codepaths don't interfere with each other's nonce or balance tracking. Block producer должен обрабатывать оба формата транзакций в одном блоке, направляя каждую на свой декодер (RLP для ETH, protobuf для Tron). Тест отправляет один ETH- и один Tron-перевод, майнит их вместе и проверяет корректность обоих. Это доказывает, что диспетчеризация по kind в BlockExecutor работает, а две ветки кода не мешают друг другу в отслеживании nonce и балансов.
Block #2 (mixed format) Блок #2 (смешанный формат)
Alice→Bob 100 (eth_rlp) Bob→Charlie 200 (tron_pb)
block.txs.length == 2 Alice == 9,900 Bob == 9,900 Charlie == 10,200
PASS ETH and Tron both credit the same receiver in one block ETH и Tron зачисляют на один и тот же аккаунт в одном блоке
Alice sends 300 USD-stable to Bob via ETH RLP, and Charlie sends 200 USD-stable to the same Bob via Tron protobuf — all in a single block. The executor processes both transaction kinds sequentially, and both credit the same account. This test catches bugs where one codepath might overwrite the other's balance update instead of accumulating, or where the account row created by the first credit isn't visible to the second within the same SERIALIZABLE transaction. Alice отправляет 300 USD-stable Бобу через ETH RLP, а Charlie — 200 USD-stable тому же Бобу через Tron protobuf, и всё в одном блоке. Executor обрабатывает оба формата последовательно, оба зачисляют на один аккаунт. Тест ловит баги, при которых один codepath может перезаписать обновление баланса от другого вместо накопления, или когда запись аккаунта, созданная первым зачислением, не видна второму внутри той же SERIALIZABLE-транзакции.
Alice 300 USD-stable Bob (eth_rlp)
Charlie 200 USD-stable Bob (tron_pb)
Bob == 10,500 Alice == 9,700 Charlie == 9,800
PASS balance contention — second tx fails when first drains the account конкуренция за баланс — второй перевод падает, когда первый опустошает аккаунт
Alice sends 9,999 USD-stable to Bob via ETH (leaving just 1 USD-stable), and simultaneously submits a 500 USD-stable Tron transfer to Charlie. Since the ETH transaction executes first and drains nearly all of Alice's balance, the Tron transfer hits insufficient funds and fails with status 0. This tests cross-format balance contention: the executor must not process the two transactions in isolation — the second must see the state left by the first. Alice отправляет 9 999 USD-stable Бобу через ETH (остаётся 1 USD-stable) и одновременно подаёт Tron-перевод на 500 USD-stable Чарли. ETH-транзакция исполняется первой и почти полностью опустошает баланс, поэтому Tron-перевод получает insufficient balance и падает со status 0. Тест проверяет конкуренцию за баланс между форматами: executor не должен обрабатывать транзакции изолированно — вторая обязана видеть состояние, оставленное первой.
Alice 9,999 USD-stable Bob (eth_rlp)
Alice 500 USD-stable (insufficient) Charlie (tron_pb)
Alice == 1 Bob == 19,999 Charlie unchanged == 10,000
PASS expired Tron transaction rejected at broadcast истекшая Tron-транзакция отклоняется при broadcast
Tron transactions carry an explicit validity window (timestamp to expiration). If the expiration is already in the past when the transaction reaches the broadcast endpoint, it must be rejected before even entering the pending queue. This test submits a transaction with both timestamp and expiration set 60–120 seconds in the past and verifies the wallet API returns error code 8 (timing error) without modifying any balances. Tron-транзакции содержат явное окно валидности (timestamp до expiration). Если срок истёк к моменту получения транзакции, она должна быть отклонена до попадания в очередь ожидающих. Тест отправляет транзакцию с timestamp и expiration на 60–120 секунд в прошлом и проверяет, что wallet API возвращает код ошибки 8 (timing error), не изменяя балансы.
Alice expired (ts−120s, exp−60s) Bob
result == false code == 8 Alice unchanged == 10,000
PASS Tron validity window > 24h rejected at broadcast Tron-транзакция с окном валидности > 24 ч отклоняется при broadcast
To prevent transactions from being valid indefinitely (which would enable replay attacks long after the sender forgot about them), the chain enforces a maximum 24-hour validity window. This test submits a transaction with a 48-hour window and verifies rejection. The check happens at broadcast time, so the transaction never enters pending_transactions. Чтобы транзакции не оставались валидными бесконечно (что открыло бы путь replay-атакам спустя долгое время после отправки), чейн ограничивает окно валидности 24 часами. Тест отправляет транзакцию с 48-часовым окном и проверяет отклонение. Проверка происходит на этапе broadcast, поэтому транзакция не попадает в pending_transactions.
Alice 48h validity window Bob
result == false code == 8 Alice unchanged == 10,000
8. HTLC Atomic Swaps 8. Атомарные свопы HTLC 5 tests 5 тестов
PASS Alice locks, Bob claims with correct preimage Alice блокирует средства, Bob забирает по правильному preimage
The happy path of a hash time-locked contract: Alice locks 500 USD-stable into the HTLC precompile by calling lock(hash, receiver, amount, deadline), where hash = SHA-256(preimage). The funds leave Alice's account and are held by the contract. Bob then calls claim(hash, preimage) with the correct secret, proving knowledge of the preimage. The contract verifies SHA-256(preimage) matches the hash, transfers the locked funds to Bob, and marks the swap as claimed. This is the primitive that enables trustless cross-chain atomic swaps. Основной сценарий hash time-locked contract: Alice блокирует 500 USD-stable в HTLC precompile вызовом lock(hash, receiver, amount, deadline), где hash = SHA-256(preimage). Средства уходят с аккаунта Alice и удерживаются контрактом. Затем Bob вызывает claim(hash, preimage) с правильным секретом, доказывая знание preimage. Контракт проверяет, что SHA-256(preimage) совпадает с хешем, переводит заблокированные средства Бобу и отмечает своп как claimed. Это базовый примитив для trustless кросс-чейн атомарных свопов.
Alice LOCK 500 USD-stable HTLC
Bob CLAIM (preimage) HTLC 500 USD-stable Bob
Alice == 9,500 (after lock) Bob == 10,500 (after claim) htlc.status == "claimed"
PASS Alice locks, deadline passes, Alice refunds Alice блокирует средства, дедлайн истекает, Alice возвращает
The safety net for the sender: if Bob never claims (perhaps the cross-chain leg failed), Alice must be able to recover her locked funds after the deadline. This test sets a tight 1-second deadline, waits for it to pass, then calls refund(hash). The contract checks that the caller is the original sender and the deadline has elapsed, then returns the locked amount to Alice. Without this mechanism, unclaimed HTLC locks would permanently freeze funds. Страховочный механизм для отправителя: если Bob не забрал средства (например, кросс-чейн этап не прошёл), Alice должна вернуть заблокированные средства после дедлайна. Тест задаёт короткий дедлайн в 1 секунду, ждёт его истечения и вызывает refund(hash). Контракт проверяет, что вызывающий — оригинальный отправитель и дедлайн прошёл, после чего возвращает заблокированную сумму Alice. Без этого механизма невостребованные HTLC-локи замораживали бы средства навсегда.
Alice LOCK 300 USD-stable (1s deadline) HTLC
Lock OK Lock OK sleep 1.1s sleep 1.1s Refund OK Refund OK
Alice REFUND HTLC 300 USD-stable Alice
Alice == 9,700 (after lock) Alice == 10,000 (after refund) htlc.status == "refunded"
PASS Bob cannot claim with wrong preimage Bob не может забрать средства с неверным preimage
The security guarantee of HTLC: only the holder of the correct preimage can claim the funds. This test locks 100 USD-stable with hash = SHA-256(preimage), then Bob tries to claim with a randomly generated wrong preimage. The contract computes SHA-256(wrong_preimage), finds it doesn't match the stored hash, and reverts. The swap stays locked and Bob's balance is unchanged. This prevents brute-force theft of locked funds. Гарантия безопасности HTLC: только обладатель правильного preimage может забрать средства. Тест блокирует 100 USD-stable с hash = SHA-256(preimage), затем Bob пытается забрать средства со случайным неверным preimage. Контракт вычисляет SHA-256(wrong_preimage), обнаруживает несовпадение с хешем и откатывает операцию. Своп остаётся заблокированным, баланс Bob не изменяется. Это защищает от перебора при попытке украсть заблокированные средства.
Alice LOCK 100 USD-stable HTLC
Bob CLAIM (wrong preimage) HTLC
htlc.status == "locked" (unchanged) Bob unchanged == 10,000
PASS Bob cannot claim after deadline Bob не может забрать средства после дедлайна
Even with the correct preimage, a claim must arrive before the deadline. After the deadline, only the sender can refund. This test locks funds with a 1-second deadline, waits for it to pass, then submits a valid claim from Bob with the correct preimage. The contract rejects it because the deadline has elapsed. This ensures the sender's refund window is protected: once the deadline passes, the receiver can no longer front-run a refund. Даже с правильным preimage claim должен прийти до дедлайна. После дедлайна только отправитель может вызвать refund. Тест блокирует средства с дедлайном в 1 секунду, ждёт его истечения и отправляет валидный claim от Bob с правильным preimage. Контракт отклоняет его, потому что дедлайн прошёл. Это защищает окно refund отправителя: после дедлайна получатель не может опередить refund своим claim.
Alice LOCK 100 USD-stable (1s deadline) HTLC
Lock OK Lock OK sleep 1.1s sleep 1.1s Claim REJECTED Claim REJECTED
htlc.status == "locked" (unchanged) Bob unchanged == 10,000
PASS balance conservation through lock-claim cycle сохранение баланса в цикле lock-claim
HTLC operations move funds between accounts and the contract's escrow. This test verifies that the total monetary supply is conserved throughout the entire lifecycle: after locking 1,000 USD-stable, the sum of all account balances plus locked HTLC amounts must equal the initial total supply. After Bob claims, the funds return to the account system and the simple account-only sum must match again. Any leakage in the lock/claim path would be caught here. Операции HTLC перемещают средства между аккаунтами и эскроу контракта. Тест проверяет, что суммарная денежная масса сохраняется на протяжении всего жизненного цикла: после блокировки 1 000 USD-stable сумма балансов всех аккаунтов плюс заблокированные суммы HTLC должна равняться начальному объёму эмиссии. После claim Боба средства возвращаются в систему аккаунтов, и простая сумма балансов снова должна сойтись. Любая утечка на пути lock/claim будет поймана этим тестом.
Alice LOCK 1,000 HTLC CLAIM Bob
accounts + htlc_locked == initial (mid-lock) accounts == initial (post-claim)
9. BlockFeed Integration 9. Интеграция BlockFeed 1 test 1 тест
PASS BlockFeed returns produced blocks with correct structure BlockFeed возвращает созданные блоки с корректной структурой
After producing blocks via RPC transactions and produce_block, this test queries BlockFeed.get_blocks to verify the block feed returns the correct payloads. It checks that the funding block contains 3 transactions, the transfer block contains 2, and that critical fields — hash (32 bytes), state_root, transactions_root, and parent_hash linkage — are all present and correct. This is the API that verifier nodes use to catch up from upstream, so its output must exactly match what the producer committed. После создания блоков через RPC-транзакции и produce_block тест запрашивает BlockFeed.get_blocks и проверяет корректность возвращаемых payload-ов. Проверяется, что блок финансирования содержит 3 транзакции, блок переводов — 2, а ключевые поля — hash (32 байта), state_root, transactions_root и связка parent_hash — присутствуют и корректны. Это тот самый API, через который верифицирующие узлы синхронизируются с upstream, поэтому его выход должен точно совпадать с тем, что зафиксировал producer.
Block #1 — Funding (3 txs) Блок #1 — Финансирование (3 tx)
tx0: Minter→Alice tx1: Minter→Bob tx2: Minter→Charlie
Block #2 — Transfers (2 txs) Блок #2 — Переводы (2 tx)
tx0: Alice→Bob tx1: Bob→Charlie
blocks.length == 2 funding.txs == 3 transfer.txs == 2 hash: 32 bytes parent_hash linkage OK
10. Verifier Replay 10. Воспроизведение верификатором 4 tests 4 теста
PASS verifier accepts independently computed valid block верификатор принимает независимо вычисленный валидный блок
The verifier receives a block payload that was independently computed via a rolled-back savepoint — the same transactions are executed, state_root and block_hash are captured, then the savepoint is undone. The verifier then replays the block from scratch on clean state: it re-executes every transaction, recomputes the state root, and derives the block hash. If all three match the claimed values, the block is accepted and the tip advances. This proves that given identical inputs, producer and verifier arrive at the same deterministic output. Верификатор получает payload блока, вычисленный независимо через откатываемый savepoint: транзакции исполняются, state_root и block_hash фиксируются, затем savepoint откатывается. Верификатор проигрывает блок с нуля на чистом состоянии: заново исполняет каждую транзакцию, пересчитывает state root и вычисляет хэш блока. Если все три значения совпадают с заявленными, блок принимается и tip продвигается. Это доказывает, что при одинаковых входных данных producer и verifier приходят к одному и тому же детерминированному результату.
Producer block payload Verifier VERIFY ACCEPT
{:ok, 1} tip.number == 1 tip.hash == payload.hash
PASS verifier replays multi-block sequence верификатор воспроизводит последовательность из нескольких блоков
Three blocks are verified in sequence, each transferring 100 USD-stable from the minter to a different recipient (Alice, Bob, Charlie). After each verify_and_commit, the verifier's tip advances and the account state updates. This tests that the verifier correctly maintains state across multiple blocks — nonces increment, balances accumulate, and the parent_hash chain stays intact through the entire sequence. Три блока верифицируются последовательно, каждый переводит 100 USD-stable от minter разным получателям (Alice, Bob, Charlie). После каждого verify_and_commit tip верификатора продвигается, состояние аккаунтов обновляется. Тест проверяет, что верификатор корректно поддерживает состояние через несколько блоков — nonce-ы растут, балансы накапливаются, цепочка parent_hash остаётся целой на протяжении всей последовательности.
Minter 100 USD-stable Alice (block 1)
Minter 100 USD-stable Bob (block 2)
Minter 100 USD-stable Charlie (block 3)
Verifier VERIFY ×3 ALL ACCEPTED
Alice == 100 Bob == 100 Charlie == 100
PASS verifier rejects tampered state_root верификатор отклоняет подменённый state_root
A valid block payload is computed, then its state_root is replaced with a fake value. The verifier re-executes the transactions and computes the real state root, which doesn't match the tampered one. It returns state_root_mismatch and refuses to advance the tip. This is the core security guarantee: a malicious producer cannot lie about the post-execution state without being caught by every verifier in the network. Вычисляется валидный payload блока, затем его state_root подменяется фиктивным значением. Верификатор заново исполняет транзакции, вычисляет реальный state root — он не совпадает с подменённым. Возвращается state_root_mismatch, tip не продвигается. Это ключевая гарантия безопасности: злонамеренный producer не может соврать о состоянии после исполнения — каждый верификатор в сети это обнаружит.
Producer valid block tamper state_root Verifier VERIFY REJECTED
{:error, {:state_root_mismatch, _}} tip == 0
PASS verifier rejects tampered block_hash верификатор отклоняет подменённый block_hash
Similar to the state_root test, but the block hash itself is replaced. The verifier recomputes the hash from the block's components (number, parent_hash, timestamp, transactions_root, state_root) and finds a mismatch. Even if all individual fields are correct, a wrong hash means the block was tampered with after construction. This prevents a subtle attack where a producer swaps in a valid-looking block from a different chain fork. Аналогично тесту с state_root, но подменяется сам хэш блока. Верификатор пересчитывает хэш из компонентов блока (number, parent_hash, timestamp, transactions_root, state_root) и обнаруживает несовпадение. Даже если все отдельные поля корректны, неправильный хэш означает, что блок был изменён после построения. Это предотвращает атаку, при которой producer подставляет валидно выглядящий блок из другого форка цепи.
Producer valid block tamper hash Verifier VERIFY REJECTED
{:error, {:block_hash_mismatch, _}}
11. Verifier Operations 11. Операционная устойчивость верификатора 8 tests 8 тестов
PASS verifier independently verifies genesis state_root and block_hash верификатор независимо проверяет state_root и block_hash генезиса
A fresh verifier with its own database recomputes the genesis state_root via StateRoot.compute() and the genesis block_hash from header components, asserting equality with the persisted row. This proves the verifier can bootstrap from an empty database without trusting the producer's persisted hashes. Свежий верификатор со своей БД пересчитывает state_root генезиса через StateRoot.compute() и block_hash из компонентов заголовка, сверяясь со строкой в базе. Это подтверждает, что верификатор может стартовать с пустой БД, не доверяя сохранённым хэшам producer-узла.
computed_state_root == genesis.state_root computed_hash == genesis.hash parent_hash == zero
PASS verifier rejects tampered transactions_root верификатор отклоняет подменённый transactions_root
The verifier now checks transactions_root in addition to state_root and block_hash. A valid payload with a tampered transactions_root is rejected before block_hash verification, catching payloads where the transaction list was replaced but the block hash was not recomputed. Верификатор теперь проверяет transactions_root в дополнение к state_root и block_hash. Валидный payload с подменённым transactions_root отклоняется до проверки block_hash, перехватывая payload-ы, где список транзакций был заменён без пересчёта хэша блока.
{:error, {:transactions_root_mismatch, _}} tip stays at 0
PASS executor derives tx hash from raw bytes, ignores payload hash исполнитель вычисляет хэш транзакции из raw, игнорирует хэш из payload
The block executor now computes transaction hashes from raw bytes (keccak256 for ETH, sha256 for Tron) instead of trusting the hash provided in the payload. A payload with deliberately wrong tx hashes is still verified successfully because the executor independently derives the correct hashes. Исполнитель блоков теперь вычисляет хэши транзакций из raw-байтов (keccak256 для ETH, sha256 для Tron) вместо доверия хэшу из payload. Payload с намеренно неверными хэшами транзакций всё равно успешно верифицируется, потому что исполнитель независимо вычисляет правильные хэши.
{:ok, 1} with tampered tx hashes
PASS executor rejects transaction with nil raw (missing raw data) исполнитель отклоняет транзакцию с nil raw (отсутствуют raw-данные)
Pre-existing transactions with NULL raw (from before the migration that added the column) caused FunctionClauseError in Crypto.to_hex(nil). Now the executor returns {:error, :missing_raw_data} and records a failed transaction (status=0) instead of crashing. Транзакции с NULL raw (из периода до миграции, добавившей колонку) вызывали FunctionClauseError в Crypto.to_hex(nil). Теперь исполнитель возвращает {:error, :missing_raw_data} и записывает неуспешную транзакцию (status=0) вместо краша.
status == 0 no crash
PASS eth_sendRawTransaction rejected in verifier mode eth_sendRawTransaction отклоняется в режиме верификатора
Verifier nodes are read-only replicas. State-mutating RPC methods (eth_sendRawTransaction, broadcasttransaction) return an error when the node runs in verifier mode, preventing accidental writes to a node that has no block producer. Узлы-верификаторы являются read-only репликами. Мутирующие RPC-методы (eth_sendRawTransaction, broadcasttransaction) возвращают ошибку в режиме верификатора, предотвращая случайные записи на узле без block producer.
error code -32601 message: "verifier"
PASS overlong r/s signature component rejected слишком длинный компонент подписи r/s отклоняется
RLP-decoded r/s scalars longer than 32 bytes are now rejected before calling pad_to_32, which previously crashed with no matching function clause. This prevents a denial-of-service vector via malformed transaction signatures. RLP-декодированные скаляры r/s длиной более 32 байтов теперь отклоняются до вызова pad_to_32, который ранее падал без подходящего clause. Это предотвращает вектор отказа в обслуживании через некорректные подписи транзакций.
{:error, :overlong_signature_component}
PASS BlockFeed includes signature in transaction payloads BlockFeed включает signature в payload транзакций
BlockFeed and finalize_block now include the :signature field in transaction payloads broadcast to downstream verifiers. Without this, Tron transactions could not be re-verified by chained verifiers (signature is needed to recover the sender). BlockFeed и finalize_block теперь включают поле :signature в payload транзакций, транслируемых нижестоящим верификаторам. Без этого Tron-транзакции не могли быть повторно верифицированы цепочкой верификаторов (подпись нужна для восстановления отправителя).
tron_pb tx has signature signature: 65 bytes
PASS finalize_block includes signature in broadcast payload finalize_block включает signature в broadcast payload
The payload returned by BlockExecutor.finalize_block (used by BlockStage.notify for live broadcasts to subscribers) now includes the signature field for each transaction, enabling verifier chaining for Tron transactions. Payload, возвращаемый BlockExecutor.finalize_block (используется BlockStage.notify для live-трансляций подписчикам), теперь включает поле signature для каждой транзакции, обеспечивая цепочку верификации для Tron-транзакций.
tx_payload.signature == sig
12. Anti-spam Throttle 12. Антиспам-throttle 2 tests 2 теста
PASS throttled sender excluded from block, other senders unblocked throttled-отправитель исключён из блока, другие отправители не блокируются
With gas_threshold lowered to 3, Alice submits 4 txs and 2 more after the first block. The throttle defers Alice's later txs once the sliding window count exceeds the threshold, so only the first 4 land. Bob submits independently and his tx lands in the same block — confirming the throttle is per-sender, not global. С gas_threshold = 3 Alice отправляет 4 транзакции и ещё 2 после первого блока. Throttle откладывает её последние транзакции, как только счётчик в скользящем окне превышает порог, поэтому в блоки попадают только первые 4. Bob отправляет независимо, и его транзакция попадает в тот же блок — это подтверждает, что throttle применяется к каждому отправителю отдельно, а не глобально.
alice_included == 4 bob_included == 1
PASS gasless block verifies — fee=0 means deterministic state_root gasless-блок верифицируется — fee=0 даёт детерминированный state_root
The producer commits a payload with gas_price=0 and the verifier replays it from raw bytes, computing a matching state_root. This proves the throttle is purely a producer-local concern: the executor path has no ETS dependency, so verifier replay is deterministic regardless of wall-clock skew across catch-up windows (closes the bug_007 vector by design). Producer коммитит payload с gas_price=0, верификатор воспроизводит его из raw-байт и получает совпадающий state_root. Это доказывает, что throttle — исключительно producer-local concern: путь исполнителя не зависит от ETS, поэтому верификация детерминирована независимо от рассинхронизации wall-clock в окнах catch-up (закрывает вектор bug_007 by design).
tx.status == 1 tx.gas_price == 0 verifier state_root matches
13. Security Audit 13. Аудит безопасности 6 tests 6 тестов
PASS eth_getLogs with malformed address returns error, not crash eth_getLogs с некорректным адресом возвращает ошибку, не падает
The eth_getLogs filter uses safe decoders (decode_address_safe / decode_hex_safe). A malformed address like "not-a-valid-address" returns JSON-RPC error -32602 instead of crashing the request worker into a 500. Фильтр eth_getLogs использует безопасные декодеры (decode_address_safe / decode_hex_safe). Некорректный адрес "not-a-valid-address" возвращает JSON-RPC ошибку -32602 вместо падения worker-а в 500.
error code -32602
PASS eth_getLogs with malformed topic returns error, not crash eth_getLogs с некорректным topic возвращает ошибку, не падает
Same hardening covers topics: a malformed hex string like "zzz-bad-hex" is rejected at the filter parser, not at the underlying SQL layer where it would have surfaced as a less informative error. То же самое для topics: некорректный hex "zzz-bad-hex" отклоняется на парсере фильтра, а не на нижнем SQL-слое, где это всплыло бы менее информативной ошибкой.
error code -32602
PASS eth_sendRawTransaction rejects stale nonce eth_sendRawTransaction отклоняет устаревший nonce
After Alice submits and her tx lands, re-submitting the same nonce returns "nonce too low" at RPC submission time. This blocks naive replay attempts before they reach the pending pool. После того как транзакция Alice попала в блок, повторная отправка с тем же nonce возвращает "nonce too low" уже на этапе RPC-приёма. Это блокирует наивные попытки replay-атаки до попадания в pending pool.
message =~ "nonce too low"
PASS eth_sendRawTransaction rejects nonce too far ahead eth_sendRawTransaction отклоняет nonce, ушедший слишком далеко вперёд
A nonce greater than account_nonce + 100 is rejected at submission. This caps how many "future" pending rows a single sender can park in the pool, blocking a mempool-pollution griefing vector while still allowing legitimate batched submissions. Nonce больше account_nonce + 100 отклоняется на приёме. Это ограничивает, сколько "future" pending-строк один отправитель может оставить в пуле, блокируя вектор mempool-pollution и при этом не мешая легитимным пакетным отправкам.
message =~ "nonce too far ahead"
PASS stale pending transactions are cleaned up устаревшие pending-транзакции вычищаются
A pending row inserted with inserted_at older than 10 minutes is removed by cleanup_stale_pending. The block producer runs this sweep every 30 blocks (~60s), so a stuck high-nonce tx cannot wedge the pool indefinitely. Pending-строка с inserted_at старше 10 минут удаляется cleanup_stale_pending. Block producer запускает этот sweep каждые 30 блоков (~60с), поэтому застрявшая транзакция с высоким nonce не может бесконечно держать пул занятым.
pending count: 1 → 0
PASS eth_sendRawTransaction rejects oversized transaction eth_sendRawTransaction отклоняет слишком большие транзакции
Raw payloads above the configured 4 KB cap are rejected before hex decode (the cap applies to the hex-string size so the actual byte budget is honored). This blocks an obvious DoS vector via giant calldata before any work is done. Raw-payload-ы больше настроенного лимита 4 KB отклоняются до hex-декодирования (лимит применяется к размеру hex-строки, так что реальный байтовый бюджет соблюдается). Это блокирует очевидный DoS-вектор через гигантский calldata до выполнения какой-либо работы.
message =~ "too large"
14. Tron protobuf precompile dispatch 14. Tron protobuf → precompile 2 tests 2 теста
PASS Alice locks via Tron TriggerSmartContract; HTLC state mutates and log is emitted Alice блокирует через Tron TriggerSmartContract; HTLC-state мутирует и эмитится лог
Alice signs an HTLC `lock(...)` selector + args wrapped in a Tron `TriggerSmartContract` envelope (with `contract` = `0x41` + HTLC address) and broadcasts via `/wallet/broadcasttransaction`. After produce_block, the htlc_swaps row materialises with the right sender / receiver / amount / deadline; the transactions row is `kind="tron_pb"` with `hash = sha256(raw_data)` matching the broadcast `txid`; and the HTLC_Locked log carries the right address, all four topic slots, and `data == <<amount::256, deadline_ms::256>>`. Confirms the Tron-format path resolves the sender to the same 20-byte account as the equivalent ETH RLP path. Alice подписывает селектор HTLC `lock(...)` с аргументами, упакованный в Tron-envelope `TriggerSmartContract` (`contract` = `0x41` + адрес HTLC), и отправляет через `/wallet/broadcasttransaction`. После produce_block появляется строка в htlc_swaps с правильными sender / receiver / amount / deadline; в transactions — запись `kind="tron_pb"` с `hash = sha256(raw_data)`, совпадающим с `txid` ответа broadcast-а; лог HTLC_Locked несёт нужный адрес, все четыре топика и `data == <<amount::256, deadline_ms::256>>`. Подтверждает, что Tron-путь резолвит отправителя в тот же 20-байтовый аккаунт, что и эквивалентный путь через ETH RLP.
tx.kind == "tron_pb" tx.hash == sha256(raw_data) htlc_swaps.sender == Alice log.topic0 == HTLC_Locked
PASS unknown selector via Tron lands in block with status=0; pending cleared, no swap row неизвестный селектор через Tron попадает в блок со status=0; pending очищен, swap не создан
Same envelope as the happy path but with selector `<<0xDE, 0xAD, 0xBE, 0xEF>>` and 96 zero-byte args. After produce_block, the transactions row lands with `kind="tron_pb"` and `status=0` (HTLC.execute reverts on the unknown selector), no htlc_swaps row appears, the pending pool is cleared (no retry loop), and Alice's balance is unchanged — the revert path doesn't debit. Тот же envelope, что в happy path, но селектор `<<0xDE, 0xAD, 0xBE, 0xEF>>` и 96 нулевых байт аргументов. После produce_block строка в transactions появляется с `kind="tron_pb"` и `status=0` (HTLC.execute ревертит на неизвестном селекторе), строки в htlc_swaps не появляется, pending pool очищается (без retry-loop), баланс Alice не меняется — revert не списывает.
tx.status == 0 pending count == 0 htlc_swaps count == 0 Alice balance unchanged