This branch contains the minimal code fix for the rectification receipt persistence bug in koinos-chain.
The bug was that rectified block receipts were computed, but the original unrectified receipt was the one persisted to block_store and broadcast through koinos.block.accept.
That means:
- the fix in this branch prevents future bad receipts from being written,
- but it does not retroactively repair block stores that already contain corrupted historical receipts.
For already-affected nodes, chain.verify-blocks=true is still required during reindex so that the node re-executes blocks instead of trusting persisted deltas.
This branch also bumps the chain version to 1.5.1.
This branch changes:
koinos-chain1.4.1 -> 1.5.1
The bump is justified as a patch release because the behavior change is a correctness fix in persisted receipt handling, not a protocol or API redesign.
The failure happened inside controller_impl::apply_block().
The code first copied the current receipt from the block application context into res.receipt, then passed that copy through maybe_rectify_state().
Relevant code:
Conceptually:
res.receipt = std::get< protocol::block_receipt >( ctx.receipt() );
maybe_rectify_state( ctx, block, *res.receipt );So far that is correct: rectification updates the copied receipt held in res.receipt.
The bug appeared afterward, because the writes still used ctx.receipt() instead of *res.receipt.
Two persistence paths were wrong.
Before the fix, the block_store.add_block request wrote the original receipt:
- previous behavior used
std::get< protocol::block_receipt >( ctx.receipt() )
After the fix, it writes the rectified receipt:
Before the fix, the accepted-block broadcast also used the original receipt.
After the fix, it uses the rectified receipt:
This branch is intentionally small.
The code fix is only two behavioral substitutions in controller.cpp:
req.mutable_add_block()->mutable_receipt_to_add()->CopyFrom( *res.receipt );and:
*ba.mutable_receipt() = *res.receipt;instead of using ctx.receipt().
That is enough to ensure the receipt persisted and broadcast after rectification is the same receipt that was actually produced by the block application path.
When reindexing, chain can rebuild state by replaying persisted deltas rather than re-executing the full block.
That fast path is:
When _verify_blocks is false, reindex uses:
_controller.apply_block_delta( block_item.block(), block_item.receipt(), _target_head.height() );If the stored receipt is missing rectification deltas, reindex reconstructs the wrong state. That can later surface as a merkle mismatch when the node tries to continue syncing from peers.
This is the operationally important point.
The code fix prevents future corruption. It does not rewrite history inside an already-corrupted block_store.
If a node has already persisted bad receipts, upgrading to this fixed binary alone is not enough. On restart, the node may still read those historical receipts and rebuild the wrong state if it stays on the delta replay path.
That is why recovery for an already-affected node still requires:
chain:
verify-blocks: trueor the equivalent CLI/runtime setting.
verify-blocks is parsed from config/CLI in:
When it is enabled, the indexer switches from receipt replay to full block verification:
rpc::chain::submit_block_request submit_block;
*submit_block.mutable_block() = block_item.block();
_controller.submit_block( submit_block, _target_head.height() );This matters because:
apply_block_delta()trusts the stored receipt,submit_block()re-executes the block from the block body and reconstructs state from execution.
So for already-corrupted nodes, verify-blocks=true is not a workaround for the code fix. It is the required recovery path to escape corrupted persisted receipts already written before the fix existed.
If a node never persisted the bad receipts, this branch is enough. It prevents future corruption and normal indexing behavior can remain unchanged.
If a node already synced through the affected window and persisted bad receipts, the recommended recovery path is:
- Upgrade to a binary containing this fix.
- Restart with
chain.verify-blocks=true. - Let the node reindex by re-executing blocks.
- Once the node has rebuilt state from execution and is healthy again, decide whether to keep or remove
verify-blocksdepending on operational preference.
The key point is that step 2 is needed because the node's historical receipt data is already wrong.
This branch is intentionally scoped.
It does not:
- mutate old receipts already stored in
block_store, - introduce a fallback path inside
apply_block_delta(), - automatically detect old corrupted receipts and self-heal,
- change network protocol behavior.
It only fixes the persistence bug at the point where the wrong receipt was being written.
This branch is meant to stay clean and isolated relative to master:
- rectification receipt persistence fix,
- patch version bump to
1.5.1, - documentation for release and recovery context.
That makes it suitable as the base for a focused PR against the main branch later.