Minter (HH #0)
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Genesis: 1M USD-stableГенезис: 1M USD-stable
Alice (HH #1)
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Funded: 10,000 USD-stableБаланс: 10 000 USD-stable
Bob (HH #2)
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Funded: 10,000 USD-stableБаланс: 10 000 USD-stable
Charlie (HH #3)
0x90F79bf6EB2c4f870365E785982E1f101E93b906
Funded: 10,000 USD-stableБаланс: 10 000 USD-stable
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
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